diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e018d..c5fb61e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/pydoclint/utils/generic.py b/pydoclint/utils/generic.py index b6cc8bd..9f3bde4 100644 --- a/pydoclint/utils/generic.py +++ b/pydoclint/utils/generic.py @@ -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 @@ -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 diff --git a/pydoclint/utils/return_yield_raise.py b/pydoclint/utils/return_yield_raise.py index 1984e61..4a316d9 100644 --- a/pydoclint/utils/return_yield_raise.py +++ b/pydoclint/utils/return_yield_raise.py @@ -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] @@ -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" @@ -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( diff --git a/setup.cfg b/setup.cfg index 030d304..9ec6c76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 diff --git a/tests/data/google/raises/cases.py b/tests/data/google/raises/cases.py index acc898f..e99a2c4 100644 --- a/tests/data/google/raises/cases.py +++ b/tests/data/google/raises/cases.py @@ -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 diff --git a/tests/data/numpy/raises/cases.py b/tests/data/numpy/raises/cases.py index 2e7db3f..0e51c66 100644 --- a/tests/data/numpy/raises/cases.py +++ b/tests/data/numpy/raises/cases.py @@ -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 diff --git a/tests/data/sphinx/raises/cases.py b/tests/data/sphinx/raises/cases.py index d4368fb..46adc8e 100644 --- a/tests/data/sphinx/raises/cases.py +++ b/tests/data/sphinx/raises/cases.py @@ -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 diff --git a/tests/test_main.py b/tests/test_main.py index 7aae66a..71f2485 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -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 diff --git a/tests/utils/test_returns_yields_raise.py b/tests/utils/test_returns_yields_raise.py index 15a151f..7b63310 100644 --- a/tests/utils/test_returns_yields_raise.py +++ b/tests/utils/test_returns_yields_raise.py @@ -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 @@ -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 """ @@ -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 @@ -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