diff --git a/.github/workflows/do-prioritize-issues.yml b/.github/workflows/do-prioritize-issues.yml index 7c88394..c1a80c5 100644 --- a/.github/workflows/do-prioritize-issues.yml +++ b/.github/workflows/do-prioritize-issues.yml @@ -24,19 +24,22 @@ jobs: uses: weibullguy/get-labels-action@main - name: Add High Urgency Labels - if: (endsWith(steps.getlabels.outputs.labels, 'convention') && endsWith(steps.getlabels.outputs.labels, 'bug')) + if: | + ${{ (contains(steps.getlabels.outputs.labels, "C: convention") && contains (steps.getlabels.outputs.labels, "P: bug")) }} uses: andymckay/labeler@master with: add-labels: "U: high" - name: Add Medium Urgency Labels - if: (endsWith(steps.getlabels.outputs.labels, 'style') && endsWith(steps.getlabels.outputs.labels, 'bug')) || (endsWith(steps.getlabels.outputs.labels, 'stakeholder') && endsWith(steps.getlabels.outputs.labels, 'bug')) || (endsWith(steps.getlabels.outputs.labels, 'convention') && endsWith(steps.getlabels.outputs.labels, 'enhancement')) + if: | + ${{ (contains(steps.getlabels.outputs.labels, "C: style") && contains(steps.getlabels.outputs.labels, "P: bug")) || (contains(steps.getlabels.outputs.labels, "C: stakeholder") && contains(steps.getlabels.outputs.labels, "P: bug")) || (contains(steps.getlabels.outputs.labels, "C: convention") && contains(steps.getlabels.outputs.labels, "P: enhancement")) }} uses: andymckay/labeler@master with: add-labels: "U: medium" - name: Add Low Urgency Labels - if: (endsWith(steps.getlabels.outputs.labels, 'style') && endsWith(steps.getlabels.outputs.labels, 'enhancement')) || (endsWith(steps.getlabels.outputs.labels, 'stakeholder') && endsWith(steps.getlabels.outputs.labels, 'enhancement')) || contains(steps.getlabels.outputs.labels, 'doc') || contains(steps.getlabels.outputs.labels, 'chore') + if: | + ${{ (contains(steps.getlabels.outputs.labels, "C: style") && contains(steps.getlabels.outputs.labels, "P: enhancement")) || (contains(steps.getlabels.outputs.labels, "C: stakeholder") && contains(steps.getlabels.outputs.labels, "P: enhancement")) || contains(steps.getlabels.outputs.labels, "doc") || contains(steps.getlabels.outputs.labels, "chore") }} uses: andymckay/labeler@master with: add-labels: "U: low" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba3dbf4..e3c6ab0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,13 +21,13 @@ repos: - id: isort args: [--settings-file, ./pyproject.toml] - repo: https://github.com/PyCQA/docformatter - rev: v1.7.0 + rev: v1.7.1 hooks: - id: docformatter additional_dependencies: [tomli] args: [--in-place, --config, ./pyproject.toml] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.267' + rev: 'v0.0.269' hooks: - id: ruff args: [ --select, "PL", --select, "F" ] diff --git a/src/docformatter/syntax.py b/src/docformatter/syntax.py index 7aad330..5c148e3 100644 --- a/src/docformatter/syntax.py +++ b/src/docformatter/syntax.py @@ -30,6 +30,8 @@ import textwrap from typing import Iterable, List, Tuple, Union +DEFAULT_INDENT = 4 + BULLET_REGEX = r"\s*[*\-+] [\S ]+" """Regular expression to use for finding bullet lists.""" @@ -51,7 +53,7 @@ OPTION_REGEX = r"^-{1,2}[\S ]+ {2}\S+" """Regular expression to use for finding option lists.""" -REST_REGEX = r"(\.{2}|``) ?[\w-]+(:{1,2}|``)?" +REST_REGEX = r"((\.{2}|`{2}) ?[\w.~-]+(:{2}|`{2})?[\w ]*?|`[\w.~]+`)" """Regular expression to use for finding reST directives.""" SPHINX_REGEX = r":[a-zA-Z0-9_\- ]*:" @@ -466,7 +468,9 @@ def do_wrap_parameter_lists( # noqa: PLR0913 _parameter[1] : parameter_idx[_idx + 1][0] ].strip() except IndexError: - _parameter_description = text[_parameter[1] :].strip() + _parameter_description = ( + text[_parameter[1] :].strip().replace(" ", "").replace("\t", "") + ) if len(_parameter_description) <= (wrap_length - len(indentation)): lines.append( @@ -474,15 +478,20 @@ def do_wrap_parameter_lists( # noqa: PLR0913 f"{_parameter_description}" ) else: + if len(indentation) > DEFAULT_INDENT: + _subsequent = indentation + int(0.5 * len(indentation)) * " " + else: + _subsequent = 2 * indentation + lines.extend( textwrap.wrap( textwrap.dedent( f"{text[_parameter[0]:_parameter[1]]} " - f"{_parameter_description.replace(2*indentation, '')}" + f"{_parameter_description.strip()}" ), width=wrap_length, initial_indent=indentation, - subsequent_indent=2 * indentation, + subsequent_indent=_subsequent, ) ) @@ -537,12 +546,19 @@ def do_wrap_urls( wrap_length, ) ) + with contextlib.suppress(IndexError): - if not text[_url[0] - len(indentation) - 2] == "\n" and not _lines[-1]: + if text[_url[0] - len(indentation) - 2] != "\n" and not _lines[-1]: _lines.pop(-1) - # Add the URL. - _lines.append(f"{do_clean_url(text[_url[0] : _url[1]], indentation)}") + # Add the URL making sure that the leading quote is kept with a quoted URL. + _text = f"{text[_url[0]: _url[1]]}" + with contextlib.suppress(IndexError): + if _lines[0][-1] == '"': + _lines[0] = _lines[0][:-2] + _text = f'"{text[_url[0] : _url[1]]}' + + _lines.append(f"{do_clean_url(_text, indentation)}") text_idx = _url[1] diff --git a/tests/test_format_docstring.py b/tests/test_format_docstring.py index 54f9a41..515f89c 100644 --- a/tests/test_format_docstring.py +++ b/tests/test_format_docstring.py @@ -934,6 +934,60 @@ def test_format_docstring_for_one_line_summary_alone_but_too_long( ) ) + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_with_class_attributes(self, test_args, args): + """Wrap long class attribute docstrings.""" + uut = Formatter( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + docstring = '''\ +class TestClass: + """This is a class docstring.""" + + test_int = 1 + """This is a very, very, very long docstring that should really be + reformatted nicely by docformatter.""" +''' + assert docstring == uut._do_format_code( + '''\ +class TestClass: + """This is a class docstring.""" + + test_int = 1 + """This is a very, very, very long docstring that should really be reformatted nicely by docformatter.""" +''' + ) + + @pytest.mark.unit + @pytest.mark.parametrize("args", [[""]]) + def test_format_docstring_no_newline_in_summary_with_symbol(self, test_args, args): + """Wrap summary with symbol should not add newline. + + See issue #79. + """ + uut = Formatter( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + docstring = '''\ +def function2(): + """Hello yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeet + -v.""" +''' + assert docstring == uut._do_format_code(docstring) + + +class TestFormatWrapURL: + """Class for testing _do_format_docstring() with line wrapping and URLs.""" + @pytest.mark.unit @pytest.mark.parametrize( "args", @@ -1472,8 +1526,11 @@ def test_format_docstring_with_short_anonymous_link(self, test_args, args): @pytest.mark.unit @pytest.mark.parametrize("args", [[""]]) - def test_format_docstring_with_class_attributes(self, test_args, args): - """Wrap long class attribute docstrings.""" + def test_format_docstring_with_quoted_link(self, test_args, args): + """Anonymous link references should not be wrapped into the link. + + See issue #218. + """ uut = Formatter( test_args, sys.stderr, @@ -1482,43 +1539,32 @@ def test_format_docstring_with_class_attributes(self, test_args, args): ) docstring = '''\ -class TestClass: - """This is a class docstring.""" +"""Construct a candidate project URL from the bundle and app name. - test_int = 1 - """This is a very, very, very long docstring that should really be - reformatted nicely by docformatter.""" +It's not a perfect guess, but it's better than having +"https://example.com". + +:param bundle: The bundle identifier. +:param app_name: The app name. +:returns: The candidate project URL +""" ''' assert docstring == uut._do_format_code( '''\ -class TestClass: - """This is a class docstring.""" +"""Construct a candidate project URL from the bundle and app name. - test_int = 1 - """This is a very, very, very long docstring that should really be reformatted nicely by docformatter.""" +It's not a perfect guess, but it's better than having "https://example.com". + +:param bundle: The bundle identifier. +:param app_name: The app name. +:returns: The candidate project URL +""" ''' ) - @pytest.mark.unit - @pytest.mark.parametrize("args", [[""]]) - def test_format_docstring_no_newline_in_summary_with_symbol(self, test_args, args): - """Wrap summary with symbol should not add newline. - See issue #79. - """ - uut = Formatter( - test_args, - sys.stderr, - sys.stdin, - sys.stdout, - ) - - docstring = '''\ -def function2(): - """Hello yeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeet - -v.""" -''' - assert docstring == uut._do_format_code(docstring) +class TestFormatWrapBlack: + """Class for testing _do_format_docstring() with line wrapping and black option.""" @pytest.mark.unit @pytest.mark.parametrize( @@ -1585,6 +1631,10 @@ def test_format_docstring_black( ) ) + +class TestFormatWrapEpytext: + """Class for testing _do_format_docstring() with line wrapping and Epytext lists.""" + @pytest.mark.unit @pytest.mark.parametrize( "args", @@ -1720,6 +1770,10 @@ def test_format_docstring_non_epytext_style( ) ) + +class TestFormatWrapSphinx: + """Class for testing _do_format_docstring() with line wrapping and Sphinx lists.""" + @pytest.mark.unit @pytest.mark.parametrize( "args", @@ -1857,6 +1911,118 @@ def test_format_docstring_non_sphinx_style( ) ) + @pytest.mark.unit + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-descriptions", + "88", + "--wrap-summaries", + "88", + "", + ] + ], + ) + def test_format_docstring_sphinx_style_remove_excess_whitespace( + self, + test_args, + args, + ): + """Should remove unneeded whitespace. + + See issue #217 + """ + uut = Formatter( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert ( + ( + '''\ +"""Base for all Commands. + + :param logger: Logger for console and logfile. + :param console: Facilitates console interaction and input solicitation. + :param tools: Cache of tools populated by Commands as they are required. + :param apps: Dictionary of project's Apps keyed by app name. + :param base_path: Base directory for Briefcase project. + :param data_path: Base directory for Briefcase tools, support packages, etc. + :param is_clone: Flag that Command was triggered by the user's requested Command; + for instance, RunCommand can invoke UpdateCommand and/or BuildCommand. + """\ +''' + ) + == uut._do_format_docstring( + INDENTATION, + '''\ +"""Base for all Commands. + +:param logger: Logger for console and logfile. +:param console: Facilitates console interaction and input solicitation. +:param tools: Cache of tools populated by Commands as they are required. +:param apps: Dictionary of project's Apps keyed by app name. +:param base_path: Base directory for Briefcase project. +:param data_path: Base directory for Briefcase tools, support packages, etc. +:param is_clone: Flag that Command was triggered by the user's requested Command; + for instance, RunCommand can invoke UpdateCommand and/or BuildCommand. +"""\ +''', + ) + ) + + @pytest.mark.unit + @pytest.mark.parametrize( + "args", + [ + [ + "--wrap-descriptions", + "88", + "--wrap-summaries", + "88", + "", + ] + ], + ) + def test_format_docstring_sphinx_style_two_directives_in_row( + self, + test_args, + args, + ): + """Should remove unneeded whitespace. + + See issue #215. + """ + uut = Formatter( + test_args, + sys.stderr, + sys.stdin, + sys.stdout, + ) + + assert ( + ( + '''\ +"""Create or return existing HTTP session. + + :return: Requests :class:`~requests.Session` object + """\ +''' + ) + == uut._do_format_docstring( + INDENTATION, + '''\ +"""Create or return existing HTTP session. + + :return: Requests :class:`~requests.Session` object + """\ +''', + ) + ) + class TestFormatStyleOptions: """Class for testing format_docstring() when requesting style options."""