Skip to content

Commit e205043

Browse files
committed
fix: DOC503 catch namespaced exceptions
Previously, AST walking was not properly identifying the name of exceptions like `m.Exception`. Update logic for determining names from an AST to correctly get the whole name (as well as related tests).
1 parent f758604 commit e205043

File tree

6 files changed

+140
-7
lines changed

6 files changed

+140
-7
lines changed

pydoclint/utils/return_yield_raise.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import ast
2-
from typing import Callable, Dict, Generator, List, Optional, Tuple, Type
2+
from typing import (
3+
Callable,
4+
Dict,
5+
Generator,
6+
List,
7+
Optional,
8+
Tuple,
9+
Type,
10+
Union,
11+
)
312

413
from pydoclint.utils import walk
514
from pydoclint.utils.annotation import unparseAnnotation
@@ -98,6 +107,14 @@ def getRaisedExceptions(node: FuncOrAsyncFuncDef) -> List[str]:
98107
return sorted(set(_getRaisedExceptions(node)))
99108

100109

110+
def _getFullAttribute(node: Union[ast.Attribute, ast.Name]) -> str:
111+
"""Get the full name of a symbol like a.b.c.foo"""
112+
if isinstance(node, ast.Name):
113+
return node.id
114+
115+
return _getFullAttribute(node.value) + '.' + node.attr
116+
117+
101118
def _getRaisedExceptions(
102119
node: FuncOrAsyncFuncDef,
103120
) -> Generator[str, None, None]:
@@ -132,7 +149,17 @@ def _getRaisedExceptions(
132149
):
133150
for subnode, _ in walk.walk_dfs(child):
134151
if isinstance(subnode, ast.Name):
135-
yield subnode.id
152+
if isinstance(child.exc, ast.Attribute):
153+
# case: looks like m.n.exception
154+
yield _getFullAttribute(child.exc)
155+
elif isinstance(child.exc, ast.Call) and isinstance(
156+
child.exc.func, ast.Attribute
157+
):
158+
# case: looks like m.n.exception()
159+
yield _getFullAttribute(child.exc.func)
160+
else:
161+
yield subnode.id
162+
136163
break
137164
else:
138165
# if "raise" statement was alone, it must be inside an "except"
@@ -149,9 +176,12 @@ def _extractExceptionsFromExcept(
149176
yield node.type.id
150177

151178
if isinstance(node.type, ast.Tuple):
152-
for child, _ in walk.walk(node.type):
153-
if isinstance(child, ast.Name):
154-
yield child.id
179+
for elt in node.type.elts:
180+
if isinstance(elt, ast.Attribute):
181+
# case: looks like m.n.exception
182+
yield _getFullAttribute(elt)
183+
elif isinstance(elt, ast.Name):
184+
yield elt.id
155185

156186

157187
def _hasExpectedStatements(

tests/data/google/raises/cases.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,26 @@ def func13(self) -> None:
182182
ValueError: typo!
183183
"""
184184
raise ValueError
185+
186+
def func14(self) -> None:
187+
"""
188+
Should fail, expects `exceptions.CustomError`.
189+
190+
Raises:
191+
CustomError: every time.
192+
"""
193+
exceptions = object()
194+
exceptions.CustomError = CustomError
195+
raise exceptions.CustomError()
196+
197+
def func15(self) -> None:
198+
"""
199+
Should fail, expects `exceptions.m.CustomError`.
200+
201+
Raises:
202+
CustomError: every time.
203+
"""
204+
exceptions = object()
205+
exceptions.m = object()
206+
exceptions.m.CustomError = CustomError
207+
raise exceptions.m.CustomError

tests/data/numpy/raises/cases.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,30 @@ def func13(self) -> None:
229229
typo!
230230
"""
231231
raise ValueError
232+
233+
def func14(self) -> None:
234+
"""
235+
Should fail, expects `exceptions.CustomError`.
236+
237+
Raises
238+
------
239+
CustomError
240+
every time.
241+
"""
242+
exceptions = object()
243+
exceptions.CustomError = CustomError
244+
raise exceptions.CustomError()
245+
246+
def func15(self) -> None:
247+
"""
248+
Should fail, expects `exceptions.m.CustomError`.
249+
250+
Raises
251+
------
252+
CustomError
253+
every time.
254+
"""
255+
exceptions = object()
256+
exceptions.m = object()
257+
exceptions.m.CustomError = CustomError
258+
raise exceptions.m.CustomError

tests/data/sphinx/raises/cases.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,24 @@ def func13(self) -> None:
153153
:raises ValueError: typo!
154154
"""
155155
raise ValueError
156+
157+
def func14(self) -> None:
158+
"""
159+
Should fail, expects `exceptions.CustomError`.
160+
161+
:raises CustomError: every time.
162+
"""
163+
exceptions = object()
164+
exceptions.CustomError = CustomError
165+
raise exceptions.CustomError()
166+
167+
def func15(self) -> None:
168+
"""
169+
Should fail, expects `exceptions.m.CustomError`.
170+
171+
:raises CustomError: every time.
172+
"""
173+
exceptions = object()
174+
exceptions.m = object()
175+
exceptions.m.CustomError = CustomError
176+
raise exceptions.m.CustomError

tests/test_main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,14 @@ def testRaises(style: str, skipRaisesCheck: bool) -> None:
832832
'docstring do not match those in the function body Raises values in the '
833833
"docstring: ['ValueError', 'ValueError']. Raised exceptions in the body: "
834834
"['ValueError'].",
835+
'DOC503: Method `B.func14` exceptions in the "Raises" section in the '
836+
'docstring do not match those in the function body Raises values in the '
837+
"docstring: ['CustomError']. Raised exceptions in the body: "
838+
"['exceptions.CustomError'].",
839+
'DOC503: Method `B.func15` exceptions in the "Raises" section in the '
840+
'docstring do not match those in the function body Raises values in the '
841+
"docstring: ['CustomError']. Raised exceptions in the body: "
842+
"['exceptions.m.CustomError'].",
835843
]
836844
expected1 = []
837845
expected = expected1 if skipRaisesCheck else expected0

tests/utils/test_returns_yields_raise.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ def func7(arg0):
357357
def func8(d):
358358
try:
359359
d[0][0]
360-
except (KeyError, TypeError):
360+
except (KeyError, TypeError, m.ValueError):
361361
raise
362362
finally:
363363
pass
@@ -416,6 +416,22 @@ def func12(a):
416416
417417
if a < 3:
418418
raise Error3
419+
420+
def func13(a):
421+
# ensure we get `Exception` and `Exception()`
422+
if a < 1:
423+
raise ValueError
424+
else:
425+
raise TypeError()
426+
427+
def func14(a):
428+
# check that we properly identify submodule exceptions.
429+
if a < 1:
430+
raise m.ValueError
431+
elif a < 2:
432+
raise m.n.ValueError()
433+
else:
434+
raise a.b.c.ValueError(msg="some msg")
419435
"""
420436

421437

@@ -439,6 +455,8 @@ def testHasRaiseStatements() -> None:
439455
(75, 0, 'func10'): True,
440456
(83, 0, 'func11'): True,
441457
(100, 0, 'func12'): True,
458+
(117, 0, 'func13'): True,
459+
(124, 0, 'func14'): True,
442460
}
443461

444462
assert result == expected
@@ -464,11 +482,17 @@ def testWhichRaiseStatements() -> None:
464482
'RuntimeError',
465483
'TypeError',
466484
],
467-
(54, 0, 'func8'): ['KeyError', 'TypeError'],
485+
(54, 0, 'func8'): ['KeyError', 'TypeError', 'm.ValueError'],
468486
(62, 0, 'func9'): ['AssertionError', 'IndexError'],
469487
(75, 0, 'func10'): ['GError'],
470488
(83, 0, 'func11'): ['ValueError'],
471489
(100, 0, 'func12'): ['Error1', 'Error2', 'Error3'],
490+
(117, 0, 'func13'): ['TypeError', 'ValueError'],
491+
(124, 0, 'func14'): [
492+
'a.b.c.ValueError',
493+
'm.ValueError',
494+
'm.n.ValueError',
495+
],
472496
}
473497

474498
assert result == expected

0 commit comments

Comments
 (0)