diff --git a/.github/workflows/do-release.yml b/.github/workflows/do-release.yml index 57fc668..6c1b69c 100644 --- a/.github/workflows/do-release.yml +++ b/.github/workflows/do-release.yml @@ -67,12 +67,14 @@ jobs: verbose: true - name: Check if diff + if: ${{ env.do_release == 1 }} continue-on-error: true run: > git diff --exit-code CHANGELOG.md && (echo "### No update" && exit 1) || (echo "### Commit update") - uses: EndBug/add-and-commit@v9 + if: ${{ env.do_release == 1 }} name: Commit and push if diff if: success() with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7fb6809 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,57 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-docstring-first + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: no-commit-to-branch + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: '23.3.0' + hooks: + - id: black + types_or: [python, pyi] + language_version: python3 + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + args: [--settings-file, ./pyproject.toml] + - repo: https://github.com/PyCQA/docformatter + rev: v1.6.5 + hooks: + - id: docformatter + additional_dependencies: [tomli] + args: [--in-place, --config, ./pyproject.toml] + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: 'v0.0.267' + hooks: + - id: ruff + args: [ --select, "PL", --select, "F" ] + - repo: https://github.com/pycqa/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + additional_dependencies: [toml] + args: [--config, ./pyproject.toml] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.3.0 + hooks: + - id: mypy + additional_dependencies: [types-python-dateutil] + args: [--config-file, ./pyproject.toml] + - repo: https://github.com/myint/eradicate + rev: '2.2.0' + hooks: + - id: eradicate + args: [] + - repo: https://github.com/rstcheck/rstcheck + rev: 'v6.1.2' + hooks: + - id: rstcheck + additional_dependencies: [tomli] + args: [-r, --config, ./pyproject.toml] diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 1b850d5..aa34783 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -51,3 +51,38 @@ And you invoke docformatter as follows: $ docformatter --config ~/.secret/path/to/pyproject.toml --wrap-summaries 68 Summaries will be wrapped at 68, not 82. + +A Note on Options to Control Styles +----------------------------------- +There are various ``docformatter`` options that can be used to control the +style of the docstring. These options can be passed on the command line or +set in a configuration file. Currently, the style options are: + + * ``--black`` + * ``-s`` or ``--style`` + +When passing the ``--black`` option, the following arguments are set +automatically: + + * ``--pre-summary-space`` is set to True + * ``--wrap-descriptions`` is set to 88 + * ``--wrap-summaries`` is set to 88 + +All of these options can be overridden from the command line. Further, the +``--pre-summary-space`` option only inserts a space before the summary when +the summary begins with a double quote ("). For example: + + ``"""This summary gets no space."""`` becomes ``"""This summary gets no space."""`` + +and + + ``""""This" summary does get a space."""`` becomes ``""" "This" summary does get a space."""`` + +The ``--style`` argument takes a string which is the name of the parameter +list style you are using. Currently, only ``sphinx`` is recognized, but +``epydoc``, ``numpy``, and ``google`` are future styles. For the selected +style, each line in the parameter lists will be wrapped at the +``--wrap-descriptions`` length as well as any portion of the elaborate +description preceding the parameter list. Parameter lists that don't follow the +passed style will cause the entire elaborate description to be ignored and +remain unwrapped. diff --git a/docs/source/requirements.rst b/docs/source/requirements.rst index 4c3a59d..0f763bb 100644 --- a/docs/source/requirements.rst +++ b/docs/source/requirements.rst @@ -51,17 +51,31 @@ Thus, an autoformatting tool: * 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 ----------------- +Docstring Style +--------------- -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 +There are at least four "flavors" of docstrings in common use today; +Epydoc, 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 parameter description of the multi-line docstring. For example, here is how each syntax documents function arguments. +Epydoc syntax: + +.. code-block:: + + @type num_dogs: int + @param num_dogs: the number of dogs + +Sphinx syntax: + +.. code-block:: + + :param param1: The first parameter, defaults to 1. + :type: int + Google syntax: .. code-block:: @@ -78,13 +92,6 @@ NumPy syntax: 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 @@ -201,8 +208,14 @@ the requirement falls in, the type of requirement, and whether ' docformatter_10.1.3.1', ' Shall maintain in-line links on one line even if the resulting line exceeds wrap length.', ' Derived', ' Shall', ' Yes [*PR #152*]' ' docformatter_10.1.3.2', ' Shall not place a newline between description text and a wrapped link.', ' Derived', ' Shall', ' Yes [PR #182]' ' docformatter_10.2', ' Should format docstrings using NumPy style.', ' Style', ' Should', ' No' + ' docformatter_10.2.1', ' Shall ignore docstrings in other styles when using NumPy style.', ' Shall', ' Yes' + ' docformatter_10.2.2', ' Shall wrap NumPy-style parameter descriptions that exceed wrap length when using NumPy style.', ' Shall', ' 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_10.3.1', ' Shall ignore docstrings in other styles when using Google style.', ' Shall', ' Yes' + ' docformatter_10.3.2', ' Shall wrap Google-style parameter descriptions that exceed wrap length when using Google style.', ' Shall', ' No' + ' docformatter_10.4', ' Should format docstrings using Sphinx style.', ' Style', ' Should', ' Yes' + ' docformatter_10.4.1', ' Shall ignore docstrings in other styles when using Sphinx style.', ' Shall', ' Yes' + ' docformatter_10.4.2', ' Shall wrap Sphinx-style parameter descriptions that exceed wrap length when using Sphinx style.', ' Shall', ' Yes' ' docformatter_10.5', ' Should format docstrings compatible with black.', ' Style', ' Should', ' Yes [PR #192]' ' docformatter_10.5.1', ' Should wrap summaries at 88 characters by default in black mode.', ' Style', ' Should', ' Yes' ' docformatter_10.5.2', ' Should wrap descriptions at 88 characters by default in black mode.', ' Style', ' Should', ' Yes' @@ -244,6 +257,9 @@ with *convention* requirements. ``docformatter`` currently provides these arguments for *style* requirements. :: + -s, --style [string, default sphinx] + name of the docstring syntax style to use for formatting parameter + lists. --black [boolean, default False] Boolean to indicate whether to format docstrings to be compatible with black. @@ -306,12 +322,6 @@ The following are new arguments that are needed to implement **should** or Boolean to indicate whether to wrap one-line docstrings. Provides option for requirement PEP_257_4.1. -The following are new *style* arguments needed to accommodate the various style options: -:: - - --syntax [string, default "sphinx"] - One of sphinx, numpy, or google - Issue and Version Management ---------------------------- diff --git a/docs/source/usage.rst b/docs/source/usage.rst index d17cab0..620a2fd 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -43,6 +43,12 @@ help output provides a summary of these options: -n, --non-cap list of words not to capitalize when they appear as the first word in the summary + -s style, --style style + the docstring style to use when formatting parameter + lists (default: sphinx) + --black + make formatting compatible with standard black options + (default: False) --wrap-summaries length wrap long summary lines at this length; set to 0 to disable wrapping @@ -51,11 +57,12 @@ help output provides a summary of these options: wrap descriptions at this length; set to 0 to disable wrapping (default: 72) - --black - make formatting consistent with black, setting - wrap-summaries and wrap-descriptions to a default 88 - if not otherwise specified - (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) --blank add blank line after elaborate description (default: False) @@ -75,12 +82,6 @@ help output provides a summary of these options: 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 @@ -88,8 +89,7 @@ help output provides a summary of these options: apply docformatter to docstrings of given length range --non-strict do not strictly follow reST syntax to identify lists - (see issue #67) - (default: False) + (see issue #67) (default: False) --config CONFIG path to file containing docformatter options (default: ./pyproject.toml) diff --git a/src/docformatter/__main__.py b/src/docformatter/__main__.py index 6754caa..6c847c3 100755 --- a/src/docformatter/__main__.py +++ b/src/docformatter/__main__.py @@ -71,6 +71,9 @@ def _help(): list of words not to capitalize when they appear as the first word in the summary + -s style, --style style + the docstring style to use when formatting parameter + lists (default: sphinx) --black make formatting compatible with standard black options (default: False) --wrap-summaries length diff --git a/src/docformatter/configuration.py b/src/docformatter/configuration.py index e89cc2d..ea26e29 100644 --- a/src/docformatter/configuration.py +++ b/src/docformatter/configuration.py @@ -158,6 +158,13 @@ def do_parse_arguments(self) -> None: _default_wrap_descriptions = 72 _default_pre_summary_space = "false" + self.parser.add_argument( + "-s", + "--style", + default=self.flargs_dct.get("style", "sphinx"), + help="name of the docstring style to use when formatting " + "parameter lists (default: sphinx)", + ) self.parser.add_argument( "--wrap-summaries", default=int( diff --git a/src/docformatter/format.py b/src/docformatter/format.py index 651d0fb..8881298 100644 --- a/src/docformatter/format.py +++ b/src/docformatter/format.py @@ -489,6 +489,7 @@ def _do_format_docstring( _syntax.is_some_sort_of_list( summary, self.args.non_strict, + self.args.style, ) or _syntax.do_find_directives(summary) ): @@ -613,6 +614,7 @@ def _do_format_multiline_docstring( wrap_length=self.args.wrap_descriptions, force_wrap=self.args.force_wrap, strict=self.args.non_strict, + style=self.args.style, ) post_description = "\n" if self.args.post_description_blank else "" return f'''\ diff --git a/src/docformatter/syntax.py b/src/docformatter/syntax.py index 290091e..dc28209 100644 --- a/src/docformatter/syntax.py +++ b/src/docformatter/syntax.py @@ -30,8 +30,11 @@ import textwrap from typing import Iterable, List, Tuple, Union +SPHINX_REGEX = r":[a-zA-Z0-9_\- ]*:" +"""Regular expression to use for finding Sphinx-style field lists.""" + REST_REGEX = r"(\.{2}|``) ?[\w-]+(:{1,2}|``)?" -"""The regular expression to use for finding reST directives.""" +"""Regular expression to use for finding reST directives.""" URL_PATTERNS = ( "afp|" @@ -81,8 +84,7 @@ ) """The URL patterns to look for when finding links. -Based on the table at - +Based on the table at """ # This is the regex used to find URL links: @@ -151,8 +153,8 @@ def description_to_list( Returns ------- _lines : list - A list containing each line of the description with any links put - back together. + A list containing each line of the description with any links put + back together. """ # This is a description containing only one paragraph. if len(re.findall(r"\n\n", text)) <= 0: @@ -176,7 +178,7 @@ def description_to_list( _lines.extend(_text) _lines.append("") with contextlib.suppress(IndexError): - if _lines[-2] == _lines[-1] == "": + if not _lines[-1] and not _lines[-2]: _lines.pop(-1) return _lines @@ -187,11 +189,9 @@ def do_clean_url(url: str, indentation: str) -> str: This function deals with situations such as: - `Get\n - Cookies.txt str: The URL that was found by the do_find_links() function and needs to be processed. indentation : str - The indentation pattern used. - + The indentation pattern used. Returns ------- url : str - The URL with internal newlines removed and excess whitespace removed. + The URL with internal newlines removed and excess whitespace removed. """ _lines = url.splitlines() for _idx, _line in enumerate(_lines): - if indentation != "" and _line[: len(indentation)] == indentation: + if indentation and _line[: len(indentation)] == indentation: _lines[_idx] = f" {_line.strip()}" return f'{indentation}{"".join(list(_lines))}' @@ -236,7 +235,25 @@ def do_find_directives(text: str) -> bool: Whether the docstring is a reST directive. """ _rest_iter = re.finditer(REST_REGEX, text) - return bool([(rest.start(0), rest.end(0)) for rest in _rest_iter]) + return bool([(_rest.start(0), _rest.end(0)) for _rest in _rest_iter]) + + +def do_find_sphinx_field_lists(text: str) -> List[Tuple[int, int]]: + r"""Determine if docstring contains any field lists. + + Parameters + ---------- + text : str + The docstring description to check for field list patterns. + + Returns + ------- + field_index : list + A list of tuples with each tuple containing the starting and ending + position of each field list found in the passed description. + """ + _field_iter = re.finditer(SPHINX_REGEX, text) + return [(_field.start(0), _field.end(0)) for _field in _field_iter] def do_find_links(text: str) -> List[Tuple[int, int]]: @@ -245,12 +262,12 @@ def do_find_links(text: str) -> List[Tuple[int, int]]: Parameters ---------- text : str - the docstring description to check for a link patterns. + The docstring description to check for link patterns. Returns ------- url_index : list - a list of tuples with each tuple containing the starting and ending + A list of tuples with each tuple containing the starting and ending position of each URL found in the passed description. """ _url_iter = re.finditer(URL_REGEX, text) @@ -291,6 +308,7 @@ def do_split_description( text: str, indentation: str, wrap_length: int, + style: str, ) -> Union[List[str], Iterable]: """Split the description into a list of lines. @@ -303,6 +321,8 @@ def do_split_description( line. wrap_length : int The column to wrap each line at. + style : str + The docstring style to use for dealing with parameter lists. Returns ------- @@ -310,80 +330,214 @@ def do_split_description( A list containing each line of the description with any links put back together. """ + _lines: List[str] = [] + _text_idx = 0 + # Check if the description contains any URLs. _url_idx = do_find_links(text) - if not _url_idx: + if style == "sphinx": + _parameter_idx = do_find_sphinx_field_lists(text) + _wrap_parameters = True + else: + _parameter_idx = [] + _wrap_parameters = False + + if not _url_idx and not (_parameter_idx and _wrap_parameters): return description_to_list( text, indentation, wrap_length, ) + + if _url_idx: + _lines, _text_idx = do_wrap_urls( + text, + _url_idx, + 0, + indentation, + wrap_length, + ) + + if _parameter_idx: + _lines, _text_idx = do_wrap_parameter_lists( + text, + _parameter_idx, + _lines, + _text_idx, + indentation, + wrap_length, + ) + else: + # Finally, add everything after the last field list directive. + with contextlib.suppress(IndexError): + _text = ( + text[_text_idx + 1 :] + if text[_text_idx] == "\n" + else text[_text_idx:] + ).splitlines() + for _idx, _line in enumerate(_text): + if _line not in ["", "\n", f"{indentation}"]: + _text[_idx] = f"{indentation}{_line.strip()}" + + _lines += _text + + return _lines + + +def do_wrap_parameter_lists( # noqa: PLR0913 + text: str, + parameter_idx: List[Tuple[int, int]], + lines: List[str], + text_idx: int, + indentation: str, + wrap_length: int, +) -> Tuple[List[str], int]: + """Wrap parameter lists in the long description. + + Parameters + ---------- + text : str + The long description text. + parameter_idx : list + The list of parameter list indices found in the description text. + lines : list + The list of formatted lines in the description that come before the + first parameter list item. + text_idx : int + The index in the description of the end of the last parameter list + item. + indentation : str + The string to use to indent each line in the long description. + wrap_length : int + The line length at which to wrap long lines in the description. + + Returns + ------- + lines, text_idx : tuple + A list of the long description lines and the index in the long + description where the last parameter list item ended. + """ + lines.extend( + description_to_list( + text[text_idx : parameter_idx[0][0]], + indentation, + wrap_length, + ) + ) + + for _idx, _parameter in enumerate(parameter_idx): + try: + _parameter_description = text[ + _parameter[1] : parameter_idx[_idx + 1][0] + ].strip() + except IndexError: + _parameter_description = text[_parameter[1] :].strip() + + if len(_parameter_description) <= (wrap_length - len(indentation)): + lines.append( + f"{indentation}{text[_parameter[0]: _parameter[1]]} " + f"{_parameter_description}" + ) + else: + lines.extend( + textwrap.wrap( + textwrap.dedent( + f"{text[_parameter[0]:_parameter[1]]} {_parameter_description}" + ), + width=wrap_length, + initial_indent=indentation, + subsequent_indent=2 * indentation, + ) + ) + + text_idx = _parameter[1] + + return lines, text_idx + + +def do_wrap_urls( + text: str, + url_idx: Iterable, + text_idx: int, + indentation: str, + wrap_length: int, +) -> Tuple[List[str], int]: + """Wrap URLs in the long description. + + Parameters + ---------- + text : str + The long description text. + url_idx : list + The list of URL indices found in the description text. + text_idx : int + The index in the description of the end of the last URL. + indentation : str + The string to use to indent each line in the long description. + wrap_length : int + The line length at which to wrap long lines in the description. + + Returns + ------- + _lines, _text_idx : tuple + A list of the long description lines and the index in the long + description where the last URL ended. + """ _lines = [] - _text_idx = 0 - for _idx in _url_idx: + for _url in url_idx: # Skip URL if it is simply a quoted pattern. - if do_skip_link(text, _idx): + if do_skip_link(text, _url): continue # If the text including the URL is longer than the wrap length, # we need to split the description before the URL, wrap the pre-URL # text, and add the URL as a separate line. - if len(text[_text_idx : _idx[1]]) > (wrap_length - len(indentation)): + if len(text[text_idx : _url[1]]) > (wrap_length - len(indentation)): # Wrap everything in the description before the first URL. _lines.extend( description_to_list( - text[_text_idx : _idx[0]], + text[text_idx : _url[0]], indentation, wrap_length, ) ) with contextlib.suppress(IndexError): - if _lines[-1] == "": + if not _lines[-1]: _lines.pop(-1) # Add the URL. _lines.append( - f"{do_clean_url(text[_idx[0] : _idx[1]], indentation)}" + f"{do_clean_url(text[_url[0] : _url[1]], indentation)}" ) - _text_idx = _idx[1] - - # Finally, add everything after the last URL. - with contextlib.suppress(IndexError): - _text = ( - text[_text_idx + 1 :] - if text[_text_idx] == "\n" - else text[_text_idx:] - ) - _text = _text.splitlines() - for _idx, _line in enumerate(_text): - if _line not in ["", "\n", f"{indentation}"]: - _text[_idx] = f"{indentation}{_line.strip()}" - - _lines += _text + text_idx = _url[1] - return _lines + return _lines, text_idx # pylint: disable=line-too-long -def is_some_sort_of_list(text: str, strict: bool) -> bool: +def is_some_sort_of_list( + text: str, + strict: bool, + style: str, +) -> bool: """Determine if docstring is a reST list. Notes ----- There are five types of lists in reST/docutils that need to be handled. - * `Bullets lists + * `Bullet lists `_ * `Enumerated lists - `_ + `_ * `Definition lists - `_ + `_ * `Field lists - `_ + `_ * `Option lists - `_ + `_ """ split_lines = text.rstrip().splitlines() @@ -397,37 +551,87 @@ def is_some_sort_of_list(text: str, strict: bool) -> bool: ) and not strict: return True - return any( - ( - # "1. item" - re.match(r"\s*\d\.", line) - or - # "@parameter" - re.match(r"\s*[\-*:=@]", line) - or - # "parameter - description" - re.match(r".*\s+[\-*:=@]\s+", line) - or - # "parameter: description" - re.match(r"\s*\S+[\-*:=@]\s+", line) - or - # "parameter:\n description" - re.match(r"\s*\S+:\s*$", line) - or - # "parameter -- description" - re.match(r"\s*\S+\s+--\s+", line) - or - # "parameter::" <-- Literal block - re.match(r"\s*[\S ]*:{2}", line) + if style == "sphinx": + return any( + ( + # "* parameter" <-- Bullet list + # "- parameter" <-- Bullet list + # "+ parameter" <-- Bullet list + re.match(r"\s*[*\-+] [\S ]+", line) + or + # "1. item" <-- Enumerated list + re.match(r"\s*\d\.", line) + or + # "-a description" <-- Option list + # "--long description" <-- Option list + re.match(r"^-{1,2}[\S ]+ {2}\S+", line) + or + # "@parameter" <-- Epydoc style + re.match(r"\s*@\S*", line) + or + # "parameter : description" <-- Numpy style + # "parameter: description" <-- Numpy style + re.match(r"^\s*(?!:)\S+ ?: \S+", line) + or + # "word\n----" <-- Numpy headings + re.match(r"^\s*-+", line) + or + # "parameter - description" + re.match(r"[\S ]+ - \S+", line) + or + # "parameter -- description" + re.match(r"\s*\S+\s+--\s+", line) + or + # Literal block + re.match(r"[\S ]*::", line) + ) + for line in split_lines + ) + else: + return any( + ( + # "* parameter" <-- Bullet list + # "- parameter" <-- Bullet list + # "+ parameter" <-- Bullet list + re.match(r"\s*[*\-+] [\S ]+", line) + or + # "1. item" <-- Enumerated list + re.match(r"\s*\d\.", line) + or + # "-a description" <-- Option list + # "--long description" <-- Option list + re.match(r"^-{1,2}[\S ]+ {2}\S+", line) + or + # "@parameter" <-- Epydoc style + re.match(r"\s*@\S*", line) + or + # ":parameter: description" <-- Sphinx style + re.match(SPHINX_REGEX, line) + or + # "parameter : description" <-- Numpy style + # "parameter: description" <-- Numpy style + re.match(r"^\s[\S ]+ ?: [\S ]+", line) + or + # "word\n----" <-- Numpy headings + re.match(r"^\s*-+", line) + or + # "parameter - description" + re.match(r"[\S ]+ - \S+", line) + or + # "parameter -- description" + re.match(r"\s*\S+\s+--\s+", line) + or + # Literal block + re.match(r"[\S ]*::", line) + ) + for line in split_lines ) - for line in split_lines - ) def is_some_sort_of_code(text: str) -> bool: """Return True if text looks like code.""" return any( - len(word) > 50 and not re.match(URL_REGEX, word) + len(word) > 50 and not re.match(URL_REGEX, word) # noqa: PLR2004 for word in text.split() ) @@ -501,11 +705,40 @@ def wrap_summary(summary, initial_indent, subsequent_indent, wrap_length): return summary -def wrap_description(text, indentation, wrap_length, force_wrap, strict): +def wrap_description( # noqa: PLR0913 + text, + indentation, + wrap_length, + force_wrap, + strict, + style: str = "sphinx", +): """Return line-wrapped description text. We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. + + Parameters + ---------- + text : str + The unwrapped description text. + indentation : str + The indentation string. + wrap_length : int + The line length at which to wrap long lines. + force_wrap : bool + Whether to force docformatter to wrap long lines when normally they + would remain untouched. + strict : bool + Whether to strictly follow reST syntax to identify lists. + style : str + The name of the docstring style to use when dealing with parameter + lists (default is sphinx). + + Returns + ------- + description : str + The description wrapped at wrap_length characters. """ text = strip_leading_blank_lines(text) @@ -521,11 +754,11 @@ def wrap_description(text, indentation, wrap_length, force_wrap, strict): and ( is_some_sort_of_code(text) or do_find_directives(text) - or is_some_sort_of_list(text, strict) + or is_some_sort_of_list(text, strict, style) ) ): return text - lines = do_split_description(text, indentation, wrap_length) + lines = do_split_description(text, indentation, wrap_length, style) return indentation + "\n".join(lines).strip() diff --git a/tests/conftest.py b/tests/conftest.py index ef8c77f..204b05d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -64,8 +64,12 @@ def temporary_file(contents, file_directory=".", file_prefix=""): finally: os.remove(f.name) + @pytest.fixture(scope="function") -def temporary_pyproject_toml(config, config_file_directory="/tmp",): +def temporary_pyproject_toml( + config, + config_file_directory="/tmp", +): """Write contents to temporary configuration and yield it.""" f = open(f"{config_file_directory}/pyproject.toml", "wb") try: @@ -75,8 +79,12 @@ def temporary_pyproject_toml(config, config_file_directory="/tmp",): finally: os.remove(f.name) + @pytest.fixture(scope="function") -def temporary_setup_cfg(config, config_file_directory="/tmp",): +def temporary_setup_cfg( + config, + config_file_directory="/tmp", +): """Write contents to temporary configuration and yield it.""" f = open(f"{config_file_directory}/setup.cfg", "wb") try: @@ -86,6 +94,7 @@ def temporary_setup_cfg(config, config_file_directory="/tmp",): finally: os.remove(f.name) + @pytest.fixture(scope="function") def run_docformatter(arguments, temporary_file): """Run subprocess with same Python path as parent. @@ -93,7 +102,7 @@ def run_docformatter(arguments, temporary_file): Return subprocess object. """ if "DOCFORMATTER_COVERAGE" in os.environ and int( - os.environ["DOCFORMATTER_COVERAGE"] + os.environ["DOCFORMATTER_COVERAGE"] ): DOCFORMATTER_COMMAND = [ "coverage", @@ -163,6 +172,11 @@ def test_args(args): "--non-cap", nargs="*", ) + parser.add_argument( + "-s", + "--style", + default="sphinx", + ) parser.add_argument( "--wrap-summaries", default=79, diff --git a/tests/test_format_docstring.py b/tests/test_format_docstring.py index 6bea255..bf74827 100644 --- a/tests/test_format_docstring.py +++ b/tests/test_format_docstring.py @@ -216,7 +216,7 @@ def test_format_docstring_with_no_period(self, test_args, args): ) @pytest.mark.unit - @pytest.mark.parametrize("args", [["--non-cap", "eBay", 'iPad', "-c", ""]]) + @pytest.mark.parametrize("args", [["--non-cap", "eBay", "iPad", "-c", ""]]) def test_format_docstring_with_non_cap_words(self, test_args, args): """Capitalize words not found in the non_cap list. @@ -235,7 +235,8 @@ def test_format_docstring_with_non_cap_words(self, test_args, args): """ eBay kinda suss """ -''') +''', + ) @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) @@ -486,7 +487,9 @@ def test_format_docstring_should_ignore_parameter_lists( ) @pytest.mark.unit - @pytest.mark.parametrize("args", [["--wrap-descriptions", "72", ""]]) + @pytest.mark.parametrize( + "args", [["--wrap-descriptions", "72", "--style", "numpy", ""]] + ) def test_format_docstring_should_ignore_colon_parameter_lists( self, test_args, args ): @@ -1156,7 +1159,13 @@ def test_format_docstring_with_target_links( @pytest.mark.unit @pytest.mark.parametrize( "args", - [["--wrap-descriptions", "72", ""]], + [ + [ + "--wrap-descriptions", + "72", + "", + ] + ], ) def test_format_docstring_with_simple_link( self, @@ -1195,7 +1204,13 @@ def test_format_docstring_with_simple_link( @pytest.mark.unit @pytest.mark.parametrize( "args", - [["--wrap-descriptions", "88", ""]], + [ + [ + "--wrap-descriptions", + "88", + "", + ] + ], ) def test_format_docstring_keep_inline_link_together( self, @@ -1234,7 +1249,13 @@ def test_format_docstring_keep_inline_link_together( @pytest.mark.unit @pytest.mark.parametrize( "args", - [["--wrap-descriptions", "88", ""]], + [ + [ + "--wrap-descriptions", + "88", + "", + ] + ], ) def test_format_docstring_keep_inline_link_together_two_paragraphs( self, @@ -1243,8 +1264,8 @@ def test_format_docstring_keep_inline_link_together_two_paragraphs( ): """Keep in-line links together with the display text. - If there is another paragraph following the in-line link, don't - strip the newline in between. + If there is another paragraph following the in-line link, don't strip the + newline in between. See issue #157. """ @@ -1286,7 +1307,13 @@ def test_format_docstring_keep_inline_link_together_two_paragraphs( @pytest.mark.unit @pytest.mark.parametrize( "args", - [["--wrap-descriptions", "72", ""]], + [ + [ + "--wrap-descriptions", + "72", + "", + ] + ], ) def test_format_docstring_with_short_link( self, @@ -1323,7 +1350,13 @@ def test_format_docstring_with_short_link( @pytest.mark.unit @pytest.mark.parametrize( "args", - [["--wrap-descriptions", "72", ""]], + [ + [ + "--wrap-descriptions", + "72", + "", + ] + ], ) def test_format_docstring_with_only_link_in_description( self, @@ -1360,7 +1393,15 @@ def test_format_docstring_with_only_link_in_description( @pytest.mark.unit @pytest.mark.parametrize( "args", - [["--wrap-descriptions", "88", "--wrap-summaries", "88", ""]], + [ + [ + "--wrap-descriptions", + "88", + "--wrap-summaries", + "88", + "", + ] + ], ) def test_format_docstring_link_only_one_newline_after_link( self, @@ -1461,7 +1502,15 @@ def function2(): assert docstring == uut._do_format_code(docstring) @pytest.mark.unit - @pytest.mark.parametrize("args", [["--black", ""]]) + @pytest.mark.parametrize( + "args", + [ + [ + "--black", + "", + ] + ], + ) def test_format_docstring_black( self, test_args, @@ -1469,8 +1518,8 @@ def test_format_docstring_black( ): """Format with black options when --black specified. - Add a space between the opening quotes and the summary if - content starts with a quote. + Add a space between the opening quotes and the summary if content starts with a + quote. """ uut = Formatter( test_args, @@ -1513,6 +1562,141 @@ def test_format_docstring_black( This long description will be wrapped at 88 characters because we passed the --black option and 88 characters is the default wrap length. """\ +''', + ) + ) + + @pytest.mark.unit + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-descriptions", + "88", + "--wrap-summaries", + "88", + "--style", + "sphinx", + "", + ] + ], + ) + def test_format_docstring_sphinx_style( + self, + test_args, + args, + ): + """Wrap sphinx style parameter lists. + + See requirement docformatter_10.4.2 + """ + uut = Formatter( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert ( + ( + '''\ +"""Return line-wrapped description text. + + We only wrap simple descriptions. We leave doctests, multi-paragraph text, and + bulleted lists alone. See + http://www.docformatter.com/. + + :param str text: the text argument. + :param str indentation: the super long description for the indentation argument that + will require docformatter to wrap this line. + :param int wrap_length: the wrap_length argument + :param bool force_wrap: the force_warp argument. + :return: really long description text wrapped at n characters and a very long + description of the return value so we can wrap this line abcd efgh ijkl mnop + qrst uvwx yz. + :rtype: str + """\ +''' + ) + == uut._do_format_docstring( + INDENTATION, + '''\ +"""Return line-wrapped description text. + + We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. + + :param str text: the text argument. + :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. + :param int wrap_length: the wrap_length argument + :param bool force_wrap: the force_warp argument. + :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. + :rtype: str +"""\ +''', + ) + ) + + @pytest.mark.unit + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-descriptions", + "88", + "--wrap-summaries", + "88", + "--style", + "numpy", + "", + ] + ], + ) + def test_format_docstring_non_sphinx_style( + self, + test_args, + args, + ): + """Ignore wrapping sphinx style parameter lists when not using sphinx style. + + See requirement docformatter_10.4.1 + """ + uut = Formatter( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert ( + ( + '''\ +"""Return line-wrapped description text. + + We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. + + :param str text: the text argument. + :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. + :param int wrap_length: the wrap_length argument + :param bool force_wrap: the force_warp argument. + :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. + :rtype: str + """\ +''' + ) + == uut._do_format_docstring( + INDENTATION, + '''\ +"""Return line-wrapped description text. + + We only wrap simple descriptions. We leave doctests, multi-paragraph text, and bulleted lists alone. See http://www.docformatter.com/. + + :param str text: the text argument. + :param str indentation: the super long description for the indentation argument that will require docformatter to wrap this line. + :param int wrap_length: the wrap_length argument + :param bool force_wrap: the force_warp argument. + :return: really long description text wrapped at n characters and a very long description of the return value so we can wrap this line abcd efgh ijkl mnop qrst uvwx yz. + :rtype: str +"""\ ''', ) ) @@ -1789,8 +1973,8 @@ def test_strip_docstring_with_single_quotes( ): """Raise ValueError when strings begin with single single quotes. - See requirement PEP_257_1. See issue #66 for example of - docformatter breaking code when encountering single quote. + See requirement PEP_257_1. See issue #66 for example of docformatter breaking + code when encountering single quote. """ uut = Formatter( test_args, @@ -1811,8 +1995,8 @@ def test_strip_docstring_with_double_quotes( ): """Raise ValueError when strings begin with single double quotes. - See requirement PEP_257_1. See issue #66 for example of - docformatter breaking code when encountering single quote. + See requirement PEP_257_1. See issue #66 for example of docformatter breaking + code when encountering single quote. """ uut = Formatter( test_args, diff --git a/tests/test_utility_functions.py b/tests/test_utility_functions.py index 0e58fd8..b21d56b 100644 --- a/tests/test_utility_functions.py +++ b/tests/test_utility_functions.py @@ -421,6 +421,7 @@ def test_is_some_sort_of_list(self): @param """, True, + "numpy", ) @pytest.mark.unit @@ -433,6 +434,7 @@ def test_is_some_sort_of_list_with_dashes(self): imag -- the imaginary part (default 0.0) """, True, + "numpy", ) @pytest.mark.unit @@ -448,7 +450,8 @@ def test_is_some_sort_of_list_without_special_symbol(self): release-1.4.1/ release-1.5/ """, - True, + False, + "numpy", ) @pytest.mark.unit @@ -460,6 +463,7 @@ def test_is_some_sort_of_list_of_parameter_list_with_newline(self): stream (BinaryIO): Binary stream (usually a file object). """, True, + "numpy", ) @pytest.mark.unit @@ -475,6 +479,7 @@ def test_is_some_sort_of_list_strict_wrap(self): rocket. """, True, + "numpy", ) @pytest.mark.unit @@ -490,6 +495,62 @@ def test_is_some_sort_of_list_non_strict_wrap(self): rocket. """, False, + "numpy", + ) + + @pytest.mark.unit + def test_is_some_sort_of_list_sphinx_style(self): + """Identify non-sphinx styles as lists when using sphinx style. + + See requirement docformatter_10.4.1 + """ + assert docformatter.is_some_sort_of_list( + """\ +Using Numpy parameter list + +Parameters +---------- + arg1 : str + The first argument. + arg2 : int + The second argument. +""", + False, + "sphinx", + ) + + @pytest.mark.unit + def test_not_is_some_sort_of_list_sphinx_style(self): + """Ignore sphinx style parameter lists when using sphinx style. + + See requirement docformatter_10.4 + """ + assert not docformatter.is_some_sort_of_list( + """\ +Using Sphinx parameter list + +:param str arg1: the first argument. +:param int arg2: the second argument. +""", + False, + "sphinx", + ) + + @pytest.mark.unit + def test_is_some_sort_of_list_not_sphinx_style(self): + """Identify sphinx styles as lists when not using sphinx style. + + See requirements docformatter_10.2.1 and docformatter_10.3.1 + """ + assert docformatter.is_some_sort_of_list( + """\ +Using Sphinx parameter list + +:param str arg1: the first argument. +:param int arg2: the second argument. +""", + False, + "numpy", ) @pytest.mark.unit @@ -512,6 +573,7 @@ def test_is_some_sort_of_list_literal_block(self): pass """, False, + "numpy", )