From 6d5455e4050b9f8b0905f9d5b0bc7bb73c6ef442 Mon Sep 17 00:00:00 2001 From: Benji York Date: Fri, 22 Sep 2023 09:11:51 -0500 Subject: [PATCH] add support for sphinx-style parameters to D417 (#595) * add support for sphinx-style parameters to D417 * add release notes entry for Sphinx D417 support * tweak internal documentation for Sphinx-style D417 --- docs/release_notes.rst | 1 + src/pydocstyle/checker.py | 78 ++++++++++++++++++++++++++++-- src/tests/test_cases/sections.py | 12 +++++ src/tests/test_sphinx.py | 81 ++++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 src/tests/test_sphinx.py diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 5ab5e11..46e3656 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -48,6 +48,7 @@ New Features * Add support for `property_decorators` config to ignore D401. * Add support for Python 3.10 (#554). * Replace D10X errors with D419 if docstring exists but is empty (#559). +* Add support for Sphinx-style parameter descriptions to D417. Bug Fixes diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index 9b6376b..981f292 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -111,7 +111,7 @@ class ConventionChecker: # Begins with 0 or more whitespace characters r"^\s*" # Followed by 1 or more unicode chars, numbers or underscores - # The above is captured as the first group as this is the paramater name. + # The below is captured as the first group as this is the parameter name. r"(\w+)" # Followed by 0 or more whitespace characters r"\s*" @@ -129,6 +129,20 @@ class ConventionChecker: ".+" ) + SPHINX_ARGS_REGEX = re( + # Begins with 0 or more whitespace characters + r"^\s*" + # Followed by the parameter marker + r":param " + # Followed by 1 or more unicode chars, numbers or underscores and a colon + # The parameter name is captured as the first group. + r"(\w+):" + # Followed by 0 or more whitespace characters + r"\s*" + # Next is the parameter description + r".+$" + ) + def check_source( self, source, @@ -905,6 +919,56 @@ def _check_args_section(docstring, definition, context): docstring_args, definition ) + @staticmethod + def _find_sphinx_params(lines): + """D417: Sphinx param section checks. + + Check for a valid Sphinx-style parameter section. + * The section documents all function arguments (D417) + except `self` or `cls` if it is a method. + + Documentation for each arg should start at the same indentation + level:: + + :param x: Lorem ipsum dolor sit amet + :param y: Ut enim ad minim veniam + """ + params = [] + for line in lines: + match = ConventionChecker.SPHINX_ARGS_REGEX.match(line) + if match: + params.append(match.group(1)) + return params + + @staticmethod + def _check_sphinx_params(lines, definition): + """D417: Sphinx param section checks. + + Check for a valid Sphinx-style parameter section. + * The section documents all function arguments (D417) + except `self` or `cls` if it is a method. + + Documentation for each arg should start at the same indentation + level. For example, in this case x and y are distinguishable:: + + :param x: Lorem ipsum dolor sit amet + :param y: Ut enim ad minim veniam + + In the case below, we only recognize x as a documented parameter + because the rest of the content is indented as if it belongs + to the description for x:: + + :param x: Lorem ipsum dolor sit amet + :param y: Ut enim ad minim veniam + """ + docstring_args = set(ConventionChecker._find_sphinx_params(lines)) + if docstring_args: + yield from ConventionChecker._check_missing_args( + docstring_args, definition + ) + return True + return False + @staticmethod def _check_missing_args(docstring_args, definition): """D417: Yield error for missing arguments in docstring. @@ -1093,10 +1157,14 @@ def check_docstring_sections(self, definition, docstring): found_numpy = yield from self._check_numpy_sections( lines, definition, docstring ) - if not found_numpy: - yield from self._check_google_sections( - lines, definition, docstring - ) + if found_numpy: + return + + found_sphinx = yield from self._check_sphinx_params(lines, definition) + if found_sphinx: + return + + yield from self._check_google_sections(lines, definition, docstring) parse = Parser() diff --git a/src/tests/test_cases/sections.py b/src/tests/test_cases/sections.py index 5bf9a7b..5fc95b0 100644 --- a/src/tests/test_cases/sections.py +++ b/src/tests/test_cases/sections.py @@ -408,6 +408,18 @@ def test_missing_numpy_args(_private_arg=0, x=1, y=2): # noqa: D406, D407 """ +@expect(_D213) +@expect("D417: Missing argument descriptions in the docstring " + "(argument(s) y are missing descriptions in " + "'test_missing_sphynx_args' docstring)") +def test_missing_sphynx_args(_private_arg=0, x=1, y=2): # noqa: D406, D407 + """Toggle the gizmo. + + :param x: The greatest integer in the history \ +of the entire world. + + """ + class TestNumpy: # noqa: D203 """Test class.""" diff --git a/src/tests/test_sphinx.py b/src/tests/test_sphinx.py new file mode 100644 index 0000000..7da5567 --- /dev/null +++ b/src/tests/test_sphinx.py @@ -0,0 +1,81 @@ +"""Unit tests for Sphinx-style parameter documentation rules. + +Use tox or pytest to run the test suite. +""" +from pydocstyle.checker import ConventionChecker +import textwrap +import pytest + +SPHINX_ARGS_REGEX = ConventionChecker.SPHINX_ARGS_REGEX + +def test_parameter_found(): + """The regex matches a line with a parameter definition.""" + line = " :param x: Lorem ipsum dolor sit amet\n" + assert SPHINX_ARGS_REGEX.match(line) is not None + + +def test_parameter_name_extracted(): + """The first match group is the parameter name.""" + line = " :param foo: Lorem ipsum dolor sit amet\n" + assert SPHINX_ARGS_REGEX.match(line).group(1) == "foo" + + +def test_finding_params(): + """Sphinx-style parameter names are found.""" + docstring = """A description of a great function. + + :param foo: Lorem ipsum dolor sit amet + :param bar: Ut enim ad minim veniam + """ + + lines = docstring.splitlines(keepends=True) + assert ConventionChecker._find_sphinx_params(lines) == ['foo', 'bar'] + + +def test_missing_params(): + """Missing parameters are reported.""" + source = textwrap.dedent('''\ + def thing(foo, bar, baz): + """Do great things. + + :param foo: Lorem ipsum dolor sit amet + :param baz: Ut enim ad minim veniam + """ + pass + ''') + errors = ConventionChecker().check_source(source, '') + for error in errors: + if error.code == "D417": + break + else: + pytest.fail('did not find D417 error') + + assert error.parameters == ('bar', 'thing') + assert error.message == ( + "D417: Missing argument descriptions in the docstring (argument(s) bar are" + " missing descriptions in 'thing' docstring)") + + +def test_missing_description(): + """A parameter is considered missing if it has no description.""" + source = textwrap.dedent('''\ + def thing(foo, bar, baz): + """Do great things. + + :param foo: Lorem ipsum dolor sit amet + :param bar: + :param baz: Ut enim ad minim veniam + """ + pass + ''') + errors = ConventionChecker().check_source(source, '') + for error in errors: + if error.code == "D417": + break + else: + pytest.fail('did not find D417 error') + + assert error.parameters == ('bar', 'thing') + assert error.message == ( + "D417: Missing argument descriptions in the docstring (argument(s) bar are" + " missing descriptions in 'thing' docstring)")