Skip to content

Commit 5e941f5

Browse files
Amar1729jsh9
andauthored
fix: DOC503 catch namespaced exceptions (#168)
Co-authored-by: jsh9 <[email protected]>
1 parent f758604 commit 5e941f5

File tree

9 files changed

+160
-9
lines changed

9 files changed

+160
-9
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Change Log
22

3+
## [0.5.8] - 2024-09-23
4+
5+
- Fixed
6+
7+
- Fixed the logic of handling exceptions namespaces (`a.b.c.MyException`)
8+
9+
- Full diff
10+
- https://github.com/jsh9/pydoclint/compare/0.5.7...0.5.8
11+
312
## [0.5.7] - 2024-09-02
413

514
- Added
@@ -8,8 +17,12 @@
817
function body match those in the "Raises" section of the docstring
918

1019
- Changed
20+
1121
- Switched from tab to 4 spaces in baseline
1222

23+
- Full diff
24+
- https://github.com/jsh9/pydoclint/compare/0.5.6...0.5.7
25+
1326
## [0.5.6] - 2024-07-17
1427

1528
- Fixed

pydoclint/utils/generic.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ast
22
import copy
33
import re
4-
from typing import List, Match, Optional, Tuple
4+
from typing import List, Match, Optional, Tuple, Union
55

66
from pydoclint.utils.astTypes import ClassOrFunctionDef, FuncOrAsyncFuncDef
77
from pydoclint.utils.method_type import MethodType
@@ -233,3 +233,11 @@ def specialEqual(str1: str, str2: str) -> bool:
233233
return False
234234

235235
return True
236+
237+
238+
def getFullAttributeName(node: Union[ast.Attribute, ast.Name]) -> str:
239+
"""Get the full name of a symbol like a.b.c.foo"""
240+
if isinstance(node, ast.Name):
241+
return node.id
242+
243+
return getFullAttributeName(node.value) + '.' + node.attr

pydoclint/utils/return_yield_raise.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pydoclint.utils import walk
55
from pydoclint.utils.annotation import unparseAnnotation
66
from pydoclint.utils.astTypes import BlockType, FuncOrAsyncFuncDef
7-
from pydoclint.utils.generic import stringStartsWith
7+
from pydoclint.utils.generic import getFullAttributeName, stringStartsWith
88

99
ReturnType = Type[ast.Return]
1010
ExprType = Type[ast.Expr]
@@ -132,7 +132,17 @@ def _getRaisedExceptions(
132132
):
133133
for subnode, _ in walk.walk_dfs(child):
134134
if isinstance(subnode, ast.Name):
135-
yield subnode.id
135+
if isinstance(child.exc, ast.Attribute):
136+
# case: looks like m.n.exception
137+
yield getFullAttributeName(child.exc)
138+
elif isinstance(child.exc, ast.Call) and isinstance(
139+
child.exc.func, ast.Attribute
140+
):
141+
# case: looks like m.n.exception()
142+
yield getFullAttributeName(child.exc.func)
143+
else:
144+
yield subnode.id
145+
136146
break
137147
else:
138148
# if "raise" statement was alone, it must be inside an "except"
@@ -148,10 +158,17 @@ def _extractExceptionsFromExcept(
148158
if isinstance(node.type, ast.Name):
149159
yield node.type.id
150160

161+
if isinstance(node.type, ast.Attribute):
162+
# case: looks like m.n.exception
163+
yield getFullAttributeName(node.type)
164+
151165
if isinstance(node.type, ast.Tuple):
152-
for child, _ in walk.walk(node.type):
153-
if isinstance(child, ast.Name):
154-
yield child.id
166+
for elt in node.type.elts:
167+
if isinstance(elt, ast.Attribute):
168+
# case: looks like m.n.exception
169+
yield getFullAttributeName(elt)
170+
elif isinstance(elt, ast.Name):
171+
yield elt.id
155172

156173

157174
def _hasExpectedStatements(

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = pydoclint
3-
version = 0.5.7
3+
version = 0.5.8
44
description = A Python docstring linter that checks arguments, returns, yields, and raises sections
55
long_description = file: README.md
66
long_description_content_type = text/markdown

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: 36 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,30 @@ def func12(a):
416416
417417
if a < 3:
418418
raise Error3
419+
420+
def func13(a):
421+
# ensure we get `Exception`, `Exception()`, and `Exception('something')`
422+
if a < 1:
423+
raise ValueError
424+
elif a < 2:
425+
raise TypeError()
426+
else:
427+
raise IOError('IO Error!')
428+
429+
def func14(a):
430+
# check that we properly identify submodule exceptions.
431+
if a < 1:
432+
raise m.ValueError
433+
elif a < 2:
434+
raise m.n.ValueError()
435+
else:
436+
raise a.b.c.ValueError(msg="some msg")
437+
438+
def func15():
439+
try:
440+
x = 1
441+
except other.Exception:
442+
raise
419443
"""
420444

421445

@@ -439,6 +463,9 @@ def testHasRaiseStatements() -> None:
439463
(75, 0, 'func10'): True,
440464
(83, 0, 'func11'): True,
441465
(100, 0, 'func12'): True,
466+
(117, 0, 'func13'): True,
467+
(126, 0, 'func14'): True,
468+
(135, 0, 'func15'): True,
442469
}
443470

444471
assert result == expected
@@ -464,11 +491,18 @@ def testWhichRaiseStatements() -> None:
464491
'RuntimeError',
465492
'TypeError',
466493
],
467-
(54, 0, 'func8'): ['KeyError', 'TypeError'],
494+
(54, 0, 'func8'): ['KeyError', 'TypeError', 'm.ValueError'],
468495
(62, 0, 'func9'): ['AssertionError', 'IndexError'],
469496
(75, 0, 'func10'): ['GError'],
470497
(83, 0, 'func11'): ['ValueError'],
471498
(100, 0, 'func12'): ['Error1', 'Error2', 'Error3'],
499+
(117, 0, 'func13'): ['IOError', 'TypeError', 'ValueError'],
500+
(126, 0, 'func14'): [
501+
'a.b.c.ValueError',
502+
'm.ValueError',
503+
'm.n.ValueError',
504+
],
505+
(135, 0, 'func15'): ['other.Exception'],
472506
}
473507

474508
assert result == expected

0 commit comments

Comments
 (0)