Skip to content

Commit 26c9042

Browse files
kasiumDanielNoord
andauthored
Enable missing-raises-doc to understand class hierarchies (#5278)
Before, missing-raises-doc could not understand class hierarchies but just compared names. So, when a method documented a raise of a parent class, but a child exception was raised, the check failed. With this change, if the name compare doesn't help, the exception class hierarchy is checked. For that, possible_exc_types was changed to return classes instead of names Resolved #4955 Co-authored-by: Pierre Sassoulas <[email protected]> Co-authored-by: Daniël van Noord <[email protected]>
1 parent af8cc2e commit 26c9042

File tree

8 files changed

+111
-18
lines changed

8 files changed

+111
-18
lines changed

ChangeLog

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ Release date: TBA
8787
* The ``testutils`` for unittests now accept ``end_lineno`` and ``end_column``. Tests
8888
without these will trigger a ``DeprecationWarning``.
8989

90+
* ``missing-raises-doc`` will now check the class hierarchy of the raised exceptions
91+
92+
.. code-block:: python
93+
94+
def my_function()
95+
"""My function.
96+
97+
Raises:
98+
Exception: if something fails
99+
"""
100+
raise ValueError
101+
102+
Closes #4955
103+
90104
..
91105
Insert your changelog randomly, it will reduce merge conflicts
92106
(Ie. not necessarily at the end)

doc/whatsnew/2.13.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,17 @@ Other Changes
8080

8181
* The ``testutils`` for unittests now accept ``end_lineno`` and ``end_column``. Tests
8282
without these will trigger a ``DeprecationWarning``.
83+
84+
* ``missing-raises-doc`` will now check the class hierarchy of the raised exceptions
85+
86+
.. code-block:: python
87+
88+
def my_function()
89+
"""My function.
90+
91+
Raises:
92+
Exception: if something fails
93+
"""
94+
raise ValueError
95+
96+
Closes #4955

pylint/extensions/_check_docs_utils.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def _split_multiple_exc_types(target: str) -> List[str]:
120120
return re.split(delimiters, target)
121121

122122

123-
def possible_exc_types(node):
123+
def possible_exc_types(node: nodes.NodeNG) -> Set[nodes.ClassDef]:
124124
"""
125125
Gets all of the possible raised exception types for the given raise node.
126126
@@ -130,44 +130,45 @@ def possible_exc_types(node):
130130
131131
132132
:param node: The raise node to find exception types for.
133-
:type node: nodes.NodeNG
134133
135134
:returns: A list of exception types possibly raised by :param:`node`.
136-
:rtype: set(str)
137135
"""
138136
excs = []
139137
if isinstance(node.exc, nodes.Name):
140138
inferred = utils.safe_infer(node.exc)
141139
if inferred:
142-
excs = [inferred.name]
140+
excs = [inferred]
143141
elif node.exc is None:
144142
handler = node.parent
145143
while handler and not isinstance(handler, nodes.ExceptHandler):
146144
handler = handler.parent
147145

148146
if handler and handler.type:
149-
inferred_excs = astroid.unpack_infer(handler.type)
150-
excs = (exc.name for exc in inferred_excs if exc is not astroid.Uninferable)
147+
try:
148+
for exc in astroid.unpack_infer(handler.type):
149+
if exc is not astroid.Uninferable:
150+
excs.append(exc)
151+
except astroid.InferenceError:
152+
pass
151153
else:
152154
target = _get_raise_target(node)
153155
if isinstance(target, nodes.ClassDef):
154-
excs = [target.name]
156+
excs = [target]
155157
elif isinstance(target, nodes.FunctionDef):
156158
for ret in target.nodes_of_class(nodes.Return):
157159
if ret.frame() != target:
158160
# return from inner function - ignore it
159161
continue
160162

161163
val = utils.safe_infer(ret.value)
162-
if (
163-
val
164-
and isinstance(val, (astroid.Instance, nodes.ClassDef))
165-
and utils.inherit_from_std_ex(val)
166-
):
167-
excs.append(val.name)
164+
if val and utils.inherit_from_std_ex(val):
165+
if isinstance(val, nodes.ClassDef):
166+
excs.append(val)
167+
elif isinstance(val, astroid.Instance):
168+
excs.append(val.getattr("__class__")[0])
168169

169170
try:
170-
return {exc for exc in excs if not utils.node_ignores_exception(node, exc)}
171+
return {exc for exc in excs if not utils.node_ignores_exception(node, exc.name)}
171172
except astroid.InferenceError:
172173
return set()
173174

pylint/extensions/docparams.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,14 +309,25 @@ def visit_raise(self, node: nodes.Raise) -> None:
309309
doc = utils.docstringify(func_node.doc, self.config.default_docstring_type)
310310
if not doc.matching_sections():
311311
if doc.doc:
312-
self._handle_no_raise_doc(expected_excs, func_node)
312+
missing = {exc.name for exc in expected_excs}
313+
self._handle_no_raise_doc(missing, func_node)
313314
return
314315

315316
found_excs_full_names = doc.exceptions()
316317

317318
# Extract just the class name, e.g. "error" from "re.error"
318319
found_excs_class_names = {exc.split(".")[-1] for exc in found_excs_full_names}
319-
missing_excs = expected_excs - found_excs_class_names
320+
321+
missing_excs = set()
322+
for expected in expected_excs:
323+
for found_exc in found_excs_class_names:
324+
if found_exc == expected.name:
325+
break
326+
if any(found_exc == ancestor.name for ancestor in expected.ancestors()):
327+
break
328+
else:
329+
missing_excs.add(expected.name)
330+
320331
self._add_raise_message(missing_excs, func_node)
321332

322333
def visit_return(self, node: nodes.Return) -> None:

tests/extensions/test_check_docs_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,5 +147,7 @@ def my_func():
147147
],
148148
)
149149
def test_exception(raise_node, expected):
150-
found = utils.possible_exc_types(raise_node)
151-
assert found == expected
150+
found_nodes = utils.possible_exc_types(raise_node)
151+
for node in found_nodes:
152+
assert isinstance(node, astroid.nodes.ClassDef)
153+
assert {node.name for node in found_nodes} == expected
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Tests for missing-raises-doc for exception class inheritance."""
2+
# pylint: disable=missing-class-docstring
3+
4+
class CustomError(NameError):
5+
pass
6+
7+
8+
class CustomChildError(CustomError):
9+
pass
10+
11+
12+
def test_find_missing_raise_for_parent(): # [missing-raises-doc]
13+
"""This is a docstring.
14+
15+
Raises:
16+
CustomError: Never
17+
"""
18+
raise NameError("hi")
19+
20+
21+
def test_no_missing_raise_for_child_builtin():
22+
"""This is a docstring.
23+
24+
Raises:
25+
Exception: Never
26+
"""
27+
raise ValueError("hi")
28+
29+
30+
def test_no_missing_raise_for_child_custom():
31+
"""This is a docstring.
32+
33+
Raises:
34+
NameError: Never
35+
"""
36+
raise CustomError("hi")
37+
38+
39+
def test_no_missing_raise_for_child_custom_nested():
40+
"""This is a docstring.
41+
42+
Raises:
43+
NameError: Never
44+
"""
45+
raise CustomChildError("hi")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[MASTER]
2+
load-plugins = pylint.extensions.docparams
3+
4+
[BASIC]
5+
accept-no-raise-doc=no
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
missing-raises-doc:12:0:18:25:test_find_missing_raise_for_parent:"""NameError""" not documented as being raised:UNDEFINED

0 commit comments

Comments
 (0)