Skip to content
This repository has been archived by the owner on Nov 3, 2023. It is now read-only.

Commit

Permalink
add support for sphinx-style parameters to D417 (#595)
Browse files Browse the repository at this point in the history
* add support for sphinx-style parameters to D417

* add release notes entry for Sphinx D417 support

* tweak internal documentation for Sphinx-style D417
  • Loading branch information
benji-york authored Sep 22, 2023
1 parent 8e2bc99 commit 6d5455e
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
78 changes: 73 additions & 5 deletions src/pydocstyle/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*"
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
12 changes: 12 additions & 0 deletions src/tests/test_cases/sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
81 changes: 81 additions & 0 deletions src/tests/test_sphinx.py
Original file line number Diff line number Diff line change
@@ -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, '<test>')
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, '<test>')
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)")

0 comments on commit 6d5455e

Please sign in to comment.