Skip to content

Commit cbf3976

Browse files
authored
Fix incorrect classification of property docstrings in Numpy-style (#5498)
1 parent 80285a9 commit cbf3976

File tree

7 files changed

+80
-64
lines changed

7 files changed

+80
-64
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ Release date: TBA
4242

4343
Closes #5323
4444

45+
* Fixed incorrect classification of Numpy-style docstring as Google-style docstring for
46+
docstrings with property setter documentation.
47+
Docstring classification is now based on the highest amount of matched sections instead
48+
of the order in which the docstring styles were tried.
49+
4550
* Fixed detection of ``arguments-differ`` when superclass static
4651
methods lacked a ``@staticmethod`` decorator.
4752

doc/whatsnew/2.13.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ Extensions
1919

2020
* Pyreverse - add output in mermaid-js format and html which is an mermaid js diagram with html boilerplate
2121

22+
* ``DocstringParameterChecker``
23+
24+
* Fixed incorrect classification of Numpy-style docstring as Google-style docstring for
25+
docstrings with property setter documentation.
26+
Docstring classification is now based on the highest amount of matched sections instead
27+
of the order in which the docstring styles were tried.
28+
2229
Other Changes
2330
=============
2431

pylint/extensions/_check_docs_utils.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -172,19 +172,20 @@ def possible_exc_types(node):
172172
return set()
173173

174174

175-
def docstringify(docstring, default_type="default"):
175+
def docstringify(docstring: str, default_type: str = "default") -> "Docstring":
176+
best_match = (0, DOCSTRING_TYPES.get(default_type, Docstring)(docstring))
176177
for docstring_type in (
177178
SphinxDocstring,
178179
EpytextDocstring,
179180
GoogleDocstring,
180181
NumpyDocstring,
181182
):
182183
instance = docstring_type(docstring)
183-
if instance.is_valid():
184-
return instance
184+
matching_sections = instance.matching_sections()
185+
if matching_sections > best_match[0]:
186+
best_match = (matching_sections, instance)
185187

186-
docstring_type = DOCSTRING_TYPES.get(default_type, Docstring)
187-
return docstring_type(docstring)
188+
return best_match[1]
188189

189190

190191
class Docstring:
@@ -210,8 +211,9 @@ def __init__(self, doc):
210211
def __repr__(self) -> str:
211212
return f"<{self.__class__.__name__}:'''{self.doc}'''>"
212213

213-
def is_valid(self):
214-
return False
214+
def matching_sections(self) -> int:
215+
"""Returns the number of matching docstring sections"""
216+
return 0
215217

216218
def exceptions(self):
217219
return set()
@@ -322,13 +324,17 @@ class SphinxDocstring(Docstring):
322324

323325
supports_yields = False
324326

325-
def is_valid(self):
326-
return bool(
327-
self.re_param_in_docstring.search(self.doc)
328-
or self.re_raise_in_docstring.search(self.doc)
329-
or self.re_rtype_in_docstring.search(self.doc)
330-
or self.re_returns_in_docstring.search(self.doc)
331-
or self.re_property_type_in_docstring.search(self.doc)
327+
def matching_sections(self) -> int:
328+
"""Returns the number of matching docstring sections"""
329+
return sum(
330+
bool(i)
331+
for i in (
332+
self.re_param_in_docstring.search(self.doc),
333+
self.re_raise_in_docstring.search(self.doc),
334+
self.re_rtype_in_docstring.search(self.doc),
335+
self.re_returns_in_docstring.search(self.doc),
336+
self.re_property_type_in_docstring.search(self.doc),
337+
)
332338
)
333339

334340
def exceptions(self):
@@ -526,13 +532,17 @@ class GoogleDocstring(Docstring):
526532

527533
supports_yields = True
528534

529-
def is_valid(self):
530-
return bool(
531-
self.re_param_section.search(self.doc)
532-
or self.re_raise_section.search(self.doc)
533-
or self.re_returns_section.search(self.doc)
534-
or self.re_yields_section.search(self.doc)
535-
or self.re_property_returns_line.search(self._first_line())
535+
def matching_sections(self) -> int:
536+
"""Returns the number of matching docstring sections"""
537+
return sum(
538+
bool(i)
539+
for i in (
540+
self.re_param_section.search(self.doc),
541+
self.re_raise_section.search(self.doc),
542+
self.re_returns_section.search(self.doc),
543+
self.re_yields_section.search(self.doc),
544+
self.re_property_returns_line.search(self._first_line()),
545+
)
536546
)
537547

538548
def has_params(self):

pylint/extensions/docparams.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def visit_raise(self, node: nodes.Raise) -> None:
307307
func_node = property_
308308

309309
doc = utils.docstringify(func_node.doc, self.config.default_docstring_type)
310-
if not doc.is_valid():
310+
if not doc.matching_sections():
311311
if doc.doc:
312312
self._handle_no_raise_doc(expected_excs, func_node)
313313
return

tests/extensions/test_check_docs.py

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -184,42 +184,6 @@ def do_something(): #@
184184
with self.assertNoMessages():
185185
self.checker.visit_functiondef(node)
186186

187-
def test_finds_missing_raises_from_setter_numpy(self) -> None:
188-
"""Example of a setter having missing raises documentation in
189-
the Numpy style docstring of the property
190-
"""
191-
property_node, node = astroid.extract_node(
192-
"""
193-
class Foo(object):
194-
@property
195-
def foo(self): #@
196-
'''int: docstring
197-
198-
Include a "Raises" section so that this is identified
199-
as a Numpy docstring and not a Google docstring.
200-
201-
Raises
202-
------
203-
RuntimeError
204-
Always
205-
'''
206-
raise RuntimeError()
207-
return 10
208-
209-
@foo.setter
210-
def foo(self, value):
211-
raise AttributeError() #@
212-
"""
213-
)
214-
with self.assertAddsMessages(
215-
MessageTest(
216-
msg_id="missing-raises-doc",
217-
node=property_node,
218-
args=("AttributeError",),
219-
)
220-
):
221-
self.checker.visit_raise(node)
222-
223187
def test_finds_missing_raises_from_setter_numpy_2(self) -> None:
224188
"""Example of a setter having missing raises documentation in
225189
its own Numpy style docstring of the property

tests/functional/ext/docparams/raise/missing_raises_doc_Numpy.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tests for missing-raises-doc and missing-raises-type-doc for Numpy style docstrings"""
22
# pylint: disable=function-redefined, invalid-name, undefined-variable, missing-function-docstring
33
# pylint: disable=unused-argument, try-except-raise, import-outside-toplevel
4+
# pylint: disable=too-few-public-methods, disallowed-name
45

56

67
def test_find_missing_numpy_raises(self): # [missing-raises-doc]
@@ -129,3 +130,30 @@ def test_find_invalid_missing_numpy_attr_raises(self):
129130
from re import error
130131

131132
raise error("hi")
133+
134+
135+
class Foo:
136+
"""test_finds_missing_raises_from_setter_numpy
137+
Example of a setter having missing raises documentation in
138+
the Numpy style docstring of the property
139+
"""
140+
141+
@property
142+
def foo(self): # [missing-raises-doc]
143+
"""int: docstring
144+
145+
Include a "Raises" section so that this is identified
146+
as a Numpy docstring and not a Google docstring.
147+
148+
Raises
149+
------
150+
RuntimeError
151+
Always
152+
"""
153+
raise RuntimeError()
154+
return 10 # [unreachable]
155+
156+
@foo.setter
157+
def foo(self, value):
158+
print(self)
159+
raise AttributeError()
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
missing-raises-doc:6:0:15:25:test_find_missing_numpy_raises:"""RuntimeError"" not documented as being raised":UNDEFINED
2-
unreachable:15:4:15:25:test_find_missing_numpy_raises:Unreachable code:UNDEFINED
3-
unreachable:29:4:29:25:test_find_all_numpy_raises:Unreachable code:UNDEFINED
4-
missing-raises-doc:32:0:45:25:test_find_rethrown_numpy_raises:"""RuntimeError"" not documented as being raised":UNDEFINED
5-
missing-raises-doc:48:0:61:25:test_find_rethrown_numpy_multiple_raises:"""RuntimeError, ValueError"" not documented as being raised":UNDEFINED
6-
missing-raises-doc:106:0:116:21:test_find_valid_missing_numpy_attr_raises:"""error"" not documented as being raised":UNDEFINED
1+
missing-raises-doc:7:0:16:25:test_find_missing_numpy_raises:"""RuntimeError"" not documented as being raised":UNDEFINED
2+
unreachable:16:4:16:25:test_find_missing_numpy_raises:Unreachable code:UNDEFINED
3+
unreachable:30:4:30:25:test_find_all_numpy_raises:Unreachable code:UNDEFINED
4+
missing-raises-doc:33:0:46:25:test_find_rethrown_numpy_raises:"""RuntimeError"" not documented as being raised":UNDEFINED
5+
missing-raises-doc:49:0:62:25:test_find_rethrown_numpy_multiple_raises:"""RuntimeError, ValueError"" not documented as being raised":UNDEFINED
6+
missing-raises-doc:107:0:117:21:test_find_valid_missing_numpy_attr_raises:"""error"" not documented as being raised":UNDEFINED
7+
missing-raises-doc:142:4:154:17:Foo.foo:"""AttributeError"" not documented as being raised":UNDEFINED
8+
unreachable:154:8:154:17:Foo.foo:Unreachable code:UNDEFINED

0 commit comments

Comments
 (0)