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

Commit 37075e6

Browse files
committed
add support for sphinx-style parameters to D417
1 parent 8e2bc99 commit 37075e6

File tree

3 files changed

+173
-5
lines changed

3 files changed

+173
-5
lines changed

src/pydocstyle/checker.py

Lines changed: 80 additions & 5 deletions
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,63 @@ 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. For example, in this case x and y are distinguishable::
932+
933+
:param x: Lorem ipsum dolor sit amet
934+
:param y: Ut enim ad minim veniam
935+
936+
In the case below, we only recognize x as a documented parameter
937+
because the rest of the content is indented as if it belongs
938+
to the description for x::
939+
940+
:param x: Lorem ipsum dolor sit amet
941+
:param y: Ut enim ad minim veniam
942+
"""
943+
params = []
944+
for line in lines:
945+
match = ConventionChecker.SPHINX_ARGS_REGEX.match(line)
946+
if match:
947+
params.append(match.group(1))
948+
return params
949+
950+
@staticmethod
951+
def _check_sphinx_params(lines, definition):
952+
"""D417: Sphinx param section checks.
953+
954+
Check for a valid Sphinx-style parameter section.
955+
* The section documents all function arguments (D417)
956+
except `self` or `cls` if it is a method.
957+
958+
Documentation for each arg should start at the same indentation
959+
level. For example, in this case x and y are distinguishable::
960+
961+
:param x: Lorem ipsum dolor sit amet
962+
:param y: Ut enim ad minim veniam
963+
964+
In the case below, we only recognize x as a documented parameter
965+
because the rest of the content is indented as if it belongs
966+
to the description for x::
967+
968+
:param x: Lorem ipsum dolor sit amet
969+
:param y: Ut enim ad minim veniam
970+
"""
971+
docstring_args = set(ConventionChecker._find_sphinx_params(lines))
972+
if docstring_args:
973+
yield from ConventionChecker._check_missing_args(
974+
docstring_args, definition
975+
)
976+
return True
977+
return False
978+
908979
@staticmethod
909980
def _check_missing_args(docstring_args, definition):
910981
"""D417: Yield error for missing arguments in docstring.
@@ -1093,10 +1164,14 @@ def check_docstring_sections(self, definition, docstring):
10931164
found_numpy = yield from self._check_numpy_sections(
10941165
lines, definition, docstring
10951166
)
1096-
if not found_numpy:
1097-
yield from self._check_google_sections(
1098-
lines, definition, docstring
1099-
)
1167+
if found_numpy:
1168+
return
1169+
1170+
found_sphinx = yield from self._check_sphinx_params(lines, definition)
1171+
if found_sphinx:
1172+
return
1173+
1174+
yield from self._check_google_sections(lines, definition, docstring)
11001175

11011176

11021177
parse = Parser()

src/tests/test_cases/sections.py

Lines changed: 12 additions & 0 deletions
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

Lines changed: 81 additions & 0 deletions
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)