Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: DOC503 catch namespaced exceptions #168

Merged
merged 5 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Change Log

## [0.5.8] - 2024-09-23

- Fixed

- Fixed the logic of handling exceptions namespaces (`a.b.c.MyException`)

- Full diff
- https://github.com/jsh9/pydoclint/compare/0.5.7...0.5.8

## [0.5.7] - 2024-09-02

- Added
Expand All @@ -8,8 +17,12 @@
function body match those in the "Raises" section of the docstring

- Changed

- Switched from tab to 4 spaces in baseline

- Full diff
- https://github.com/jsh9/pydoclint/compare/0.5.6...0.5.7

## [0.5.6] - 2024-07-17

- Fixed
Expand Down
10 changes: 9 additions & 1 deletion pydoclint/utils/generic.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ast
import copy
import re
from typing import List, Match, Optional, Tuple
from typing import List, Match, Optional, Tuple, Union

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

return True


def getFullAttributeName(node: Union[ast.Attribute, ast.Name]) -> str:
"""Get the full name of a symbol like a.b.c.foo"""
if isinstance(node, ast.Name):
return node.id

return getFullAttributeName(node.value) + '.' + node.attr
27 changes: 22 additions & 5 deletions pydoclint/utils/return_yield_raise.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pydoclint.utils import walk
from pydoclint.utils.annotation import unparseAnnotation
from pydoclint.utils.astTypes import BlockType, FuncOrAsyncFuncDef
from pydoclint.utils.generic import stringStartsWith
from pydoclint.utils.generic import getFullAttributeName, stringStartsWith

ReturnType = Type[ast.Return]
ExprType = Type[ast.Expr]
Expand Down Expand Up @@ -132,7 +132,17 @@ def _getRaisedExceptions(
):
for subnode, _ in walk.walk_dfs(child):
if isinstance(subnode, ast.Name):
yield subnode.id
if isinstance(child.exc, ast.Attribute):
# case: looks like m.n.exception
yield getFullAttributeName(child.exc)
elif isinstance(child.exc, ast.Call) and isinstance(
child.exc.func, ast.Attribute
):
# case: looks like m.n.exception()
yield getFullAttributeName(child.exc.func)
else:
yield subnode.id

break
else:
# if "raise" statement was alone, it must be inside an "except"
Expand All @@ -148,10 +158,17 @@ def _extractExceptionsFromExcept(
if isinstance(node.type, ast.Name):
yield node.type.id

if isinstance(node.type, ast.Attribute):
# case: looks like m.n.exception
yield getFullAttributeName(node.type)

if isinstance(node.type, ast.Tuple):
for child, _ in walk.walk(node.type):
if isinstance(child, ast.Name):
yield child.id
for elt in node.type.elts:
if isinstance(elt, ast.Attribute):
# case: looks like m.n.exception
yield getFullAttributeName(elt)
elif isinstance(elt, ast.Name):
yield elt.id


def _hasExpectedStatements(
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = pydoclint
version = 0.5.7
version = 0.5.8
description = A Python docstring linter that checks arguments, returns, yields, and raises sections
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down
23 changes: 23 additions & 0 deletions tests/data/google/raises/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,26 @@ def func13(self) -> None:
ValueError: typo!
"""
raise ValueError

def func14(self) -> None:
"""
Should fail, expects `exceptions.CustomError`.

Raises:
CustomError: every time.
"""
exceptions = object()
exceptions.CustomError = CustomError
raise exceptions.CustomError()

def func15(self) -> None:
"""
Should fail, expects `exceptions.m.CustomError`.

Raises:
CustomError: every time.
"""
exceptions = object()
exceptions.m = object()
exceptions.m.CustomError = CustomError
raise exceptions.m.CustomError
27 changes: 27 additions & 0 deletions tests/data/numpy/raises/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,30 @@ def func13(self) -> None:
typo!
"""
raise ValueError

def func14(self) -> None:
"""
Should fail, expects `exceptions.CustomError`.

Raises
------
CustomError
every time.
"""
exceptions = object()
exceptions.CustomError = CustomError
raise exceptions.CustomError()

def func15(self) -> None:
"""
Should fail, expects `exceptions.m.CustomError`.

Raises
------
CustomError
every time.
"""
exceptions = object()
exceptions.m = object()
exceptions.m.CustomError = CustomError
raise exceptions.m.CustomError
21 changes: 21 additions & 0 deletions tests/data/sphinx/raises/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,24 @@ def func13(self) -> None:
:raises ValueError: typo!
"""
raise ValueError

def func14(self) -> None:
"""
Should fail, expects `exceptions.CustomError`.

:raises CustomError: every time.
"""
exceptions = object()
exceptions.CustomError = CustomError
raise exceptions.CustomError()

def func15(self) -> None:
"""
Should fail, expects `exceptions.m.CustomError`.

:raises CustomError: every time.
"""
exceptions = object()
exceptions.m = object()
exceptions.m.CustomError = CustomError
raise exceptions.m.CustomError
8 changes: 8 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,14 @@ def testRaises(style: str, skipRaisesCheck: bool) -> None:
'docstring do not match those in the function body Raises values in the '
"docstring: ['ValueError', 'ValueError']. Raised exceptions in the body: "
"['ValueError'].",
'DOC503: Method `B.func14` exceptions in the "Raises" section in the '
'docstring do not match those in the function body Raises values in the '
"docstring: ['CustomError']. Raised exceptions in the body: "
"['exceptions.CustomError'].",
'DOC503: Method `B.func15` exceptions in the "Raises" section in the '
'docstring do not match those in the function body Raises values in the '
"docstring: ['CustomError']. Raised exceptions in the body: "
"['exceptions.m.CustomError'].",
]
expected1 = []
expected = expected1 if skipRaisesCheck else expected0
Expand Down
38 changes: 36 additions & 2 deletions tests/utils/test_returns_yields_raise.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ def func7(arg0):
def func8(d):
try:
d[0][0]
except (KeyError, TypeError):
except (KeyError, TypeError, m.ValueError):
raise
finally:
pass
Expand Down Expand Up @@ -416,6 +416,30 @@ def func12(a):

if a < 3:
raise Error3

def func13(a):
# ensure we get `Exception`, `Exception()`, and `Exception('something')`
if a < 1:
raise ValueError
elif a < 2:
raise TypeError()
else:
raise IOError('IO Error!')

def func14(a):
# check that we properly identify submodule exceptions.
if a < 1:
raise m.ValueError
elif a < 2:
raise m.n.ValueError()
else:
raise a.b.c.ValueError(msg="some msg")

def func15():
try:
x = 1
except other.Exception:
raise
"""


Expand All @@ -439,6 +463,9 @@ def testHasRaiseStatements() -> None:
(75, 0, 'func10'): True,
(83, 0, 'func11'): True,
(100, 0, 'func12'): True,
(117, 0, 'func13'): True,
(126, 0, 'func14'): True,
(135, 0, 'func15'): True,
}

assert result == expected
Expand All @@ -464,11 +491,18 @@ def testWhichRaiseStatements() -> None:
'RuntimeError',
'TypeError',
],
(54, 0, 'func8'): ['KeyError', 'TypeError'],
(54, 0, 'func8'): ['KeyError', 'TypeError', 'm.ValueError'],
(62, 0, 'func9'): ['AssertionError', 'IndexError'],
(75, 0, 'func10'): ['GError'],
(83, 0, 'func11'): ['ValueError'],
(100, 0, 'func12'): ['Error1', 'Error2', 'Error3'],
(117, 0, 'func13'): ['IOError', 'TypeError', 'ValueError'],
(126, 0, 'func14'): [
'a.b.c.ValueError',
'm.ValueError',
'm.n.ValueError',
],
(135, 0, 'func15'): ['other.Exception'],
}

assert result == expected
Loading