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

Commit 6d5455e

Browse files
authored
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
1 parent 8e2bc99 commit 6d5455e

File tree

4 files changed

+167
-5
lines changed

4 files changed

+167
-5
lines changed

docs/release_notes.rst

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ New Features
4848
* Add support for `property_decorators` config to ignore D401.
4949
* Add support for Python 3.10 (#554).
5050
* Replace D10X errors with D419 if docstring exists but is empty (#559).
51+
* Add support for Sphinx-style parameter descriptions to D417.
5152

5253
Bug Fixes
5354

src/pydocstyle/checker.py

+73-5
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class ConventionChecker:
111111
# Begins with 0 or more whitespace characters
112112
r"^\s*"
113113
# Followed by 1 or more unicode chars, numbers or underscores
114-
# The above is captured as the first group as this is the paramater name.
114+
# The below is captured as the first group as this is the parameter name.
115115
r"(\w+)"
116116
# Followed by 0 or more whitespace characters
117117
r"\s*"
@@ -129,6 +129,20 @@ class ConventionChecker:
129129
".+"
130130
)
131131

132+
SPHINX_ARGS_REGEX = re(
133+
# Begins with 0 or more whitespace characters
134+
r"^\s*"
135+
# Followed by the parameter marker
136+
r":param "
137+
# Followed by 1 or more unicode chars, numbers or underscores and a colon
138+
# The parameter name is captured as the first group.
139+
r"(\w+):"
140+
# Followed by 0 or more whitespace characters
141+
r"\s*"
142+
# Next is the parameter description
143+
r".+$"
144+
)
145+
132146
def check_source(
133147
self,
134148
source,
@@ -905,6 +919,56 @@ def _check_args_section(docstring, definition, context):
905919
docstring_args, definition
906920
)
907921

922+
@staticmethod
923+
def _find_sphinx_params(lines):
924+
"""D417: Sphinx param section checks.
925+
926+
Check for a valid Sphinx-style parameter section.
927+
* The section documents all function arguments (D417)
928+
except `self` or `cls` if it is a method.
929+
930+
Documentation for each arg should start at the same indentation
931+
level::
932+
933+
:param x: Lorem ipsum dolor sit amet
934+
:param y: Ut enim ad minim veniam
935+
"""
936+
params = []
937+
for line in lines:
938+
match = ConventionChecker.SPHINX_ARGS_REGEX.match(line)
939+
if match:
940+
params.append(match.group(1))
941+
return params
942+
943+
@staticmethod
944+
def _check_sphinx_params(lines, definition):
945+
"""D417: Sphinx param section checks.
946+
947+
Check for a valid Sphinx-style parameter section.
948+
* The section documents all function arguments (D417)
949+
except `self` or `cls` if it is a method.
950+
951+
Documentation for each arg should start at the same indentation
952+
level. For example, in this case x and y are distinguishable::
953+
954+
:param x: Lorem ipsum dolor sit amet
955+
:param y: Ut enim ad minim veniam
956+
957+
In the case below, we only recognize x as a documented parameter
958+
because the rest of the content is indented as if it belongs
959+
to the description for x::
960+
961+
:param x: Lorem ipsum dolor sit amet
962+
:param y: Ut enim ad minim veniam
963+
"""
964+
docstring_args = set(ConventionChecker._find_sphinx_params(lines))
965+
if docstring_args:
966+
yield from ConventionChecker._check_missing_args(
967+
docstring_args, definition
968+
)
969+
return True
970+
return False
971+
908972
@staticmethod
909973
def _check_missing_args(docstring_args, definition):
910974
"""D417: Yield error for missing arguments in docstring.
@@ -1093,10 +1157,14 @@ def check_docstring_sections(self, definition, docstring):
10931157
found_numpy = yield from self._check_numpy_sections(
10941158
lines, definition, docstring
10951159
)
1096-
if not found_numpy:
1097-
yield from self._check_google_sections(
1098-
lines, definition, docstring
1099-
)
1160+
if found_numpy:
1161+
return
1162+
1163+
found_sphinx = yield from self._check_sphinx_params(lines, definition)
1164+
if found_sphinx:
1165+
return
1166+
1167+
yield from self._check_google_sections(lines, definition, docstring)
11001168

11011169

11021170
parse = Parser()

src/tests/test_cases/sections.py

+12
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,18 @@ def test_missing_numpy_args(_private_arg=0, x=1, y=2): # noqa: D406, D407
408408
409409
"""
410410

411+
@expect(_D213)
412+
@expect("D417: Missing argument descriptions in the docstring "
413+
"(argument(s) y are missing descriptions in "
414+
"'test_missing_sphynx_args' docstring)")
415+
def test_missing_sphynx_args(_private_arg=0, x=1, y=2): # noqa: D406, D407
416+
"""Toggle the gizmo.
417+
418+
:param x: The greatest integer in the history \
419+
of the entire world.
420+
421+
"""
422+
411423

412424
class TestNumpy: # noqa: D203
413425
"""Test class."""

src/tests/test_sphinx.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Unit tests for Sphinx-style parameter documentation rules.
2+
3+
Use tox or pytest to run the test suite.
4+
"""
5+
from pydocstyle.checker import ConventionChecker
6+
import textwrap
7+
import pytest
8+
9+
SPHINX_ARGS_REGEX = ConventionChecker.SPHINX_ARGS_REGEX
10+
11+
def test_parameter_found():
12+
"""The regex matches a line with a parameter definition."""
13+
line = " :param x: Lorem ipsum dolor sit amet\n"
14+
assert SPHINX_ARGS_REGEX.match(line) is not None
15+
16+
17+
def test_parameter_name_extracted():
18+
"""The first match group is the parameter name."""
19+
line = " :param foo: Lorem ipsum dolor sit amet\n"
20+
assert SPHINX_ARGS_REGEX.match(line).group(1) == "foo"
21+
22+
23+
def test_finding_params():
24+
"""Sphinx-style parameter names are found."""
25+
docstring = """A description of a great function.
26+
27+
:param foo: Lorem ipsum dolor sit amet
28+
:param bar: Ut enim ad minim veniam
29+
"""
30+
31+
lines = docstring.splitlines(keepends=True)
32+
assert ConventionChecker._find_sphinx_params(lines) == ['foo', 'bar']
33+
34+
35+
def test_missing_params():
36+
"""Missing parameters are reported."""
37+
source = textwrap.dedent('''\
38+
def thing(foo, bar, baz):
39+
"""Do great things.
40+
41+
:param foo: Lorem ipsum dolor sit amet
42+
:param baz: Ut enim ad minim veniam
43+
"""
44+
pass
45+
''')
46+
errors = ConventionChecker().check_source(source, '<test>')
47+
for error in errors:
48+
if error.code == "D417":
49+
break
50+
else:
51+
pytest.fail('did not find D417 error')
52+
53+
assert error.parameters == ('bar', 'thing')
54+
assert error.message == (
55+
"D417: Missing argument descriptions in the docstring (argument(s) bar are"
56+
" missing descriptions in 'thing' docstring)")
57+
58+
59+
def test_missing_description():
60+
"""A parameter is considered missing if it has no description."""
61+
source = textwrap.dedent('''\
62+
def thing(foo, bar, baz):
63+
"""Do great things.
64+
65+
:param foo: Lorem ipsum dolor sit amet
66+
:param bar:
67+
:param baz: Ut enim ad minim veniam
68+
"""
69+
pass
70+
''')
71+
errors = ConventionChecker().check_source(source, '<test>')
72+
for error in errors:
73+
if error.code == "D417":
74+
break
75+
else:
76+
pytest.fail('did not find D417 error')
77+
78+
assert error.parameters == ('bar', 'thing')
79+
assert error.message == (
80+
"D417: Missing argument descriptions in the docstring (argument(s) bar are"
81+
" missing descriptions in 'thing' docstring)")

0 commit comments

Comments
 (0)