From 23f4c865f093d5bf12c259c3a4ec45f2802d2729 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Thu, 11 Aug 2022 08:02:36 -0400 Subject: [PATCH 01/14] chore: update version number to 1.5.0 --- docformatter.py | 2 +- pyproject.toml | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docformatter.py b/docformatter.py index 24db59c..7e7d5d6 100755 --- a/docformatter.py +++ b/docformatter.py @@ -59,7 +59,7 @@ except ImportError: TOMLI_INSTALLED = False -__version__ = "1.5.0-rc1" +__version__ = "1.5.0" if sys.version_info.major == 3: diff --git a/pyproject.toml b/pyproject.toml index 1517319..6ecbb69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "docformatter" -version = "1.4" +version = "1.5.0" description = "Formats docstrings to follow PEP 257" authors = ["Steven Myint"] maintainers = [ @@ -36,18 +36,19 @@ tomli = {version="1.2.3", optional=true} untokenize = "^0.1.1" [tool.poetry.dev-dependencies] +black = [ + {version = "^22.0.0", python = ">=3.6.2"}, +] +coverage = "^6.2.0" +docformatter = "^1.4" +isort = "^5.7.0" +mock = "^4.0.0" +pycodestyle = "^2.8.0" pydocstyle = "^6.1.1" pylint = "^2.12.0" -pycodestyle = "^2.8.0" -coverage = "^6.2.0" -rstcheck = "<6.0.0" pytest = "<7.0.0" pytest-cov = "^3.0.0" -mock = "^4.0.0" -isort = "^5.7.0" -black = [ - {version = "^22.0.0", python = ">=3.6.2"}, -] +rstcheck = "<6.0.0" [tool.poetry.scripts] docformatter = "docformatter:main" From 2ba74432f303f6ef693573adeab86fa9b210fc76 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Thu, 11 Aug 2022 09:45:35 -0400 Subject: [PATCH 02/14] docs: add REQUIREMENTS document --- README.rst | 9 +- docs/REQUIREMENTS.rst | 318 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 docs/REQUIREMENTS.rst diff --git a/README.rst b/README.rst index 7bf11cb..0773dfb 100644 --- a/README.rst +++ b/README.rst @@ -36,11 +36,11 @@ Formats docstrings to follow `PEP 257`_. .. _`PEP 257`: http://www.python.org/dev/peps/pep-0257/ - Features ======== -*docformatter* automatically formats docstrings to follow a subset of the PEP 257 conventions. Below are the relevant items quoted from PEP 257. +``docformatter`` automatically formats docstrings to follow a subset of the PEP +257 conventions. Below are the relevant items quoted from PEP 257. - For consistency, always use triple double quotes around docstrings. - Triple quotes are used even though the string fits on one line. @@ -50,12 +50,15 @@ Features - Unless the entire docstring fits on a line, place the closing quotes on a line by themselves. -docformatter also handles some of the PEP 8 conventions. +``docformatter`` also handles some of the PEP 8 conventions. - Don't write string literals that rely on significant trailing whitespace. Such trailing whitespace is visually indistinguishable and some editors (or more recently, reindent.py) will trim them. +See the `REQUIREMENTS`_ document for more details. + +.. _`REQUIREMENTS`: docs/REQUIREMENTS.rst Installation ============ diff --git a/docs/REQUIREMENTS.rst b/docs/REQUIREMENTS.rst new file mode 100644 index 0000000..157bae4 --- /dev/null +++ b/docs/REQUIREMENTS.rst @@ -0,0 +1,318 @@ +========================= +docformatter Requirements +========================= + +The goal of ``docformatter`` is to be an autoformatting tool for producing +PEP 257 compliant docstrings. This document provides a discussion of the +requirements from various sources for ``docformatter``. Every effor will be +made to keep this document up to date, but this is not a formal requirements +document and shouldn't be construed as such. + +PEP 257 Requirements +-------------------- + +PEP 257 provides conventions for docstrings. Conventions are general agreements +or customs of usage rather than strict engineering requirements. This is +appropriate for providing guidance to a broad community. In order to provide a +tool for automatically formatting or style checking docstrings, however, some +objective criteria is needed. Fortunately, the language of PEP 257 lends +itself to defining objective criteria, or requirements, for such tools. + +The conventions in PEP 257 define the high-level structure of docstrings: + + * How the docstring needs to be formatted. + * What information needs to be in a docstring. + +PEP 257 explicitly ignores markup syntax in the docstring; these are style +choices left to the individual or organization to enforce. This gives us two +categories of requirements in PEP 257. Let's call them *convention* +requirements and *methodology* requirements to be consistent with PEP 257 +terminology. + +An autoformatter should produce docstrings with the proper *convention* so tools +such as ``Docutils`` or ``pydocstyle`` can process them properly. The +contents of a docstring are irrelevant to tools like ``Docutils`` or +``pydocstyle``. An autoformatter may be able to produce some content, but +much of the content requirements would be difficult at best to satisfy +automatically. + +Requirements take one of three types, **shall**, **should**, and **may**. +Various sources provide definitions of, and synonyms for, these words. But +generally: + + * **Shall** represents an absolute. + * **Should** represents a goal. + * **May** represents an option. + +Thus, an autoformatting tool: + + * Must produce output that satisfies all the *convention* **shall** requirements. + * Ought to provide arguments to allow the user to dictate how each *convention* **should** or **may** requirement is interpreted. + * Would be nice to produce as much output that satisfies the *methodology* requirements. + * Would be nice to provide arguments to allow the user to turn on/off each *methodology* requirement the tool supports. + +Docstring Syntax +---------------- + +There are at least three "flavors" of docstrings in common use today; Sphinx, +NumPy, and Google. Each of these docstring flavors follow the PEP 257 +*convention* requirements. What differs between the three docstring flavors +is the reST syntax used in the elaborate description of the multi-line +docstring. + +For example, here is how each syntax documents function arguments. + +Google syntax: + +.. code-block:: + + Args: + param1 (int): The first parameter. + +NumPy syntax: + +.. code-block:: + + Parameters + ---------- + param1 : int + The first parameter. + +Sphinx syntax: + +.. code-block:: + + :param param1: The first parameter, defaults to 1. + :type: int + +Syntax is also important to ``Docutils``. An autoformatter should be aware of +syntactical directives so they can be placed properly in the structure of the +docstring. To accommodate the various syntax flavors used in docstrings, a +third requirement category is introduced, *style*. + +Another consideration in for the *style* category is line wrapping. +According to PEP 257, splitting a one-line docstring is to allow "Emacs’ +``fill-paragraph`` command" to be used. The ``fill-paragraph`` command is a +line-wrapping command. Additionally, it would be desirable to wrap +docstrings for visual continuity with the code. + +NumPy makes a stylistic decision to place a blank line after the long +description. + +Some code formatting tools also format docstrings. For example, black places +a space before a one-line or the summary line when that line begins with a +double quote ("). It would be desirable to provide the user an option to +have docformatter also insert this space for compatibility. + +Thus, an autoformatting tool: + + * Ought to provide arguments to allow the user to select the *style* or "flavor" of their choice. + * Ought to provide arguments to allow the user to, as seamlessly as possible, produce output of a compatible *style* with other formatting tools in the eco-system. + * Would be nice to to provide short cut arguments that represent aliases for a commonly used group of *style* arguments. + +Program Control +--------------- + +Finally, how the ``docformatter`` tool is used should have some user-defined +options to accommodate various use-cases. These could best be described as +*stakeholder* requirements. An autoformatting tool: + + * Ought to provide arguments to allow the user to integrate it into their existing workflow. + +Exceptions and Interpretations +`````````````````````````````` +As anyone who's ever been involved with turning a set of engineering +requirements into a real world product knows, they're never crystal clear and +they're always revised along the way. Interpreting and taking exception to +the requirements for an aerospace vehicle would be frowned upon without +involving the people who wrote the requirements. However, the consequences +for a PEP 257 autoformatting tool doing this are slightly less dire. We have +confidence the GitHub issue system is the appropriate mechanism if there's a +misinterpretation or inappropriate exception taken. + +The following items are exceptions or interpretations of the PEP 257 +requirements: + + * One-line and summary lines can end with any punctuation. ``docformatter`` will recognize any of [. ! ?]. Exception to requirement PEP_257_4.5; consistent with Google style. See also #56 for situations when this is not desired. + * One-line and summary lines will have the first word capitalized. ``docformatter`` will capitalize the first word for grammatical correctness. Interpretation of requirement PEP_257_4.5. + * PEP 257 discusses placing closing quotes on a new line in the multi-line section. However, it really makes no sense here as there is no way this condition could be met for a multi-line docstring. Given the basis provided in PEP 257, this requirement really applies to wrapped one-liners. Thus, this is assumed to apply to wrapped one-liners and the closing quotes will be placed on a line by themselves in this case. However, an argument will be provided to allow the user to select their desired behavior. Interpretation of requirement PEP_257_5.5. + +These give rise to the *derived* requirement category which would also cover +any requirements that must be met for a higher level requirement to be met. + +The table below summarizes the requirements for ``docformatter``. It +includes an ID for reference, the description from PEP 257, which category +the requirement falls in, the type of requirement, and whether +``docformatter`` has implemented the requirement. + +.. csv-table:: **PEP 257 Requirements Summary** + :align: left + :header: " ID", " Requirement", " Category", " Type", " Implemented" + :quote: ' + :widths: auto + + ' PEP_257_1','Always use """triple double quotes"""',' Convention',' Shall',' Yes' + ' PEP_257_2','Use r"""raw triple double quotes""" if you use backslashes.',' Convention',' Shall',' Yes' + ' PEP_257_3','Use u"""unicode triple double quotes""" for unicode docstrings.',' Convention',' Shall',' Yes' + ' PEP_257_4','**One-line docstrings:**' + ' PEP_257_4.1',' Should fit on a single line.',' Convention',' Should',' Yes' + ' PEP_257_4.2',' Use triple quotes.',' Convention',' Shall',' Yes' + ' PEP_257_4.3',' Closing quotes are on the same line as opening quotes.',' Convention',' Shall',' Yes' + ' PEP_257_4.4',' No blank line before or after the docstring.',' Convention',' Shall',' Yes' + ' PEP_257_4.5',' Is a phrase ending in a period.',' Convention',' Shall',' Yes' + ' docformatter_4.5.1', ' One-line docstrings may end in any of the following punctuation marks [. ! ?]', ' Derived', ' May', ' Yes' + ' docformatter_4.5.1', ' One-line docstrings will have the first word capitalized.', ' Derived', ' Shall', ' No' + ' PEP_257_5','**Multi-line docstrings:**' + ' PEP_257_5.1',' A summary is just like a one-line docstring.',' Convention',' Shall',' Yes' + ' docformatter_5.1.1', ' The summary line shall satisfy all the requirements of a one-line docstring.', ' Derived', ' Shall', ' Yes' + ' PEP_257_5.2',' The summary line may be on the same line as the opening quotes or the next line.',' Convention',' May',' Yes, with option' + ' PEP_257_5.3',' A blank line.', ' Convention', ' Shall',' Yes' + ' PEP_257_5.4',' A more elaborate description.',' Convention',' Shall',' Yes' + ' PEP_257_5.5',' Place the closing quotes on a line by themselves unless the entire docstring fits on a line.',' Convention',' Shall',' Yes, with option' + ' docformatter_5.5.1', ' An argument should be provided to allow the user to choose where the closing quotes are placed for one-line docstrings.', ' Derived', ' Should', ' Yes [*PR #104*]' + ' PEP_257_5.6',' Indented the same as the quotes at its first line.',' Convention',' Shall',' Yes' + ' PEP_257_6','**Class docstrings:**' + ' PEP_257_6.1',' Insert blank line after.',' Convention',' Shall',' Yes' + ' PEP_257_6.2',' Summarize its behavior.',' Methodology',' Should',' No' + ' PEP_257_6.3',' List the public methods and instance variables.',' Methodology',' Should',' No' + ' PEP_257_6.4',' List subclass interfaces separately.',' Methodology',' Should',' No' + ' PEP_257_6.5',' Class constructor should be documented in the __init__ method docstring.',' Methodology',' Should',' No' + ' PEP_257_6.6',' Use the verb "override" to indicate that a subclass method replaces a superclass method.',' Methodology',' Should',' No' + ' PEP_257_6.7',' Use the verb "extend" to indicate that a subclass method calls the superclass method and then has additional behavior.', ' Methodology',' Should',' No' + ' PEP_257_7','**Script docstring:**' + ' PEP_257_7.1',' Should be usable as its "usage" message.',' Methodology',' Should',' No' + ' PEP_257_7.2',' Should document the scripts function and command line syntax, environment variables, and files.',' Methodology',' Should',' No' + ' PEP_257_8','**Module and Package docstrings:**' + ' PEP_257_8.1',' List classes, exceptions, and functions that are exported by the module with a one-line summary of each.',' Methodology',' Should',' No' + ' PEP_257_9','**Function and Method docstrings:**' + ' PEP_257_9.1',' Summarize its behavior.',' Methodology',' Should',' No' + ' PEP_257_9.2',' Document its arguments, return values(s), side effects, exceptions raised, and restrictions on when it can be called.',' Methodology',' Should',' No' + ' PEP_257_9.3',' Optional arguments should be indicated.',' Methodology',' Should',' No' + ' PEP_257_9.4',' Should be documented whether keyword arguments are part of the interface.',' Methodology',' Should',' No' + ' docformatter_10', '**docstring Syntax**' + ' docformatter_10.1', ' Should wrap docstrings at n characters.', ' Style', ' Should', ' Yes' + ' docformatter_10.1.1', ' Shall not wrap lists or syntax directive statements', ' Derived', ' Shall', ' Yes' + ' docformatter_10.1.1.1', ' Should allow wrapping of lists and syntax directive statements.', ' Stakeholder', ' Should', ' Yes [*PR #5*, *PR #93*]' + ' docformatter_10.1.2', ' Should allow/disallow wrapping of one-line docstrings.', ' Derived', ' Should', ' No' + ' docformatter_10.2', ' Should format docstrings using NumPy style.', ' Style', ' Should', ' No' + ' docformatter_10.3', ' Should format docstrings using Google style.', ' Style', ' Should', ' No' + ' docformatter_10.4', ' Should format docstrings using Sphinx style.',' Style', ' Should', ' No' + ' docformatter_11', '**Program Control**' + ' docformatter_11.1', ' Should check formatting and report incorrectly documented docstrings.', ' Stakeholder', ' Should', ' Yes [*PR #32*]' + ' docformatter_11.2', ' Should fix formatting and save changes to file.', ' Stakeholder', ' Should', ' Yes' + ' docformatter_11.3', ' Should only format docstrings that are [minimum, maximum] lines long.', ' Stakeholder', ' Should', ' Yes [*PR #63*]' + ' docformatter_11.4', ' Should only format docstrings found between [start, end] lines in the file.', ' Stakeholder', ' Should', ' Yes [*PR #7*}' + ' docformatter_11.5', ' Should exclude processing directories and files by name.', ' Stakeholder', ' Should', ' Yes' + ' docformatter_11.6', ' Should recursively search directories for files to check and format.', ' Stakeholder', ' Should', ' Yes [*PR #44*]' + ' docformatter_11.7', ' Should be able to store configuration options in a configuration file.', ' Stakeholder', ' Should', ' Yes [*PR #77*]' + ' docformatter_11.7.1', ' Command line options shall take precedence over configuration file options.', ' Derived', ' Shall', ' Yes' + ' docformatter_11.8',' Should read docstrings from stdin and report results to stdout.', ' Stakeholder', ' Should', ' Yes [*PR #8*]' + +Requirement ID's that begin with PEP_257 are taken from PEP 257. Those +prefaced with docformatter are un-releated to PEP 257. + +Test Suite +---------- + +Each requirement in the table above should have one or more test in the test +suite to verify compliance. Ideally the test docstring will reference the +requirement(s) it is verifying to provide traceability. + +Current Implementation +---------------------- + +``docformatter`` currently provides the following arguments for interacting +with *convention* requirements. +:: + + --pre-summary-newline [boolean, default False] + Boolean to indicate whether to place the summary line on the line after + the opening quotes in a multi-line docstring. See requirement + PEP_257_5.2. + +The following are new arguments that are needed to implement **should** or +**may** *convention* requirements: +:: + + --wrap-one-line [boolean, default False] + Boolean to indicate whether to wrap one-line docstrings. Provides + option for requirement PEP_257_4.1. + +``docformatter`` currently provides these arguments for *style* requirements. +:: + + --blank [boolean, default False] + Boolean to indicate whether to add a blank line after the + elaborate description. + --close-quotes-on-newline [boolean, default False] + Boolean to indicate whether to place closing triple quotes on new line + for wrapped one-line docstrings. + --make-summary-multi-line [boolean, default False] + Boolean to indicate whether to add a newline before and after a + one-line docstring. This option results in non-conventional + docstrings; violates requirements PEP_257_4.1 and PEP_257_4.3. + --non-strict [boolean, default False] + Boolean to indicate whether to ignore strict compliance with reST list + syntax (see issue #67). + --pre-summary-space [boolean, default False] + Boolean to indicate whether to add a space between the opening triple + quotes and the first word in a one-line or summary line of a + multi-line docstring. + --tab-width [integer, defaults to 1] + Sets the number of characters represented by a tab when line + wrapping, for Richard Hendricks and others who use tabs instead of + spaces. + --wrap-descriptions length [integer, default 79] + Wrap long descriptions at this length. + --wrap-summaries length [integer, default 72] + Wrap long one-line docstrings and summary lines in multi-line + docstrings at this length. + +The following are new *style* arguments needed to accommodate the various style options: +:: + + --syntax [string, default "sphinx"] + One of sphinx, numpy, or google + --black [boolean, default False] + Formats docstrings to be compatible with black. + +``docformatter`` currently provides these arguments for *stakeholder* requirements. +:: + + --check + Only check and report incorrectly formatted files. + --config CONFIG + Path to the file containing docformatter options. + --docstring-length min_length max_length + Only format docstrings that are [min_length, max_length] rows long. + --exclude + Exclude directories and files by names. + --force-wrap + Force descriptions to be wrapped even if it may result in a mess. + This should likely be removed after implementing the syntax option. + --in-place + Make changes to files instead of printing diffs. + --range start end + Only format docstrings that are between [start, end] rows in the file. + --recursive + Drill down directories recursively. + +Issue and Version Management +---------------------------- + +As bug reports and feature requests arise in the GitHub issue system, these +will need to be prioritized. The requirement categories, coupled with the +urgency of the issue reported can be used to provide the general +prioritization scheme: + + * Priority 1: *convention* **bug** + * Priority 2: *style* **bug** + * Priority 3: *stakeholder* **bug** + * Priority 4: *convention* **enhancement** + * Priority 5: *style* **enhancement** + * Priority 6: *stakeholder* **enhancement** + * Priority 7: **chore** + +Integration of a bug fix will result in a patch version bump (i.e., 1.5.0 -> +1.5.1). Integration of one or more enhancements will result in a minor +version bump (i.e., 1.5.0 -> 1.6.0). \ No newline at end of file From cc395a1f9e7d9781869c9b2a7ec86d45fc09f325 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Thu, 11 Aug 2022 19:59:41 -0400 Subject: [PATCH 03/14] doc: create user documentation --- docs/Makefile | 20 ++ docs/make.bat | 35 +++ docs/source/conf.py | 28 ++ docs/source/index.rst | 21 ++ .../requirements.rst} | 0 docs/source/usage.rst | 268 ++++++++++++++++++ pyproject.toml | 6 +- 7 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst rename docs/{REQUIREMENTS.rst => source/requirements.rst} (100%) create mode 100644 docs/source/usage.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..908ed4a --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,28 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'docformatter' +copyright = '2022, Steven Myint' +author = 'Steven Myint' +release = '1.5.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [] + +templates_path = ['_templates'] +exclude_patterns = [] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..4956057 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,21 @@ +.. docformatter documentation master file, created by + sphinx-quickstart on Thu Aug 11 18:58:56 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to docformatter! +======================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + usage + requirements + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/REQUIREMENTS.rst b/docs/source/requirements.rst similarity index 100% rename from docs/REQUIREMENTS.rst rename to docs/source/requirements.rst diff --git a/docs/source/usage.rst b/docs/source/usage.rst new file mode 100644 index 0000000..583b516 --- /dev/null +++ b/docs/source/usage.rst @@ -0,0 +1,268 @@ +How to Use docformatter +======================= + +There are several ways you can use ``docformatter``. You can use it from the +command line, as a file watcher in PyCharm, in your pre-commit checks, and as +a GitHub action. However, before you can use ``docformatter``, you'll need +to install it. + +Installation +------------ +The latest released version of ``docformatter`` is available from PyPi. To +install it using pip: + +.. code-block:: console + + $ pip install --upgrade docformatter + +Or, if you want to use pyproject.toml to configure ``docformatter``: + +.. code-block:: console + + $ pip install --upgrade docformatter[tomli] + +If you'd like to use an unreleased version, you can also use pip to install +``docformatter`` from GitHub. + +.. code-block:: console + + $ python -m pip install git+https://github.com/PyCQA/docformatter.git@v1.5.0-rc1 + +Replace the tag ``v1.5.0-rc1`` with a commit SHA to install an untagged +version. + +Use from the Command Line +------------------------- + +To use ``docformatter`` from the command line, simply: + +.. code-block:: console + + $ docformatter name_of_python_file.py + +``docformatter`` recognizes a number of options for controlling how the tool +runs as well as how it will treat various patterns in the docstrings. The +help output provides a summary of these options: + +.. code-block:: console + + usage: docformatter [-h] [-i | -c] [-r] [--wrap-summaries length] + [--wrap-descriptions length] [--blank] + [--pre-summary-newline] [--make-summary-multi-line] + [--force-wrap] [--range start_line end_line] + [--docstring-length min_length max_length] + [--config CONFIG] [--version] + files [files ...] + + Formats docstrings to follow PEP 257. + + positional arguments: + files files to format or '-' for standard in + + optional arguments: + -h, --help show this help message and exit + -i, --in-place make changes to files instead of printing diffs + -c, --check only check and report incorrectly formatted files + -r, --recursive drill down directories recursively + -e, --exclude exclude directories and files by names + + --wrap-summaries length + wrap long summary lines at this length; set + to 0 to disable wrapping + (default: 79) + --wrap-descriptions length + wrap descriptions at this length; set to 0 to + disable wrapping + (default: 72) + --blank + add blank line after elaborate description + (default: False) + --pre-summary-newline + add a newline before one-line or the summary of a + multi-line docstring + (default: False) + --pre-summary-space + add a space between the opening triple quotes and + the first word in a one-line or summary line of a + multi-line docstring + (default: False) + --make-summary-multi-line + add a newline before and after a one-line docstring + (default: False) + --close-quotes-on-newline + place closing triple quotes on a new-line when a + one-line docstring wraps to two or more lines + (default: False) + --force-wrap + force descriptions to be wrapped even if it may result + in a mess (default: False) + --tab_width width + tabs in indentation are this many characters when + wrapping lines (default: 1) + --range start_line end_line + apply docformatter to docstrings between these lines; + line numbers are indexed at 1 + --docstring-length min_length max_length + apply docformatter to docstrings of given length range + --non-strict + do not strictly follow reST syntax to identify lists + (see issue #67) + (default: False) + --config CONFIG + path to file containing docformatter options + (default: ./pyproject.toml) + --version + show program's version number and exit + +Possible exit codes from ``docformatter``: + +- **1** - if any error encountered +- **2** - if it was interrupted +- **3** - if any file needs to be formatted (in ``--check`` mode) + +Use as a PyCharm File Watcher +----------------------------- + +``docformatter`` can be configured as a PyCharm file watcher to automatically +format docstrings on saving python files. + +Head over to ``Preferences > Tools > File Watchers``, click the ``+`` icon +and configure ``docformatter`` as shown below: + +.. image:: https://github.com/PyCQA/docformatter/blob/master/docs/images/pycharm-file-watcher-configurations.png?raw=true + :alt: PyCharm file watcher configurations + +Use with pre-commit +------------------- + +``docformatter`` is configured for `pre-commit`_ and can be set up as a hook +with the following ``.pre-commit-config.yaml`` configuration: + +.. _`pre-commit`: https://pre-commit.com/ + +.. code-block:: yaml + + - repo: https://github.com/PyCQA/docformatter + rev: v1.5.0 + hooks: + - id: docformatter + args: [--in-place --config ./pyproject.toml] + +You will need to install ``pre-commit`` and run ``pre-commit install``. + +Whether you use ``args: [--check]`` or ``args: [--in-place]``, the commit +will fail if ``docformatter`` processes a change. The ``--in-place`` option +fails because pre-commit does a diff check and fails if it detects a hook +changed a file. The ``--check`` option fails because ``docformatter`` returns +a non-zero exit code. + +Use with GitHub Actions +----------------------- + +``docformatter`` is one of the tools included in the `python-lint-plus`_ +action. + +.. _`python-lint-plus`: https://github.com/marketplace/actions/python-code-style-quality-and-lint + +How to Configure docformatter +============================= + +The command line options for ``docformatter`` can also be stored in a +configuration file. Currently only ``pyproject.toml``, ``setup.cfg``, and +``tox.ini`` are supported. The configuration file can be passed with a full +path. For example: + +.. code-block:: console + + $ docformatter --config ~/.secret/path/to/pyproject.toml + +If no configuration file is explicitly passed, ``docformatter`` will search +the current directory for the supported files and use the first one found. +The order of precedence is ``pyproject.toml``, ``setup.cfg``, then ``tox.ini``. + +In any of the configuration files, add a section ``[tool.docformatter]`` with +options listed using the same name as command line options. For example: + +.. code-block:: yaml + + [tool.docformatter] + recursive = true + wrap-summaries = 82 + blank = true + +The ``setup.cfg`` and ``tox.ini`` files will also support the +``[tool:docformatter]`` syntax. + +Known Issues and Idiosyncrasies +=============================== + +There are some know issues or idiosyncrasies when using ``docformatter``. +These are stylistic issues and are in the process of being addressed. + +Wrapping Descriptions +--------------------- + +``docformatter`` will wrap descriptions, but only in simple cases. If there is +text that seems like a bulleted/numbered list, ``docformatter`` will leave the +description as is: + +.. code-block:: rest + + - Item one. + - Item two. + - Item three. + +This prevents the risk of the wrapping turning things into a mess. To force +even these instances to get wrapped use ``--force-wrap``. This is being +addressed by the constellation of issues related to the various syntaxes used +in docstrings. + +Interaction with Black +---------------------- + +Black places a space between the opening triple quotes and the first +character, but only if the first character is a quote. Thus, black turns this: + +.. code-block:: rest + + """"Good" politicians don't exist.""" + +into this: + +.. code-block:: rest + + """ "Good" politicians don't exist.""" + +``docformatter`` will then turn this: + +.. code-block:: rest + + """ "Good" politicians don't exist.""" + +into this: + +.. code-block:: rest + + """"Good" politicians don't exist.""" + +If you pass the ``--pre-summary-space`` option to ``docformatter``, this: + +.. code-block:: rest + + """Good, politicians don't exist.""" + +becomes this: + +.. code-block:: rest + + """ Good, politicians don't exist.""" + +which black will turn into this: + +.. code-block:: rest + + """Good, politicians don't exist.""" + +For now, you'll have to decide whether you like chickens or eggs and then +execute the tools in the order you prefer. This is being addressed by issue +#94. diff --git a/pyproject.toml b/pyproject.toml index 3f1413c..6aa3406 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,19 +39,17 @@ untokenize = "^0.1.1" black = [ {version = "^22.0.0", python = ">=3.6.2"}, ] -coverage = "^6.2.0" +coverage = {extras = ["toml"], version = "^6.2.0"} docformatter = "^1.4" isort = "^5.7.0" mock = "^4.0.0" pycodestyle = "^2.8.0" pydocstyle = "^6.1.1" pylint = "^2.12.0" -pycodestyle = "^2.8.0" -coverage = {extras = ["toml"], version = "^6.2.0"} -rstcheck = "<6.0.0" pytest = "<7.0.0" pytest-cov = "^3.0.0" rstcheck = "<6.0.0" +Sphinx = "^5.0.0" [tool.poetry.scripts] docformatter = "docformatter:main" From 7c406b638b301f347703c76e8e79518c6cfae295 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Fri, 12 Aug 2022 08:52:09 -0400 Subject: [PATCH 04/14] docs: add license to documentation --- docs/source/index.rst | 1 + docs/source/license.rst | 4 ++++ pyproject.toml | 1 + 3 files changed, 6 insertions(+) create mode 100644 docs/source/license.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 4956057..ace6a0c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,6 +12,7 @@ Welcome to docformatter! usage requirements + license Indices and tables ================== diff --git a/docs/source/license.rst b/docs/source/license.rst new file mode 100644 index 0000000..40a7f6a --- /dev/null +++ b/docs/source/license.rst @@ -0,0 +1,4 @@ +License +======= + +.. literalinclude:: ../../LICENSE \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6aa3406..eb923ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -173,6 +173,7 @@ commands = pytest -s -x -c ./pyproject.toml --cache-clear \ --cov-config=pyproject.toml --cov=docformatter \ --cov-branch --cov-report=term tests/ + coverage xml --rcfile=pyproject.toml [testenv:style] deps = From 68cae939edfdd89369408db4f3b2ea808f631d44 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Fri, 12 Aug 2022 09:03:37 -0400 Subject: [PATCH 05/14] chore: add workflow to update AUTHORS file --- .github/workflows/do-update-authors.yml | 86 +++++++++++++++++++++++++ AUTHORS.rst | 10 +-- docs/source/authors.rst | 4 ++ docs/source/index.rst | 1 + 4 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/do-update-authors.yml create mode 100644 docs/source/authors.rst diff --git a/.github/workflows/do-update-authors.yml b/.github/workflows/do-update-authors.yml new file mode 100644 index 0000000..274d090 --- /dev/null +++ b/.github/workflows/do-update-authors.yml @@ -0,0 +1,86 @@ +--- +name: Update AUTHORS.rst + +# What this workflow does: +# 1. Update the AUTHORS.rst file +# 2. Git commit and push the file if there are changes. + +on: # yamllint disable-line rule:truthy + workflow_dispatch: + + push: + tags: + - "!*" + branches: + - master + +jobs: + update-authors: + name: Update AUTHORS.rst file + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v3 + + - name: Update AUTHORS.rst file + shell: python + run: | + import subprocess + + git_authors = subprocess.run( + ["git", "log", "--format=%aN <%aE>"], capture_output=True, check=True + ).stdout.decode() + + skip_list = ( + "Steven Myint", + "dependabot", + "pre-commit-ci", + "github-action", + "GitHub Actions", + ) + authors = [ + author + for author in set(git_authors.strip().split("\n")) + if not author.startswith(skip_list) + ] + authors.sort() + + file_head = ( + ".. This file is automatically generated/updated by a github actions workflow.\n" + ".. Every manual change will be overwritten on push to main.\n" + ".. You can find it here: ``.github/workflows/update-authors.yaml``\n" + ".. For more information see " + "`https://github.com/rstcheck/rstcheck/graphs/contributors`\n\n" + "Author\n" + "------\n" + "Steven Myint \n\n" + "Additional contributions by (sorted by name)\n" + "--------------------------------------------\n" + ) + + with open("AUTHORS.rst", "w") as authors_file: + authors_file.write(file_head) + authors_file.write("- ") + authors_file.write("\n- ".join(authors)) + authors_file.write("\n") + + - name: Check if diff + continue-on-error: true + run: > + git diff --exit-code AUTHORS.rst && + (echo "### No update" && exit 1) || (echo "### Commit update") + + - uses: EndBug/add-and-commit@v9 + name: Commit and push if diff + if: success() + with: + add: AUTHORS.rst + message: Update AUTHORS.rst file with new author(s) + author_name: GitHub Actions + author_email: action@github.com + committer_name: GitHub Actions + committer_email: actions@github.com + push: true \ No newline at end of file diff --git a/AUTHORS.rst b/AUTHORS.rst index 65db8d2..ca6ed24 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -2,12 +2,12 @@ Author ------ - Steven Myint (https://github.com/myint) -Patches -------- +Contributors +------------ +- Kapshuna Alexander (https://github.com/kapsh) - Cheng Xi Bao (https://github.com/happlebao) +- Peter Boothe (https://github.com/pboothe) - Andy Hayden (https://github.com/hayd) - Manuel Kaufmann (https://github.com/humitos) -- Peter Boothe (https://github.com/pboothe) -- Kapshuna Alexander (https://github.com/kapsh) -- Serhiy Yevtushenko (https://github.com/serhiy-yevtushenko) - Doyle Rowland (https://github.com/weibullguy) +- Serhiy Yevtushenko (https://github.com/serhiy-yevtushenko) diff --git a/docs/source/authors.rst b/docs/source/authors.rst new file mode 100644 index 0000000..f3e9708 --- /dev/null +++ b/docs/source/authors.rst @@ -0,0 +1,4 @@ +Authors +======= + +.. include:: ../../AUTHORS.rst \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index ace6a0c..e366493 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,6 +12,7 @@ Welcome to docformatter! usage requirements + authors license Indices and tables From 22ee950f1bb4d166d8cf476acbb85750adbc977f Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Fri, 12 Aug 2022 09:25:03 -0400 Subject: [PATCH 06/14] docs: split up usage.rst --- docs/source/configuration.rst | 29 ++++++++ docs/source/faq.rst | 74 ++++++++++++++++++++ docs/source/index.rst | 8 +++ docs/source/installation.rst | 33 +++++++++ docs/source/usage.rst | 128 ---------------------------------- 5 files changed, 144 insertions(+), 128 deletions(-) create mode 100644 docs/source/configuration.rst create mode 100644 docs/source/faq.rst create mode 100644 docs/source/installation.rst diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst new file mode 100644 index 0000000..88f4e24 --- /dev/null +++ b/docs/source/configuration.rst @@ -0,0 +1,29 @@ + +How to Configure docformatter +============================= + +The command line options for ``docformatter`` can also be stored in a +configuration file. Currently only ``pyproject.toml``, ``setup.cfg``, and +``tox.ini`` are supported. The configuration file can be passed with a full +path. For example: + +.. code-block:: console + + $ docformatter --config ~/.secret/path/to/pyproject.toml + +If no configuration file is explicitly passed, ``docformatter`` will search +the current directory for the supported files and use the first one found. +The order of precedence is ``pyproject.toml``, ``setup.cfg``, then ``tox.ini``. + +In any of the configuration files, add a section ``[tool.docformatter]`` with +options listed using the same name as command line options. For example: + +.. code-block:: yaml + + [tool.docformatter] + recursive = true + wrap-summaries = 82 + blank = true + +The ``setup.cfg`` and ``tox.ini`` files will also support the +``[tool:docformatter]`` syntax. diff --git a/docs/source/faq.rst b/docs/source/faq.rst new file mode 100644 index 0000000..3cee74f --- /dev/null +++ b/docs/source/faq.rst @@ -0,0 +1,74 @@ + +Known Issues and Idiosyncrasies +=============================== + +There are some know issues or idiosyncrasies when using ``docformatter``. +These are stylistic issues and are in the process of being addressed. + +Wrapping Descriptions +--------------------- + +``docformatter`` will wrap descriptions, but only in simple cases. If there is +text that seems like a bulleted/numbered list, ``docformatter`` will leave the +description as is: + +.. code-block:: rest + + - Item one. + - Item two. + - Item three. + +This prevents the risk of the wrapping turning things into a mess. To force +even these instances to get wrapped use ``--force-wrap``. This is being +addressed by the constellation of issues related to the various syntaxes used +in docstrings. + +Interaction with Black +---------------------- + +Black places a space between the opening triple quotes and the first +character, but only if the first character is a quote. Thus, black turns this: + +.. code-block:: rest + + """"Good" politicians don't exist.""" + +into this: + +.. code-block:: rest + + """ "Good" politicians don't exist.""" + +``docformatter`` will then turn this: + +.. code-block:: rest + + """ "Good" politicians don't exist.""" + +into this: + +.. code-block:: rest + + """"Good" politicians don't exist.""" + +If you pass the ``--pre-summary-space`` option to ``docformatter``, this: + +.. code-block:: rest + + """Good, politicians don't exist.""" + +becomes this: + +.. code-block:: rest + + """ Good, politicians don't exist.""" + +which black will turn into this: + +.. code-block:: rest + + """Good, politicians don't exist.""" + +For now, you'll have to decide whether you like chickens or eggs and then +execute the tools in the order you prefer. This is being addressed by issue +#94. diff --git a/docs/source/index.rst b/docs/source/index.rst index e366493..9205e72 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,8 +10,16 @@ Welcome to docformatter! :maxdepth: 2 :caption: Contents: + installation usage + configuration + +.. toctree:: + :maxdepth: 2 + :caption: Miscellaneous: + requirements + faq authors license diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 0000000..5134793 --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,33 @@ +How to Install docformatter +=========================== + +Install from PyPI +----------------- +The latest released version of ``docformatter`` is available from PyPI. To +install it using pip: + +.. code-block:: console + + $ pip install --upgrade docformatter + +Extras +`````` +If you want to use pyproject.toml to configure ``docformatter``, you'll need +to install with TOML support: + +.. code-block:: console + + $ pip install --upgrade docformatter[tomli] + +Install from GitHub +------------------- + +If you'd like to use an unreleased version, you can also use pip to install +``docformatter`` from GitHub. + +.. code-block:: console + + $ python -m pip install git+https://github.com/PyCQA/docformatter.git@v1.5.0-rc1 + +Replace the tag ``v1.5.0-rc1`` with a commit SHA to install an untagged +version. diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 583b516..236e905 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -6,31 +6,6 @@ command line, as a file watcher in PyCharm, in your pre-commit checks, and as a GitHub action. However, before you can use ``docformatter``, you'll need to install it. -Installation ------------- -The latest released version of ``docformatter`` is available from PyPi. To -install it using pip: - -.. code-block:: console - - $ pip install --upgrade docformatter - -Or, if you want to use pyproject.toml to configure ``docformatter``: - -.. code-block:: console - - $ pip install --upgrade docformatter[tomli] - -If you'd like to use an unreleased version, you can also use pip to install -``docformatter`` from GitHub. - -.. code-block:: console - - $ python -m pip install git+https://github.com/PyCQA/docformatter.git@v1.5.0-rc1 - -Replace the tag ``v1.5.0-rc1`` with a commit SHA to install an untagged -version. - Use from the Command Line ------------------------- @@ -163,106 +138,3 @@ Use with GitHub Actions action. .. _`python-lint-plus`: https://github.com/marketplace/actions/python-code-style-quality-and-lint - -How to Configure docformatter -============================= - -The command line options for ``docformatter`` can also be stored in a -configuration file. Currently only ``pyproject.toml``, ``setup.cfg``, and -``tox.ini`` are supported. The configuration file can be passed with a full -path. For example: - -.. code-block:: console - - $ docformatter --config ~/.secret/path/to/pyproject.toml - -If no configuration file is explicitly passed, ``docformatter`` will search -the current directory for the supported files and use the first one found. -The order of precedence is ``pyproject.toml``, ``setup.cfg``, then ``tox.ini``. - -In any of the configuration files, add a section ``[tool.docformatter]`` with -options listed using the same name as command line options. For example: - -.. code-block:: yaml - - [tool.docformatter] - recursive = true - wrap-summaries = 82 - blank = true - -The ``setup.cfg`` and ``tox.ini`` files will also support the -``[tool:docformatter]`` syntax. - -Known Issues and Idiosyncrasies -=============================== - -There are some know issues or idiosyncrasies when using ``docformatter``. -These are stylistic issues and are in the process of being addressed. - -Wrapping Descriptions ---------------------- - -``docformatter`` will wrap descriptions, but only in simple cases. If there is -text that seems like a bulleted/numbered list, ``docformatter`` will leave the -description as is: - -.. code-block:: rest - - - Item one. - - Item two. - - Item three. - -This prevents the risk of the wrapping turning things into a mess. To force -even these instances to get wrapped use ``--force-wrap``. This is being -addressed by the constellation of issues related to the various syntaxes used -in docstrings. - -Interaction with Black ----------------------- - -Black places a space between the opening triple quotes and the first -character, but only if the first character is a quote. Thus, black turns this: - -.. code-block:: rest - - """"Good" politicians don't exist.""" - -into this: - -.. code-block:: rest - - """ "Good" politicians don't exist.""" - -``docformatter`` will then turn this: - -.. code-block:: rest - - """ "Good" politicians don't exist.""" - -into this: - -.. code-block:: rest - - """"Good" politicians don't exist.""" - -If you pass the ``--pre-summary-space`` option to ``docformatter``, this: - -.. code-block:: rest - - """Good, politicians don't exist.""" - -becomes this: - -.. code-block:: rest - - """ Good, politicians don't exist.""" - -which black will turn into this: - -.. code-block:: rest - - """Good, politicians don't exist.""" - -For now, you'll have to decide whether you like chickens or eggs and then -execute the tools in the order you prefer. This is being addressed by issue -#94. From e3c1a02288106a74170cf4fb2ef686bf976ccf84 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Fri, 12 Aug 2022 09:30:13 -0400 Subject: [PATCH 07/14] docs: clean up README --- README.rst | 174 ++--------------------------------------------------- 1 file changed, 5 insertions(+), 169 deletions(-) diff --git a/README.rst b/README.rst index 0773dfb..b76291c 100644 --- a/README.rst +++ b/README.rst @@ -3,9 +3,11 @@ docformatter ============ .. |CI| image:: https://img.shields.io/github/workflow/status/PyCQA/docformatter/CI - :target: https://github.com/PyCQA/docformatter/actions/workflows/ci.yml + :target: https://github.com/PyCQA/docformatter/actions/workflows/ci.yml +.. |COVERALLS| image:: https://img.shields.io/coveralls/github/PyCQA/docformatter + :target: https://coveralls.io/github/PyCQA/docformatter .. |CONTRIBUTORS| image:: https://img.shields.io/github/contributors/PyCQA/docformatter - :target: https://github.com/PyCQA/docformatter/graphs/contributors + :target: https://github.com/PyCQA/docformatter/graphs/contributors .. |COMMIT| image:: https://img.shields.io/github/last-commit/PyCQA/docformatter .. |BLACK| image:: https://img.shields.io/badge/%20style-black-000000.svg :target: https://github.com/psf/black @@ -56,9 +58,7 @@ Features whitespace. Such trailing whitespace is visually indistinguishable and some editors (or more recently, reindent.py) will trim them. -See the `REQUIREMENTS`_ document for more details. - -.. _`REQUIREMENTS`: docs/REQUIREMENTS.rst +See the the full documentation at read-the-docs. Installation ============ @@ -155,161 +155,6 @@ gets formatted into this if factorial(10): launch_rocket() - -Options -======= - -Below is the help output:: - - usage: docformatter [-h] [-i | -c] [-r] [--wrap-summaries length] - [--wrap-descriptions length] [--blank] - [--pre-summary-newline] [--make-summary-multi-line] - [--force-wrap] [--range start_line end_line] - [--docstring-length min_length max_length] - [--config CONFIG] [--version] - files [files ...] - - Formats docstrings to follow PEP 257. - - positional arguments: - files files to format or '-' for standard in - - optional arguments: - -h, --help show this help message and exit - -i, --in-place make changes to files instead of printing diffs - -c, --check only check and report incorrectly formatted files - -r, --recursive drill down directories recursively - -e, --exclude exclude directories and files by names - - --wrap-summaries length - wrap long summary lines at this length; set - to 0 to disable wrapping - (default: 79) - --wrap-descriptions length - wrap descriptions at this length; set to 0 to - disable wrapping - (default: 72) - --blank - add blank line after elaborate description - (default: False) - --pre-summary-newline - add a newline before one-line or the summary of a - multi-line docstring - (default: False) - --pre-summary-space - add a space between the opening triple quotes and - the first word in a one-line or summary line of a - multi-line docstring - (default: False) - --make-summary-multi-line - add a newline before and after a one-line docstring - (default: False) - --close-quotes-on-newline - place closing triple quotes on a new-line when a - one-line docstring wraps to two or more lines - (default: False) - --force-wrap - force descriptions to be wrapped even if it may result - in a mess (default: False) - --tab_width width - tabs in indentation are this many characters when - wrapping lines (default: 1) - --range start_line end_line - apply docformatter to docstrings between these lines; - line numbers are indexed at 1 - --docstring-length min_length max_length - apply docformatter to docstrings of given length range - --non-strict - do not strictly follow reST syntax to identify lists - (see issue #67) - (default: False) - --config CONFIG - path to file containing docformatter options - (default: ./pyproject.toml) - --version - show program's version number and exit - -Possible exit codes: - -- **1** - if any error encountered -- **3** - if any file needs to be formatted (in ``--check`` mode) - -*docformatter* options can also be stored in a configuration file. Currently only -``pyproject.toml``, ``setup.cfg``, and ``tox.ini`` are supported. The configuration file can be passed with a full path. For example:: - - docformatter --config ~/.secret/path/to/pyproject.toml - -If no configuration file is passed explicitly, *docformatter* will search the current directory for the supported files and use the first one found. The order of precedence is ``pyproject.toml``, ``setup.cfg``, then ``tox.ini``. - -Add section ``[tool.docformatter]`` with options listed using the same name as command line options. For example:: - - [tool.docformatter] - recursive = true - wrap-summaries = 82 - blank = true - -The ``setup.cfg`` and ``tox.ini`` files will also support the ``[tool:docformatter]`` syntax. - -See the discussions in `issue_39`_ and `issue_94`_ regarding *docformatter* and -black interactions. - -.. _`issue_39`: https://github.com/PyCQA/docformatter/issues/39 -.. _`issue_94`: https://github.com/PyCQA/docformatter/issues/94 - -Wrapping Descriptions -===================== - -docformatter will wrap descriptions, but only in simple cases. If there is text -that seems like a bulleted/numbered list, docformatter will leave the -description as is:: - - - Item one. - - Item two. - - Item three. - -This prevents the risk of the wrapping turning things into a mess. To force -even these instances to get wrapped use ``--force-wrap``. - - -Integration -=========== - -Git Hook --------- - -*docformatter* is configured for `pre-commit`_ and can be set up as a hook with the following ``.pre-commit-config.yaml`` configuration: - -.. _`pre-commit`: https://pre-commit.com/ - -.. code-block:: yaml - - - repo: https://github.com/PyCQA/docformatter - rev: v1.4 - hooks: - - id: docformatter - args: [--in-place] - -You will need to install ``pre-commit`` and run ``pre-commit install``. - -Whether you use ``args: [--check]`` or ``args: [--in-place]``, the commit will fail if *docformatter* processes a change. The ``--in-place`` option fails because pre-commit does a diff check and fails if it detects a hook changed a file. The ``--check`` option fails because *docformatter* returns a non-zero exit code. - -PyCharm -------- - -*docformatter* can be configured as a PyCharm file watcher to automatically format docstrings on saving python files. - -Head over to ``Preferences > Tools > File Watchers``, click the ``+`` icon and configure *docformatter* as shown below: - -.. image:: https://github.com/PyCQA/docformatter/blob/master/docs/images/pycharm-file-watcher-configurations.png?raw=true - :alt: PyCharm file watcher configurations - -GitHub Actions --------------- - -*docformatter* is one of the tools included in the `python-lint-plus`_ action. - -.. _`python-lint-plus`: https://github.com/marketplace/actions/python-code-style-quality-and-lint - Marketing ========= Do you use *docformatter*? What style docstrings do you use? Add some badges to your project's **README** and let everyone know. @@ -344,18 +189,9 @@ Do you use *docformatter*? What style docstrings do you use? Add some badges t .. image:: https://img.shields.io/badge/%20style-sphinx-0a507a.svg :target: https://www.sphinx-doc.org/en/master/usage/index.html - Issues ====== Bugs and patches can be reported on the `GitHub page`_. .. _`GitHub page`: https://github.com/PyCQA/docformatter/issues - - -Links -===== - -* Coveralls_ - -.. _`Coveralls`: https://coveralls.io/r/myint/docformatter From 95189c38ebfe33a32135c9280d93974c82c1a415 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Fri, 12 Aug 2022 12:40:42 -0400 Subject: [PATCH 08/14] chore: add COVERAGE_FILE env variable --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index eb923ff..d370e9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,6 +168,8 @@ deps = pytest-cov coverage mock +setenv = + COVERAGE_FILE={toxinidir}/.coverage commands = pip install .[tomli] pytest -s -x -c ./pyproject.toml --cache-clear \ From 91739eb0cd1164d81f7b1d59e2aa378496885cbd Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Fri, 12 Aug 2022 12:49:40 -0400 Subject: [PATCH 09/14] chore: add conf.py to files for version update --- .github/workflows/on-push-tag.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/on-push-tag.yml b/.github/workflows/on-push-tag.yml index 7e5b929..6076f62 100644 --- a/.github/workflows/on-push-tag.yml +++ b/.github/workflows/on-push-tag.yml @@ -3,7 +3,7 @@ # - Job 1: # - Get new tag. # - Update CHANGELOG.md -# - Update VERSION, pyproject.toml, and docs/conf.py with new version. +# - Update pyproject.toml and docs/source/conf.py with new version. # - Set PR variables. # - Cut PR to merge files and create release/* branch. name: Push Version Tag Workflow @@ -56,13 +56,14 @@ jobs: stripGeneratorNotice: true verbose: true - - name: Update VERSION, pyproject.toml, and docs/conf.py + - name: Update pyproject.toml and docs/conf.py uses: vemel/nextversion@main with: path: ./pyproject.toml result: ${{ steps.newversion.outputs.new_version }} update: | ./pyproject.toml + ./docs/source/conf.py - name: Request release pull request uses: peter-evans/create-pull-request@v3 From c8e6e4cf4c688e68651b9b6a761d435bd88f99f0 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Fri, 12 Aug 2022 16:39:55 -0400 Subject: [PATCH 10/14] docs: add link to RTD in the README --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b76291c..8861a37 100644 --- a/README.rst +++ b/README.rst @@ -58,7 +58,9 @@ Features whitespace. Such trailing whitespace is visually indistinguishable and some editors (or more recently, reindent.py) will trim them. -See the the full documentation at read-the-docs. +See the the full documentation at `read-the-docs`_. + +.. _read-the-docs: https://docformatter.readthedocs.io Installation ============ From a7ed954d0f9faf8295d7b73e9c8ae3f6c3c593b2 Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Sat, 13 Aug 2022 10:18:00 -0400 Subject: [PATCH 11/14] chore: update pyproject.toml dependencies --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d370e9d..feddc64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,6 @@ black = [ {version = "^22.0.0", python = ">=3.6.2"}, ] coverage = {extras = ["toml"], version = "^6.2.0"} -docformatter = "^1.4" isort = "^5.7.0" mock = "^4.0.0" pycodestyle = "^2.8.0" @@ -50,6 +49,10 @@ pytest = "<7.0.0" pytest-cov = "^3.0.0" rstcheck = "<6.0.0" Sphinx = "^5.0.0" +twine = [ + {version="<4.0.0", python = "<3.7"}, + {version="^4.0.0", python = ">=3.7"}, +] [tool.poetry.scripts] docformatter = "docformatter:main" From 6eedc00e7e68c6f2611ae10329aac4db752f7b0c Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Sat, 13 Aug 2022 11:46:52 -0400 Subject: [PATCH 12/14] fix: AttributeError when no config file exists --- docformatter.py | 13 +++++++------ pyproject.toml | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/docformatter.py b/docformatter.py index 7e7d5d6..2e7ee98 100755 --- a/docformatter.py +++ b/docformatter.py @@ -128,6 +128,7 @@ def __init__(self, args: List[Union[bool, int, str]]) -> None: Any command line arguments passed during invocation. """ self.args_lst = args + self.config_file = "" self.parser = argparse.ArgumentParser( description=__doc__, prog="docformatter", @@ -143,7 +144,8 @@ def __init__(self, args: List[Union[bool, int, str]]) -> None: self.config_file = f"./{_configuration_file}" break - self._do_read_configuration_file() + if os.path.isfile(self.config_file): + self._do_read_configuration_file() def do_parse_arguments(self) -> None: """Parse configuration file and command line arguments.""" @@ -308,11 +310,10 @@ def do_parse_arguments(self) -> None: def _do_read_configuration_file(self) -> None: """Read docformatter options from a configuration file.""" - if os.path.isfile(self.config_file): - argfile = os.path.basename(self.config_file) - for f in self.configuration_file_lst: - if argfile == f: - break + argfile = os.path.basename(self.config_file) + for f in self.configuration_file_lst: + if argfile == f: + break fullpath, ext = os.path.splitext(self.config_file) filename = os.path.basename(fullpath) diff --git a/pyproject.toml b/pyproject.toml index feddc64..caeda9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,10 @@ include = ["LICENSE"] [tool.poetry.dependencies] python = "^3.6" -tomli = {version="1.2.3", optional=true} +tomli = [ + {version="<2.0.0", optional=true, python="<3.7"}, + {version="^2.0.0", optional=true, python=">=3.7"}, +] untokenize = "^0.1.1" [tool.poetry.dev-dependencies] @@ -54,6 +57,9 @@ twine = [ {version="^4.0.0", python = ">=3.7"}, ] +[tool.poetry.extras] +tomli = ["tomli"] + [tool.poetry.scripts] docformatter = "docformatter:main" @@ -163,14 +169,16 @@ legacy_tox_ini = """ [tox] skipsdist = True isolated_build = True -envlist = py{36,37,38,39,310,py36}, style +envlist = + py{36,37,38,39,310,py36}, + style [testenv:py{36,37,38,39,310,py36}] deps = - pytest - pytest-cov coverage mock + pytest + pytest-cov setenv = COVERAGE_FILE={toxinidir}/.coverage commands = From 796aa2e497f91edd7c2f7c5c08186429fc62760a Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Sat, 13 Aug 2022 21:39:27 -0400 Subject: [PATCH 13/14] test: update tests for _format_code now in class --- docformatter.py | 710 ++++++++++++++++++++------------------ tests/conftest.py | 127 +++++++ tests/test_format_code.py | 570 +++++++++++++++++++++++------- 3 files changed, 946 insertions(+), 461 deletions(-) diff --git a/docformatter.py b/docformatter.py index 2e7ee98..2cf2daa 100755 --- a/docformatter.py +++ b/docformatter.py @@ -46,9 +46,10 @@ import textwrap import tokenize from configparser import ConfigParser -from typing import List, Tuple, Union +from typing import List, TextIO, Tuple, Union # Third Party Imports +import _io import untokenize try: @@ -92,7 +93,7 @@ _PYTHON_LIBS = set(sysconfig.get_paths().values()) -class FormatResult(object): +class FormatResult: """Possible exit codes.""" ok = 0 @@ -207,7 +208,7 @@ def do_parse_arguments(self) -> None: metavar="width", default=int(self.flargs_dct.get("tab-width", 1)), help="tabs in indentation are this many characters when " - "wrapping lines (default: %(default)s)", + "wrapping lines (default: %(default)s)", ) self.parser.add_argument( "--blank", @@ -351,6 +352,368 @@ def _do_read_parser_configuration(self) -> None: } +class Formator: + """Format docstrings.""" + + parser = None + """Parser object.""" + + args: argparse.Namespace = None + + def __init__( + self, + args: argparse.Namespace, + stderror: TextIO, + stdin: TextIO, + stdout: TextIO, + ) -> None: + """Initialize a Formattor instance. + + Parameters + ---------- + args : argparse.Namespace + Any command line arguments passed during invocation or + configuration file options. + stderror : TextIO + The standard error device. Typically, the screen. + stdin : TextIO + The standard input device. Typically, the keyboard. + stdout : TextIO + The standard output device. Typically, the screen. + + Returns + ------- + object + """ + self.args = args + self.stderror: TextIO = stderror + self.stdin: TextIOr = stdin + self.stdout: TextIO = stdout + + def do_format_standard_in(self, parser: argparse.ArgumentParser): + """Print formatted text to standard out. + + Parameters + ---------- + parser: argparse.ArgumentParser + The argument parser containing the formatting options. + """ + if len(self.args.files) > 1: + parser.error("cannot mix standard in and regular files") + + if self.args.in_place: + parser.error("--in-place cannot be used with standard input") + + if self.args.recursive: + parser.error("--recursive cannot be used with standard input") + + encoding = None + source = self.stdin.read() + if not isinstance(source, unicode): + encoding = self.stdin.encoding or _get_encoding() + source = source.decode(encoding) + + formatted_source = _format_code_with_args(source, args=self.args) + + if encoding: + formatted_source = formatted_source.encode(encoding) + + self.stdout.write(formatted_source) + + def do_format_files(self): + """Format multiple files. + + Return + ------ + code: int + One of the FormatResult codes. + """ + outcomes = collections.Counter() + for filename in find_py_files( + set(self.args.files), self.args.recursive, self.args.exclude + ): + try: + result = self._do_format_file(filename) + outcomes[result] += 1 + if result == FormatResult.check_failed: + print(unicode(filename), file=self.stderror) + except IOError as exception: + outcomes[FormatResult.error] += 1 + print(unicode(exception), file=self.stderror) + + return_codes = [ # in order of preference + FormatResult.error, + FormatResult.check_failed, + FormatResult.ok, + ] + + for code in return_codes: + if outcomes[code]: + return code + + def _do_format_file(self, filename): + """Run format_code() on a file. + + Parameters + ---------- + filename: str + The path to the file to be formatted. + + Return + ------ + code: int + One of the FormatResult codes. + """ + encoding = detect_encoding(filename) + with open_with_encoding(filename, encoding=encoding) as input_file: + source = input_file.read() + formatted_source = self._do_format_code(source) + + if source != formatted_source: + if self.args.check: + return FormatResult.check_failed + elif self.args.in_place: + with open_with_encoding( + filename, mode="w", encoding=encoding + ) as output_file: + output_file.write(formatted_source) + else: + # Standard Library Imports + import difflib + + diff = difflib.unified_diff( + source.splitlines(), + formatted_source.splitlines(), + f"before/{filename}", + f"after/{filename}", + lineterm="", + ) + self.stdout.write("\n".join(list(diff) + [""])) + + return FormatResult.ok + + def _do_format_code(self, source): + """Return source code with docstrings formatted. + + Parameters + ---------- + source: str + The text from the source file. + """ + try: + original_newline = find_newline(source.splitlines(True)) + code = self._format_code(source) + + return normalize_line_endings( + code.splitlines(True), original_newline + ) + except (tokenize.TokenError, IndentationError): + return source + + def _format_code( + self, + source, + ): + """Return source code with docstrings formatted. + + Parameters + ---------- + source: str + The source code string. + + Returns + ------- + formatted_source: str + The source code with formatted docstrings. + """ + if not source: + return source + + if self.args.line_range is not None: + assert self.args.line_range[0] > 0 and self.args.line_range[1] > 0 + + if self.args.length_range is not None: + assert ( + self.args.length_range[0] > 0 and self.args.length_range[1] > 0 + ) + + modified_tokens = [] + + sio = io.StringIO(source) + previous_token_string = "" + previous_token_type = None + only_comments_so_far = True + + try: + for ( + token_type, + token_string, + start, + end, + line, + ) in tokenize.generate_tokens(sio.readline): + if ( + token_type == tokenize.STRING + and token_string.startswith(QUOTE_TYPES) + and ( + previous_token_type == tokenize.INDENT + or only_comments_so_far + ) + and is_in_range(self.args.line_range, start[0], end[0]) + and has_correct_length( + self.args.length_range, start[0], end[0] + ) + ): + indentation = ( + "" if only_comments_so_far else previous_token_string + ) + token_string = self._do_format_docstring( + indentation, + token_string, + ) + + if token_type not in [ + tokenize.COMMENT, + tokenize.NEWLINE, + tokenize.NL, + ]: + only_comments_so_far = False + + previous_token_string = token_string + previous_token_type = token_type + + # If the current token is a newline, the previous token was a + # newline or a comment, and these two sequential newlines follow a + # function definition, ignore the blank line. + if ( + len(modified_tokens) <= 2 + or token_type not in {tokenize.NL, tokenize.NEWLINE} + or modified_tokens[-1][0] + not in {tokenize.NL, tokenize.NEWLINE} + or modified_tokens[-2][1] != ":" + and modified_tokens[-2][0] != tokenize.COMMENT + or modified_tokens[-2][4][:3] != "def" + ): + modified_tokens.append( + (token_type, token_string, start, end, line) + ) + + return untokenize.untokenize(modified_tokens) + except tokenize.TokenError: + return source + + def _do_format_docstring( + self, + indentation: str, + docstring: str, + ) -> str: + """Return formatted version of docstring. + + Parameters + ---------- + indentation: str + The indentation characters for the docstring. + docstring: str + The docstring itself. + + Returns + ------- + docstring_formatted: str + The docstring formatted according the various options. + """ + contents, open_quote = strip_docstring(docstring) + open_quote = ( + f"{open_quote} " if self.args.pre_summary_space else open_quote + ) + + # Skip if there are nested triple double quotes + if contents.count(QUOTE_TYPES[0]): + return docstring + + # Do not modify things that start with doctests. + if contents.lstrip().startswith(">>>"): + return docstring + + summary, description = split_summary_and_description(contents) + + # Leave docstrings with underlined summaries alone. + if remove_section_header(description).strip() != description.strip(): + return docstring + + if not self.args.force_wrap and is_some_sort_of_list( + summary, + self.args.non_strict, + ): + # Something is probably not right with the splitting. + return docstring + + # Compensate for textwrap counting each tab in indentation as 1 + # character. + tab_compensation = indentation.count("\t") * (self.args.tab_width - 1) + self.args.wrap_summaries -= tab_compensation + self.args.wrap_descriptions -= tab_compensation + + if description: + # Compensate for triple quotes by temporarily prepending 3 spaces. + # This temporary prepending is undone below. + initial_indent = ( + indentation + if self.args.pre_summary_newline + else 3 * " " + indentation + ) + pre_summary = ( + "\n" + indentation if self.args.pre_summary_newline else "" + ) + summary = wrap_summary( + normalize_summary(summary), + wrap_length=self.args.wrap_summaries, + initial_indent=initial_indent, + subsequent_indent=indentation, + ).lstrip() + description = wrap_description( + description, + indentation=indentation, + wrap_length=self.args.wrap_descriptions, + force_wrap=self.args.force_wrap, + strict=self.args.non_strict, + ) + post_description = "\n" if self.args.post_description_blank else "" + return f'''\ +{open_quote}{pre_summary}{summary} + +{description}{post_description} +{indentation}"""\ +''' + else: + if not self.args.make_summary_multi_line: + summary_wrapped = wrap_summary( + open_quote + normalize_summary(contents) + '"""', + wrap_length=self.args.wrap_summaries, + initial_indent=indentation, + subsequent_indent=indentation, + ).strip() + if ( + self.args.close_quotes_on_newline + and "\n" in summary_wrapped + ): + summary_wrapped = ( + f"{summary_wrapped[:-3]}" + f"\n{indentation}" + f"{summary_wrapped[-3:]}" + ) + return summary_wrapped + else: + beginning = f"{open_quote}\n{indentation}" + ending = f'\n{indentation}"""' + summary_wrapped = wrap_summary( + normalize_summary(contents), + wrap_length=self.args.wrap_summaries, + initial_indent=indentation, + subsequent_indent=indentation, + ).strip() + return f"{beginning}{summary_wrapped}{ending}" + + def has_correct_length(length_range, start, end): """Return True if docstring's length is in range.""" if length_range is None: @@ -402,219 +765,6 @@ def _find_shortest_indentation(lines): return indentation or "" -def format_code(source, **kwargs): - """Return source code with docstrings formatted. - - Wrap summary lines if summary_wrap_length is greater than 0. - - See "_format_code()" for parameters. - """ - try: - original_newline = find_newline(source.splitlines(True)) - code = _format_code(source, **kwargs) - - return normalize_line_endings(code.splitlines(True), original_newline) - except (tokenize.TokenError, IndentationError): - return source - - -def _format_code( - source, - summary_wrap_length=79, - description_wrap_length=72, - force_wrap=False, - tab_width=1, - pre_summary_newline=False, - pre_summary_space=False, - make_summary_multi_line=False, - close_quotes_on_newline=False, - post_description_blank=False, - - line_range=None, - length_range=None, - strict=True, -): - """Return source code with docstrings formatted.""" - if not source: - return source - - if line_range is not None: - assert line_range[0] > 0 and line_range[1] > 0 - - if length_range is not None: - assert length_range[0] > 0 and length_range[1] > 0 - - modified_tokens = [] - - sio = io.StringIO(source) - previous_token_string = "" - previous_token_type = None - only_comments_so_far = True - - for ( - token_type, - token_string, - start, - end, - line, - ) in tokenize.generate_tokens(sio.readline): - if ( - token_type == tokenize.STRING - and token_string.startswith(QUOTE_TYPES) - and ( - previous_token_type == tokenize.INDENT or only_comments_so_far - ) - and is_in_range(line_range, start[0], end[0]) - and has_correct_length(length_range, start[0], end[0]) - ): - indentation = "" if only_comments_so_far else previous_token_string - - token_string = format_docstring( - indentation, - token_string, - summary_wrap_length=summary_wrap_length, - description_wrap_length=description_wrap_length, - force_wrap=force_wrap, - tab_width=tab_width, - pre_summary_newline=pre_summary_newline, - pre_summary_space=pre_summary_space, - make_summary_multi_line=make_summary_multi_line, - close_quotes_on_newline=close_quotes_on_newline, - post_description_blank=post_description_blank, - strict=strict, - ) - - if token_type not in [tokenize.COMMENT, tokenize.NEWLINE, tokenize.NL]: - only_comments_so_far = False - - previous_token_string = token_string - previous_token_type = token_type - - # If the current token is a newline, the previous token was a - # newline or a comment, and these two sequential newlines follow a - # function definition, ignore the blank line. - if ( - len(modified_tokens) <= 2 - or token_type not in {tokenize.NL, tokenize.NEWLINE} - or modified_tokens[-1][0] not in {tokenize.NL, tokenize.NEWLINE} - or modified_tokens[-2][1] != ":" - and modified_tokens[-2][0] != tokenize.COMMENT - or modified_tokens[-2][4][:3] != "def" - ): - modified_tokens.append( - (token_type, token_string, start, end, line) - ) - - return untokenize.untokenize(modified_tokens) - - -def format_docstring( - indentation, - docstring, - summary_wrap_length=0, - description_wrap_length=0, - force_wrap=False, - tab_width=1, - pre_summary_newline=False, - pre_summary_space=False, - make_summary_multi_line=False, - close_quotes_on_newline=False, - post_description_blank=False, - strict=True, -): - """Return formatted version of docstring. - - Wrap summary lines if summary_wrap_length is greater than 0. - - Relevant parts of PEP 257: - - For consistency, always use triple double quotes around docstrings. - - Triple quotes are used even though the string fits on one line. - - Multi-line docstrings consist of a summary line just like a one-line - docstring, followed by a blank line, followed by a more elaborate - description. - - Unless the entire docstring fits on a line, place the closing quotes - on a line by themselves. - """ - contents, open_quote = strip_docstring(docstring) - open_quote = f"{open_quote} " if pre_summary_space else open_quote - - # Skip if there are nested triple double quotes - if contents.count(QUOTE_TYPES[0]): - return docstring - - # Do not modify things that start with doctests. - if contents.lstrip().startswith(">>>"): - return docstring - - summary, description = split_summary_and_description(contents) - - # Leave docstrings with underlined summaries alone. - if remove_section_header(description).strip() != description.strip(): - return docstring - - if not force_wrap and is_some_sort_of_list(summary, strict): - # Something is probably not right with the splitting. - return docstring - - # Compensate for textwrap counting each tab in indentation as 1 character. - tab_compensation = indentation.count('\t') * (tab_width - 1) - summary_wrap_length -= tab_compensation - description_wrap_length -= tab_compensation - - if description: - # Compensate for triple quotes by temporarily prepending 3 spaces. - # This temporary prepending is undone below. - initial_indent = ( - indentation if pre_summary_newline else 3 * " " + indentation - ) - pre_summary = "\n" + indentation if pre_summary_newline else "" - summary = wrap_summary( - normalize_summary(summary), - wrap_length=summary_wrap_length, - initial_indent=initial_indent, - subsequent_indent=indentation, - ).lstrip() - description = wrap_description( - description, - indentation=indentation, - wrap_length=description_wrap_length, - force_wrap=force_wrap, - strict=strict, - ) - post_description = "\n" if post_description_blank else "" - return f'''\ -{open_quote}{pre_summary}{summary} - -{description}{post_description} -{indentation}"""\ -''' - else: - if not make_summary_multi_line: - summary_wrapped = wrap_summary( - open_quote + normalize_summary(contents) + '"""', - wrap_length=summary_wrap_length, - initial_indent=indentation, - subsequent_indent=indentation, - ).strip() - if close_quotes_on_newline and "\n" in summary_wrapped: - summary_wrapped = ( - f"{summary_wrapped[:-3]}" - f"\n{indentation}" - f"{summary_wrapped[-3:]}" - ) - return summary_wrapped - else: - beginning = f"{open_quote}\n{indentation}" - ending = f'\n{indentation}"""' - summary_wrapped = wrap_summary( - normalize_summary(contents), - wrap_length=summary_wrap_length, - initial_indent=indentation, - subsequent_indent=indentation, - ).strip() - return f"{beginning}{summary_wrapped}{ending}" - - def is_probably_beginning_of_sentence(line): """Return True if this line begins a new sentence.""" # Check heuristically for a parameter list. @@ -949,101 +1099,24 @@ def detect_encoding(filename): return "latin-1" -def format_file(filename, args, standard_out): - """Run format_code() on a file. - - Return: one of the FormatResult codes. - """ - encoding = detect_encoding(filename) - with open_with_encoding(filename, encoding=encoding) as input_file: - source = input_file.read() - formatted_source = _format_code_with_args(source, args) - - if source != formatted_source: - if args.check: - return FormatResult.check_failed - elif args.in_place: - with open_with_encoding( - filename, mode="w", encoding=encoding - ) as output_file: - output_file.write(formatted_source) - else: - # Standard Library Imports - import difflib - - diff = difflib.unified_diff( - source.splitlines(), - formatted_source.splitlines(), - f"before/{filename}", - f"after/{filename}", - lineterm="", - ) - standard_out.write("\n".join(list(diff) + [""])) - - return FormatResult.ok - - -def _format_code_with_args(source, args): - """Run format_code with parsed command-line arguments.""" - return format_code( - source, - summary_wrap_length=args.wrap_summaries, - description_wrap_length=args.wrap_descriptions, - force_wrap=args.force_wrap, - tab_width=args.tab_width, - pre_summary_newline=args.pre_summary_newline, - pre_summary_space=args.pre_summary_space, - make_summary_multi_line=args.make_summary_multi_line, - close_quotes_on_newline=args.close_quotes_on_newline, - post_description_blank=args.post_description_blank, - line_range=args.line_range, - strict=not args.non_strict, - ) - - def _main(argv, standard_out, standard_error, standard_in): """Run internal main entry point.""" configurator = Configurator(argv) configurator.do_parse_arguments() + formator = Formator( + configurator.args, + stderror=standard_error, + stdin=standard_in, + stdout=standard_out, + ) + if "-" in configurator.args.files: - _format_standard_in( - configurator.args, - parser=configurator.parser, - standard_out=standard_out, - standard_in=standard_in, + formator.do_format_standard_in( + configurator.parser, ) else: - return _format_files( - configurator.args, - standard_out=standard_out, - standard_error=standard_error, - ) - - -def _format_standard_in(args, parser, standard_out, standard_in): - """Print formatted text to standard out.""" - if len(args.files) > 1: - parser.error("cannot mix standard in and regular files") - - if args.in_place: - parser.error("--in-place cannot be used with standard input") - - if args.recursive: - parser.error("--recursive cannot be used with standard input") - - encoding = None - source = standard_in.read() - if not isinstance(source, unicode): - encoding = standard_in.encoding or _get_encoding() - source = source.decode(encoding) - - formatted_source = _format_code_with_args(source, args=args) - - if encoding: - formatted_source = formatted_source.encode(encoding) - - standard_out.write(formatted_source) + return formator.do_format_files() def _get_encoding(): @@ -1104,37 +1177,6 @@ def is_excluded(name, exclude): yield name -def _format_files(args, standard_out, standard_error): - """Format multiple files. - - Return: one of the FormatResult codes. - """ - outcomes = collections.Counter() - for filename in find_py_files( - set(args.files), args.recursive, args.exclude - ): - try: - result = format_file( - filename, args=args, standard_out=standard_out - ) - outcomes[result] += 1 - if result == FormatResult.check_failed: - print(unicode(filename), file=standard_error) - except IOError as exception: - outcomes[FormatResult.error] += 1 - print(unicode(exception), file=standard_error) - - return_codes = [ # in order of preference - FormatResult.error, - FormatResult.check_failed, - FormatResult.ok, - ] - - for code in return_codes: - if outcomes[code]: - return code - - def main(): """Run main entry point.""" # SIGPIPE is not available on Windows. diff --git a/tests/conftest.py b/tests/conftest.py index d2b1c90..318ddd3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ """docformatter test suite configuration file.""" # Standard Library Imports +import argparse import os import shutil import subprocess @@ -100,3 +101,129 @@ def run_docformatter(arguments, temporary_file): stdin=subprocess.PIPE, env=environ, ) + + +@pytest.fixture(scope="function") +def test_args(args): + """Create a set of arguments to use with tests. + + To pass no arguments, just an empty file name: + @pytest.mark.parametrize("test_args", [[""]]) + + To pass an argument and empty file name: + @pytest.mark.parametrize("test_args", [["wrap-summaries", "79", ""]]) + """ + parser = argparse.ArgumentParser( + description="parser object for docformatter tests", + prog="docformatter", + ) + + changes = parser.add_mutually_exclusive_group() + changes.add_argument( + "-i", + "--in-place", + action="store_true", + ) + changes.add_argument( + "-c", + "--check", + action="store_true", + ) + parser.add_argument( + "-r", + "--recursive", + action="store_true", + default=False, + ) + parser.add_argument( + "-e", + "--exclude", + nargs="*", + ) + parser.add_argument( + "--wrap-summaries", + default=79, + type=int, + metavar="length", + ) + parser.add_argument( + "--wrap-descriptions", + default=72, + type=int, + metavar="length", + ) + parser.add_argument( + "--force-wrap", + action="store_true", + default=False, + ) + parser.add_argument( + "--tab_width", + type=int, + dest="tab_width", + metavar="width", + default=1, + ) + parser.add_argument( + "--blank", + dest="post_description_blank", + action="store_true", + default=False, + ) + parser.add_argument( + "--pre-summary-newline", + action="store_true", + default=False, + ) + parser.add_argument( + "--pre-summary-space", + action="store_true", + default=False, + ) + parser.add_argument( + "--make-summary-multi-line", + action="store_true", + default=False, + ) + parser.add_argument( + "--close-quotes-on-newline", + action="store_true", + default=False, + ) + parser.add_argument( + "--range", + metavar="line", + dest="line_range", + default=None, + type=int, + nargs=2, + ) + parser.add_argument( + "--docstring-length", + metavar="length", + dest="length_range", + default=None, + type=int, + nargs=2, + ) + parser.add_argument( + "--non-strict", + action="store_true", + default=False, + ) + parser.add_argument( + "--config", + ) + parser.add_argument( + "--version", + action="version", + version="test version", + ) + parser.add_argument( + "files", + nargs="+", + ) + + _test_args = parser.parse_args(args) + + yield _test_args diff --git a/tests/test_format_code.py b/tests/test_format_code.py index 706e35d..a830e32 100644 --- a/tests/test_format_code.py +++ b/tests/test_format_code.py @@ -24,91 +24,72 @@ # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -"""Module for testing the format_code() function.""" +"""Module for testing the Formattor._do_format_code() function.""" + +# Standard Library Imports +import sys # Third Party Imports import pytest # docformatter Package Imports import docformatter +from docformatter import Formator class TestFormatCode: - """Class for testing format_code() with no arguments.""" + """Class for testing _format_code() with no arguments.""" @pytest.mark.unit - def test_format_code(self): - """Should place one-liner on single line.""" - assert '''\ -def foo(): - """Hello foo.""" -''' == docformatter.format_code( - '''\ -def foo(): - """ - Hello foo. - """ -''' - ) - - @pytest.mark.unit - def test_format_code_with_module_docstring(self): - """Should format module docstrings.""" - assert '''\ -#!/usr/env/bin python -"""This is a module docstring. - -1. One -2. Two -""" - -"""But -this -is -not.""" -''' == docformatter.format_code( - '''\ -#!/usr/env/bin python -"""This is -a module -docstring. - -1. One -2. Two -""" - -"""But -this -is -not.""" -''' + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_should_ignore_non_docstring(self, test_args, args): + """Should ignore triple quoted strings that are assigned values.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, ) - @pytest.mark.unit - def test_format_code_should_ignore_non_docstring(self): - """Shoud ignore triple quoted strings that are assigned values.""" source = '''\ x = """This is -not.""" +not a +docstring.""" ''' - assert source == docformatter.format_code(source) + assert source == uut._format_code(source) @pytest.mark.unit - def test_format_code_with_empty_string(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_empty_string(self, test_args, args): """Should do nothing with an empty string.""" - assert "" == docformatter.format_code("") - assert "" == docformatter.format_code("") + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert "" == uut._format_code("") + assert "" == uut._format_code("") @pytest.mark.unit - def test_format_code_with_tabs(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_tabs(self, test_args, args): """Should retain tabbed indentation.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): \t"""Hello foo.""" \tif True: \t\tx = 1 -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): \t""" @@ -120,14 +101,22 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_mixed_tabs(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_mixed_tabs(self, test_args, args): """Should retain mixed tabbed and spaced indentation.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): \t"""Hello foo.""" \tif True: \t x = 1 -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): \t""" @@ -139,13 +128,21 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_escaped_newlines(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_escaped_newlines(self, test_args, args): """Should leave escaped newlines in code untouched.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert r'''def foo(): """Hello foo.""" x = \ 1 -''' == docformatter.format_code( +''' == uut._format_code( r'''def foo(): """ Hello foo. @@ -156,15 +153,23 @@ def test_format_code_with_escaped_newlines(self): ) @pytest.mark.unit - def test_format_code_with_comments(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_comments(self, test_args, args): """Should leave comments as is.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert r''' def foo(): """Hello foo.""" # My comment # My comment with escape \ 123 -'''.lstrip() == docformatter.format_code( +'''.lstrip() == uut._format_code( r''' def foo(): """ @@ -177,13 +182,23 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_escaped_newline_in_inline_comment(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_escaped_newline_in_inline_comment( + self, test_args, args + ): """Should leave code with inline comment as is.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert r''' def foo(): """Hello foo.""" def test_method_no_chr_92(): the501(92) # \ -'''.lstrip() == docformatter.format_code( +'''.lstrip() == uut._format_code( r''' def foo(): """ @@ -194,16 +209,24 @@ def test_method_no_chr_92(): the501(92) # \ ) @pytest.mark.unit - def test_format_code_raw_docstring_double_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_raw_docstring_double_quotes(self, test_args, args): """Should format raw docstrings with triple double quotes. See requirement PEP_257_2. See issue #54 for request to handle raw docstrings. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): r"""Hello raw foo.""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): r""" @@ -215,7 +238,7 @@ def foo(): assert '''\ def foo(): R"""Hello Raw foo.""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): R""" @@ -225,16 +248,24 @@ def foo(): ) @pytest.mark.unit - def test_format_code_raw_docstring_single_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_raw_docstring_single_quotes(self, test_args, args): """Should format raw docstrings with triple single quotes. See requirement PEP_257_2. See issue #54 for request to handle raw docstrings. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): r"""Hello raw foo.""" -''' == docformatter.format_code( +''' == uut._format_code( """\ def foo(): r''' @@ -246,7 +277,7 @@ def foo(): assert '''\ def foo(): R"""Hello Raw foo.""" -''' == docformatter.format_code( +''' == uut._format_code( """\ def foo(): R''' @@ -256,16 +287,26 @@ def foo(): ) @pytest.mark.unit - def test_format_code_unicode_docstring_double_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_unicode_docstring_double_quotes( + self, test_args, args + ): """Should format unicode docstrings with triple double quotes. See requirement PEP_257_3. See issue #54 for request to handle raw docstrings. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): u"""Hello unicode foo.""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): u""" @@ -277,7 +318,7 @@ def foo(): assert '''\ def foo(): U"""Hello Unicode foo.""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): U""" @@ -287,16 +328,26 @@ def foo(): ) @pytest.mark.unit - def test_format_code_unicode_docstring_single_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_unicode_docstring_single_quotes( + self, test_args, args + ): """Should format unicode docstrings with triple single quotes. See requirement PEP_257_3. See issue #54 for request to handle raw docstrings. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): u"""Hello unicode foo.""" -''' == docformatter.format_code( +''' == uut._format_code( """\ def foo(): u''' @@ -308,7 +359,7 @@ def foo(): assert '''\ def foo(): U"""Hello Unicode foo.""" -''' == docformatter.format_code( +''' == uut._format_code( """\ def foo(): U''' @@ -318,25 +369,41 @@ def foo(): ) @pytest.mark.unit - def test_format_code_skip_nested(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_skip_nested(self, test_args, args): """Should ignore nested triple quotes.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + code = """\ def foo(): '''Hello foo. \"\"\"abc\"\"\" ''' """ - assert code == docformatter.format_code(code) + assert code == uut._format_code(code) @pytest.mark.unit - def test_format_code_with_multiple_sentences(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_multiple_sentences(self, test_args, args): """Should create multi-line docstring from multiple sentences.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo. This is a docstring. """ -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): """ @@ -347,15 +414,25 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_multiple_sentences_same_line(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_multiple_sentences_same_line( + self, test_args, args + ): """Should create multi-line docstring from multiple sentences.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo. This is a docstring. """ -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): """ @@ -365,15 +442,25 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_multiple_sentences_multi_line_summary(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_multiple_sentences_multi_line_summary( + self, test_args, args + ): """Should put summary line on a single line.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo. This is a docstring. """ -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): """ @@ -384,15 +471,23 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_empty_lines(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_empty_lines(self, test_args, args): """Summary line on one line when wrapped, followed by empty line.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo and this is a docstring. More stuff. """ -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): """ @@ -405,15 +500,23 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_trailing_whitespace(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_trailing_whitespace(self, test_args, args): """Should strip trailing whitespace.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo and this is a docstring. More stuff. """ -''' == docformatter.format_code( +''' == uut._format_code( ( '''\ def foo(): @@ -428,8 +531,16 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_parameters_list(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_parameters_list(self, test_args, args): """Should treat parameters list as elaborate description.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Test. @@ -437,7 +548,7 @@ def foo(): one - first two - second """ -''' == docformatter.format_code( +''' == uut._format_code( ( '''\ def foo(): @@ -450,16 +561,24 @@ def foo(): ) @pytest.mark.unit - def test_ignore_code_with_single_quote(self): + @pytest.mark.parametrize("args", [[""]]) + def test_ignore_code_with_single_quote(self, test_args, args): """Single single quote on first line of code should remain untouched. See requirement PEP_257_1. See issue #66 for example of docformatter breaking code when encountering single quote. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert """\ def foo(): 'Just a regular string' -""" == docformatter.format_code( +""" == uut._format_code( """\ def foo(): 'Just a regular string' @@ -467,16 +586,24 @@ def foo(): ) @pytest.mark.unit - def test_ignore_code_with_double_quote(self): + @pytest.mark.parametrize("args", [[""]]) + def test_ignore_code_with_double_quote(self, test_args, args): """Single double quotes on first line of code should remain untouched. See requirement PEP_257_1. See issue #66 for example of docformatter breaking code when encountering single quote. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert """\ def foo(): "Just a regular string" -""" == docformatter.format_code( +""" == uut._format_code( """\ def foo(): "Just a regular string" @@ -484,21 +611,39 @@ def foo(): ) @pytest.mark.unit - def test_format_code_should_skip_nested_triple_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_should_skip_nested_triple_quotes( + self, test_args, args + ): """Should ignore triple quotes nested in a string.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + line = '''\ def foo(): 'Just a """foo""" string' ''' - assert line == docformatter.format_code(line) + assert line == uut._format_code(line) @pytest.mark.unit - def test_format_code_with_assignment_on_first_line(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_assignment_on_first_line(self, test_args, args): """Should ignore triple quotes in variable assignment.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): x = """Just a regular string. Alpha.""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): x = """Just a regular string. Alpha.""" @@ -506,8 +651,16 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_regular_strings_too(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_regular_strings_too(self, test_args, args): """Should ignore triple quoted strings after the docstring.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def foo(): """Hello foo and this is a docstring. @@ -520,7 +673,7 @@ def foo(): """More stuff that should not be touched\t""" -''' == docformatter.format_code( +''' == uut._format_code( '''\ def foo(): """ @@ -539,23 +692,59 @@ def foo(): ) @pytest.mark.unit - def test_format_code_with_syntax_error(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_syntax_error(self, test_args, args): """Should ignore single set of triple quotes followed by newline.""" - assert '"""\n' == docformatter.format_code('"""\n') + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""\n' == uut._format_code('"""\n') @pytest.mark.unit - def test_format_code_with_syntax_error_case_slash_r(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_syntax_error_case_slash_r(self, test_args, args): """Should ignore single set of triple quotes followed by return.""" - assert '"""\r' == docformatter.format_code('"""\r') + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""\r' == uut._format_code('"""\r') @pytest.mark.unit - def test_format_code_with_syntax_error_case_slash_r_slash_n(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_with_syntax_error_case_slash_r_slash_n( + self, test_args, args + ): """Should ignore single triple quote followed by return, newline.""" - assert '"""\r\n' == docformatter.format_code('"""\r\n') + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""\r\n' == uut._format_code('"""\r\n') @pytest.mark.unit - def test_format_code_dominant_line_ending_style_preserved(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_dominant_line_ending_style_preserved( + self, test_args, args + ): """Should retain carriage return line endings.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + goes_in = '''\ def foo():\r """\r @@ -572,29 +761,49 @@ def foo():\r \r This is a docstring.\r """\r -''' == docformatter.format_code( +''' == uut._do_format_code( goes_in ) @pytest.mark.unit - def test_format_code_additional_empty_line_before_doc(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_additional_empty_line_before_doc( + self, test_args, args + ): """Should remove empty line between function def and docstring. See issue #51. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( '\n\n\ndef my_func():\n"""Summary of my function."""\npass' - == docformatter._format_code( + == uut._do_format_code( '\n\n\ndef my_func():\n\n"""Summary of my function."""\npass' ) ) @pytest.mark.unit - def test_format_code_extra_newline_following_comment(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_extra_newline_following_comment( + self, test_args, args + ): """Should remove extra newline following in-line comment. See issue #51. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''\ def crash_rocket(location): # pragma: no cover @@ -604,53 +813,80 @@ def crash_rocket(location): # pragma: no cover assert '''\ def crash_rocket(location): # pragma: no cover """This is a docstring following an in-line comment.""" - return location''' == docformatter._format_code( + return location''' == uut._do_format_code( docstring ) @pytest.mark.unit - def test_format_code_no_docstring(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_code_no_docstring(self, test_args, args): """Should leave code as is if there is no docstring. See issue #97. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = ( "def pytest_addoption(parser: pytest.Parser) -> " "None:\n register_toggle.pytest_addoption(parser)\n" ) - assert docstring == docformatter._format_code(docstring) + assert docstring == uut._do_format_code(docstring) docstring = ( "def pytest_addoption(parser: pytest.Parser) -> " "None: # pragma: no cover\n " "register_toggle.pytest_addoption(parser)\n" ) - assert docstring == docformatter._format_code(docstring) + assert docstring == uut._do_format_code(docstring) + class TestFormatCodeRanges: - """Class for testing format_code() with the line_range or - length_range arguments.""" + """Class for testing _format_code() with the line_range or length_range + arguments.""" @pytest.mark.unit - def test_format_code_range_miss(self): + @pytest.mark.parametrize("args", [["--range", "1", "1", ""]]) + def test_format_code_range_miss(self, test_args, args): """Should leave docstrings outside line range as is.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" - pass''' == docformatter.format_code('''\ + pass''' == uut._format_code( + '''\ def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" - pass''', line_range=[1, 1]) + pass''' + ) @pytest.mark.unit - def test_format_code_range_hit(self): + @pytest.mark.parametrize("args", [["--range", "1", "2", ""]]) + def test_format_code_range_hit(self, test_args, args): """Should format docstrings within line_range.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def f(x): """This is a docstring. @@ -660,17 +896,27 @@ def f(x): pass def g(x): """ Badly indented docstring""" - pass''' == docformatter.format_code('''\ + pass''' == uut._format_code( + '''\ def f(x): """ This is a docstring. That should be on more lines""" pass def g(x): """ Badly indented docstring""" - pass''', line_range=[1, 2]) + pass''' + ) @pytest.mark.unit - def test_format_code_docstring_length(self): + @pytest.mark.parametrize("args", [["--docstring-length", "1", "1", ""]]) + def test_format_code_docstring_length(self, test_args, args): """Should leave docstrings outside length_range as is.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''\ def f(x): """This is a docstring. @@ -681,7 +927,8 @@ def f(x): pass def g(x): """Badly indented docstring.""" - pass''' == docformatter.format_code('''\ + pass''' == uut._format_code( + '''\ def f(x): """This is a docstring. @@ -691,4 +938,73 @@ def f(x): pass def g(x): """ Badly indented docstring""" - pass''', length_range=[1, 1]) + pass''' + ) + + +class TestDoFormatCode: + """Class for testing _do_format_code() with no arguments.""" + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_do_format_code(self, test_args, args): + """Should place one-liner on single line.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '''\ +def foo(): + """Hello foo.""" +''' == uut._do_format_code( + '''\ +def foo(): + """ + Hello foo. + """ +''' + ) + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_do_format_code_with_module_docstring(self, test_args, args): + """Should format module docstrings.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '''\ +#!/usr/env/bin python +"""This is a module docstring. + +1. One +2. Two +""" + +"""But +this +is +not.""" +''' == uut._do_format_code( + '''\ +#!/usr/env/bin python +"""This is +a module +docstring. + +1. One +2. Two +""" + +"""But +this +is +not.""" +''' + ) From 159714efc5c142d4fd2a8376c194972aaa7aacde Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Fri, 19 Aug 2022 11:25:01 -0400 Subject: [PATCH 14/14] test: update tests for _do_format_docstring now in class --- docformatter.py | 11 +- pyproject.toml | 1 + tests/conftest.py | 12 +- tests/test_format_docstring.py | 564 ++++++++++++++++++++++++++------- 4 files changed, 464 insertions(+), 124 deletions(-) diff --git a/docformatter.py b/docformatter.py index 2cf2daa..0fcfffc 100755 --- a/docformatter.py +++ b/docformatter.py @@ -49,7 +49,6 @@ from typing import List, TextIO, Tuple, Union # Third Party Imports -import _io import untokenize try: @@ -202,7 +201,7 @@ def do_parse_arguments(self) -> None: "result in a mess (default: %(default)s)", ) self.parser.add_argument( - "--tab_width", + "--tab-width", type=int, dest="tab_width", metavar="width", @@ -387,7 +386,7 @@ def __init__( """ self.args = args self.stderror: TextIO = stderror - self.stdin: TextIOr = stdin + self.stdin: TextIO = stdin self.stdout: TextIO = stdout def do_format_standard_in(self, parser: argparse.ArgumentParser): @@ -413,7 +412,7 @@ def do_format_standard_in(self, parser: argparse.ArgumentParser): encoding = self.stdin.encoding or _get_encoding() source = source.decode(encoding) - formatted_source = _format_code_with_args(source, args=self.args) + formatted_source = self._do_format_code(source) if encoding: formatted_source = formatted_source.encode(encoding) @@ -583,8 +582,8 @@ def _format_code( previous_token_type = token_type # If the current token is a newline, the previous token was a - # newline or a comment, and these two sequential newlines follow a - # function definition, ignore the blank line. + # newline or a comment, and these two sequential newlines + # follow a function definition, ignore the blank line. if ( len(modified_tokens) <= 2 or token_type not in {tokenize.NL, tokenize.NEWLINE} diff --git a/pyproject.toml b/pyproject.toml index caeda9b..ec068f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -182,6 +182,7 @@ deps = setenv = COVERAGE_FILE={toxinidir}/.coverage commands = + pip install -U pip pip install .[tomli] pytest -s -x -c ./pyproject.toml --cache-clear \ --cov-config=pyproject.toml --cov=docformatter \ diff --git a/tests/conftest.py b/tests/conftest.py index 318ddd3..c31d6e1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -108,10 +108,10 @@ def test_args(args): """Create a set of arguments to use with tests. To pass no arguments, just an empty file name: - @pytest.mark.parametrize("test_args", [[""]]) + @pytest.mark.parametrize("args", [[""]]) - To pass an argument and empty file name: - @pytest.mark.parametrize("test_args", [["wrap-summaries", "79", ""]]) + To pass an argument AND empty file name: + @pytest.mark.parametrize("args", [["--wrap-summaries", "79", ""]]) """ parser = argparse.ArgumentParser( description="parser object for docformatter tests", @@ -158,7 +158,7 @@ def test_args(args): default=False, ) parser.add_argument( - "--tab_width", + "--tab-width", type=int, dest="tab_width", metavar="width", @@ -224,6 +224,4 @@ def test_args(args): nargs="+", ) - _test_args = parser.parse_args(args) - - yield _test_args + yield parser.parse_args(args) diff --git a/tests/test_format_docstring.py b/tests/test_format_docstring.py index f5394e0..5bd49e4 100644 --- a/tests/test_format_docstring.py +++ b/tests/test_format_docstring.py @@ -30,25 +30,37 @@ # Standard Library Imports import itertools import random +import sys # Third Party Imports import pytest # docformatter Package Imports import docformatter +from docformatter import Formator # docformatter Local Imports from . import generate_random_docstring +INDENTATION = " " + class TestFormatDocstring: - """Class for testing format_docstring() with no arguments.""" + """Class for testing _do_format_docstring() with no arguments.""" @pytest.mark.unit - def test_format_docstring(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring(self, test_args, args): """Return one-line docstring.""" - assert '"""Hello."""' == docformatter.format_docstring( - " ", + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""Hello."""' == uut._do_format_docstring( + INDENTATION, ''' """ @@ -58,10 +70,20 @@ def test_format_docstring(self): ) @pytest.mark.unit - def test_format_docstring_with_summary_that_ends_in_quote(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_summary_that_ends_in_quote( + self, test_args, args + ): """Return one-line docstring with period after quote.""" - assert '''""""Hello"."""''' == docformatter.format_docstring( - " ", + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '''""""Hello"."""''' == uut._do_format_docstring( + INDENTATION, ''' """ @@ -71,15 +93,23 @@ def test_format_docstring_with_summary_that_ends_in_quote(self): ) @pytest.mark.unit - def test_format_docstring_with_bad_indentation(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "44", ""]]) + def test_format_docstring_with_bad_indentation(self, test_args, args): """Add spaces to indentation when too few.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Hello. - This should be indented but it is not. The - next line should be indented too. And - this too. - """''' == docformatter.format_docstring( - " ", + This should be indented but it is not. + The next line should be indented too. + And this too. + """''' == uut._do_format_docstring( + INDENTATION, ''' """Hello. @@ -91,8 +121,16 @@ def test_format_docstring_with_bad_indentation(self): ) @pytest.mark.unit - def test_format_docstring_with_too_much_indentation(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_too_much_indentation(self, test_args, args): """Remove spaces from indentation when too many.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Hello. This should be dedented. @@ -100,8 +138,8 @@ def test_format_docstring_with_too_much_indentation(self): 1. This too. 2. And this. 3. And this. - """''' == docformatter.format_docstring( - " ", + """''' == uut._do_format_docstring( + INDENTATION, ''' """Hello. @@ -116,14 +154,23 @@ def test_format_docstring_with_too_much_indentation(self): ) @pytest.mark.unit - def test_format_docstring_with_trailing_whitespace(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "52", ""]]) + def test_format_docstring_with_trailing_whitespace(self, test_args, args): """Remove trailing white space.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Hello. This should be not have trailing whitespace. The - next line should not have trailing whitespace either. - """''' == docformatter.format_docstring( - " ", + next line should not have trailing whitespace + either. + """''' == uut._do_format_docstring( + INDENTATION, ''' """Hello.\t \t @@ -135,15 +182,31 @@ def test_format_docstring_with_trailing_whitespace(self): ) @pytest.mark.unit - def test_format_docstring_with_empty_docstring(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_empty_docstring(self, test_args, args): """Do nothing with empty docstring.""" - assert '""""""' == docformatter.format_docstring(" ", '""""""') + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '""""""' == uut._do_format_docstring(INDENTATION, '""""""') @pytest.mark.unit - def test_format_docstring_with_no_period(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_no_period(self, test_args, args): """Add period to end of one-line and summary line.""" - assert '"""Hello."""' == docformatter.format_docstring( - " ", + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""Hello."""' == uut._do_format_docstring( + INDENTATION, ''' """ @@ -153,10 +216,18 @@ def test_format_docstring_with_no_period(self): ) @pytest.mark.unit - def test_format_docstring_with_single_quotes(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_single_quotes(self, test_args, args): """Replace single triple quotes with triple double quotes.""" - assert '"""Hello."""' == docformatter.format_docstring( - " ", + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert '"""Hello."""' == uut._do_format_docstring( + INDENTATION, """ ''' @@ -166,15 +237,25 @@ def test_format_docstring_with_single_quotes(self): ) @pytest.mark.unit - def test_format_docstring_with_single_quotes_multi_line(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_single_quotes_multi_line( + self, test_args, args + ): """Replace single triple quotes with triple double quotes.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ''' """Return x factorial. This uses math.factorial. """ -'''.strip() == docformatter.format_docstring( - " ", +'''.strip() == uut._do_format_docstring( + INDENTATION, """ ''' Return x factorial. @@ -185,7 +266,18 @@ def test_format_docstring_with_single_quotes_multi_line(self): ) @pytest.mark.unit - def test_format_docstring_leave_underlined_summaries_alone(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_leave_underlined_summaries_alone( + self, test_args, args + ): + """Leave underlined summary lines as is.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''""" Foo bar ------- @@ -193,51 +285,92 @@ def test_format_docstring_leave_underlined_summaries_alone(self): This is more. """''' - assert docstring == docformatter.format_docstring(" ", docstring) + assert docstring == uut._do_format_docstring(INDENTATION, docstring) class TestFormatLists: """Class for testing format_docstring() with lists in the docstring.""" @pytest.mark.unit - def test_format_docstring_should_ignore_numbered_lists(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_numbered_lists( + self, test_args, args + ): """Ignore lists beginning with numbers.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''"""Hello. 1. This should be indented but it is not. The next line should be indented too. But this is okay. """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_ignore_parameter_lists(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_parameter_lists( + self, test_args, args + ): """Ignore lists beginning with -.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''"""Hello. foo - This is a foo. This is a foo. This is a foo. This is a foo. This is. bar - This is a bar. This is a bar. This is a bar. This is a bar. This is. """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_ignore_colon_parameter_lists(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_colon_parameter_lists( + self, test_args, args + ): """Ignore lists beginning with :""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''"""Hello. foo: This is a foo. This is a foo. This is a foo. This is a foo. This is. bar: This is a bar. This is a bar. This is a bar. This is a bar. This is. """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_leave_list_alone(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_should_leave_list_alone(self, test_args, args): + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''""" one two @@ -251,13 +384,14 @@ def test_format_docstring_should_leave_list_alone(self): ten eleven """''' - assert docstring == docformatter.format_docstring( - " ", docstring, strict=False + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) class TestFormatWrap: - """Class for testing format_docstring() when requesting line wrapping.""" + """Class for testing _do_format_docstring() with line wrapping.""" @pytest.mark.unit def test_unwrap_summary(self): @@ -267,8 +401,20 @@ def test_unwrap_summary(self): ) @pytest.mark.unit - def test_format_docstring_with_wrap(self): - """Wrap docstring.""" + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_wrap( + self, + test_args, + args, + ): + """Wrap the docstring.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + # This function uses `random` so make sure each run of this test is # repeatable. random.seed(0) @@ -278,12 +424,12 @@ def test_format_docstring_with_wrap(self): range(min_line_length, 100), range(20) ): indentation = " " * num_indents - formatted_text = indentation + docformatter.format_docstring( + uut.args.wrap_summaries = max_length + formatted_text = indentation + uut._do_format_docstring( indentation=indentation, docstring=generate_random_docstring( max_word_length=min_line_length // 2 ), - summary_wrap_length=max_length, ) for line in formatted_text.split("\n"): # It is not the formatter's fault if a word is too long to @@ -292,18 +438,29 @@ def test_format_docstring_with_wrap(self): assert len(line) <= max_length @pytest.mark.unit - def test_format_docstring_with_weird_indentation_and_punctuation(self): - """""" + @pytest.mark.parametrize("args", [["--wrap-summaries", "79", ""]]) + def test_format_docstring_with_weird_indentation_and_punctuation( + self, + test_args, + args, + ): + """Wrap and dedent docstring.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ''' """Creates and returns four was awakens to was created tracked ammonites was the fifty, arithmetical four was pyrotechnic to pyrotechnic physicists. - `four' falsified x falsified ammonites - to awakens to. `created' to ancestor was four to x dynamo to was - four ancestor to physicists(). + `four' falsified x falsified ammonites to awakens to. `created' to + ancestor was four to x dynamo to was four ancestor to physicists(). """ - '''.strip() == docformatter.format_docstring( - " ", + '''.strip() == uut._do_format_docstring( + INDENTATION, ''' """Creates and returns four was awakens to was created tracked ammonites was the fifty, arithmetical four was pyrotechnic to @@ -312,18 +469,29 @@ def test_format_docstring_with_weird_indentation_and_punctuation(self): four ancestor to physicists(). """ '''.strip(), - summary_wrap_length=79, ) @pytest.mark.unit - def test_format_docstring_with_description_wrapping(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_with_description_wrapping( + self, + test_args, + args, + ): """Wrap description at 72 characters.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Hello. This should be indented but it is not. The next line should be indented too. But this is okay. - """''' == docformatter.format_docstring( - " ", + """''' == uut._do_format_docstring( + INDENTATION, ''' """Hello. @@ -333,12 +501,23 @@ def test_format_docstring_with_description_wrapping(self): """ '''.strip(), - description_wrap_length=72, ) @pytest.mark.unit - def test_format_docstring_should_ignore_multi_paragraph(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_multi_paragraph( + self, + test_args, + args, + ): """Ignore multiple paragraphs in elaborate description.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''"""Hello. This should be indented but it is not. The @@ -349,44 +528,83 @@ def test_format_docstring_should_ignore_multi_paragraph(self): next line should be indented too. But this is okay. """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_ignore_doctests(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_doctests( + self, + test_args, + args, + ): """Leave doctests alone.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''"""Hello. >>> 4 4 """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_ignore_doctests_in_summary(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_ignore_doctests_in_summary( + self, + test_args, + args, + ): """Leave doctests alone if they're in the summary.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + docstring = '''""" >>> 4 4 """''' - assert docstring == docformatter.format_docstring( - " ", docstring, description_wrap_length=72 + assert docstring == uut._do_format_docstring( + INDENTATION, + docstring, ) @pytest.mark.unit - def test_format_docstring_should_maintain_indentation_of_doctest(self): + @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + def test_format_docstring_should_maintain_indentation_of_doctest( + self, + test_args, + args, + ): """Don't change indentation of doctest lines.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Foo bar bing bang. >>> tests = DocTestFinder().find(_TestClass) >>> runner = DocTestRunner(verbose=False) >>> tests.sort(key = lambda test: test.name) - """''' == docformatter.format_docstring( - " ", + """''' == uut._do_format_docstring( + INDENTATION, docstring='''"""Foo bar bing bang. >>> tests = DocTestFinder().find(_TestClass) @@ -394,12 +612,35 @@ def test_format_docstring_should_maintain_indentation_of_doctest(self): >>> tests.sort(key = lambda test: test.name) """''', - description_wrap_length=72, ) @pytest.mark.unit - def test_force_wrap(self): + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-descriptions", + "72", + "--wrap-summaries", + "50", + "--force-wrap", + "", + ] + ], + ) + def test_force_wrap( + self, + test_args, + args, + ): """Force even lists to be wrapped.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( ( '''\ @@ -408,42 +649,80 @@ def test_force_wrap(self): convergence."""\ ''' ) - == docformatter.format_docstring( - " ", + == uut._do_format_docstring( + INDENTATION, '''\ """ num_iterations is the number of updates - instead of a better definition of convergence. """\ ''', - description_wrap_length=50, - summary_wrap_length=50, - force_wrap=True, ) ) @pytest.mark.unit + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-summaries", + "30", + "--tab-width", + "4", + "", + ] + ], + ) def test_format_docstring_with_summary_only_and_wrap_and_tab_indentation( self, + test_args, + args, ): """"Should account for length of tab when wrapping. See PR #69. """ + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ''' \t\t"""Some summary x x x \t\tx.""" -'''.strip() == docformatter.format_docstring( +'''.strip() == uut._do_format_docstring( "\t\t", ''' \t\t"""Some summary x x x x.""" '''.strip(), - summary_wrap_length=30, - tab_width=4, ) @pytest.mark.unit - def test_format_docstring_for_multi_line_summary_alone(self): + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-summaries", + "69", + "--close-quotes-on-newline", + "", + ] + ], + ) + def test_format_docstring_for_multi_line_summary_alone( + self, + test_args, + args, + ): """Place closing quotes on newline when wrapping one-liner.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( ( '''\ @@ -452,33 +731,51 @@ def test_format_docstring_for_multi_line_summary_alone(self): """\ ''' ) - == docformatter.format_docstring( - " ", + == uut._do_format_docstring( + INDENTATION, '''\ """This one-line docstring will be multi-line because it's quite long."""\ ''', - summary_wrap_length=69, - close_quotes_on_newline=True, ) ) @pytest.mark.unit - def test_format_docstring_for_one_line_summary_alone_but_too_long(self): + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-summaries", + "88", + "--close-quotes-on-newline", + "", + ] + ], + ) + def test_format_docstring_for_one_line_summary_alone_but_too_long( + self, + test_args, + args, + ): """""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( ( '''\ """This one-line docstring will not be wrapped and quotes will be in-line."""\ ''' ) - == docformatter.format_docstring( - " ", + == uut._do_format_docstring( + INDENTATION, '''\ """This one-line docstring will not be wrapped and quotes will be in-line."""\ ''', - summary_wrap_length=88, - close_quotes_on_newline=True, ) ) @@ -487,13 +784,25 @@ class TestFormatStyleOptions: """Class for testing format_docstring() when requesting style options.""" @pytest.mark.unit - def test_format_docstring_with_no_post_description_blank(self): + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_no_post_description_blank( + self, + test_args, + args, + ): """Remove blank lines before closing triple quotes.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''"""Hello. Description. - """''' == docformatter.format_docstring( - " ", + """''' == uut._do_format_docstring( + INDENTATION, ''' """ @@ -504,18 +813,29 @@ def test_format_docstring_with_no_post_description_blank(self): """ '''.strip(), - post_description_blank=False, ) @pytest.mark.unit - def test_format_docstring_with_pre_summary_newline(self): + @pytest.mark.parametrize("args", [["--pre-summary-newline", ""]]) + def test_format_docstring_with_pre_summary_newline( + self, + test_args, + args, + ): """Remove blank line before summary.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert '''""" Hello. Description. - """''' == docformatter.format_docstring( - " ", + """''' == uut._do_format_docstring( + INDENTATION, ''' """ @@ -526,11 +846,23 @@ def test_format_docstring_with_pre_summary_newline(self): """ '''.strip(), - pre_summary_newline=True, ) @pytest.mark.unit - def test_format_docstring_make_summary_multi_line(self): + @pytest.mark.parametrize("args", [["--make-summary-multi-line", ""]]) + def test_format_docstring_make_summary_multi_line( + self, + test_args, + args, + ): + """Place the one-line docstring between triple quotes.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( ( '''\ @@ -539,24 +871,34 @@ def test_format_docstring_make_summary_multi_line(self): """\ ''' ) - == docformatter.format_docstring( - " ", + == uut._do_format_docstring( + INDENTATION, '''\ """This one-line docstring will be multi-line"""\ ''', - make_summary_multi_line=True, ) ) @pytest.mark.unit - def test_format_docstring_pre_summary_space(self): - """""" + @pytest.mark.parametrize("args", [["--pre-summary-space", ""]]) + def test_format_docstring_pre_summary_space( + self, + test_args, + args, + ): + """Place a space between the opening quotes and the summary.""" + uut = Formator( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + assert ( '''""" This one-line docstring will have a leading space."""''' - ) == docformatter.format_docstring( - " ", + ) == uut._do_format_docstring( + INDENTATION, '''\ """This one-line docstring will have a leading space."""\ ''', - pre_summary_space=True, )