diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fd39bb..e6e0295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Change Log +## [0.5.15] - 2025-01-10 + +- Changed + + - Changed to using v0.0.10 of docstring_parser_fork, which now throws a + `ParseError` when a non-empty docstring section cannot be parsed (in Numpy + style). This `ParseError` would lead to DOC001. + +- Full diff + - https://github.com/jsh9/pydoclint/compare/0.5.14...0.5.15 + ## [0.5.14] - 2024-12-26 - Changed diff --git a/pydoclint/utils/visitor_helper.py b/pydoclint/utils/visitor_helper.py index ba8f3b8..d2a287c 100644 --- a/pydoclint/utils/visitor_helper.py +++ b/pydoclint/utils/visitor_helper.py @@ -507,13 +507,17 @@ def extractYieldTypeFromGeneratorOrIteratorAnnotation( try: if hasGeneratorAsReturnAnnotation: - if sys.version_info >= (3, 9): + if isinstance( + ast.parse(returnAnnoText).body[0].value.slice, # type:ignore[attr-defined,arg-type] + ast.Constant, + ): + # This means returnAnnoText is something like "Generator[None]" yieldType = unparseName( - ast.parse(returnAnnoText).body[0].value.slice.elts[0] # type:ignore[attr-defined,arg-type] + ast.parse(returnAnnoText).body[0].value.slice # type:ignore[attr-defined,arg-type] ) else: yieldType = unparseName( - ast.parse(returnAnnoText).body[0].value.slice.value.elts[0] + ast.parse(returnAnnoText).body[0].value.slice.elts[0] # type:ignore[attr-defined,arg-type] ) elif hasIteratorOrIterableAsReturnAnnotation: yieldType = unparseName( diff --git a/pydoclint/visitor.py b/pydoclint/visitor.py index d6139e5..035ec05 100644 --- a/pydoclint/visitor.py +++ b/pydoclint/visitor.py @@ -168,12 +168,18 @@ def visit_FunctionDef(self, node: FuncOrAsyncFuncDef) -> None: # noqa: D102 doc: Doc = Doc(docstring=docstring, style=self.style) except Exception as excp: doc = Doc(docstring='', style=self.style) + msgPostfix: str = ( + str(excp).replace('\n', ' ') + + ' (Note: DOC001 could trigger other unrelated' + + ' violations under this function/method too. Please' + + ' fix the docstring formatting first.)' + ) self.violations.append( Violation( code=1, line=node.lineno, msgPrefix=f'Function/method `{node.name}`:', - msgPostfix=str(excp).replace('\n', ' '), + msgPostfix=msgPostfix, ) ) diff --git a/setup.cfg b/setup.cfg index 729471c..c9e114b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pydoclint -version = 0.5.14 +version = 0.5.15 description = A Python docstring linter that checks arguments, returns, yields, and raises sections long_description = file: README.md long_description_content_type = text/markdown @@ -16,7 +16,7 @@ classifiers = packages = find: install_requires = click>=8.1.0 - docstring_parser_fork>=0.0.9 + docstring_parser_fork>=0.0.10 tomli>=2.0.1; python_version<'3.11' python_requires = >=3.9 diff --git a/tests/data/edge_cases/22_PEP696_generator/case.py b/tests/data/edge_cases/22_PEP696_generator/case.py new file mode 100644 index 0000000..88dc963 --- /dev/null +++ b/tests/data/edge_cases/22_PEP696_generator/case.py @@ -0,0 +1,57 @@ +# From: https://github.com/jsh9/pydoclint/issues/198 + +@pytest.fixture +def setup_custom_logger(caplog: pytest.LogCaptureFixture) -> Generator[None]: + """ + Set up a custom logger. + + Parameters + ---------- + caplog : pytest.LogCaptureFixture + Pytest logging capture fixture. + + Yields + ------ + None + Yield to run test before cleanup. + """ + # Set class + logging.setLoggerClass(CustomLogger) + # Set new format + format_copy = caplog.handler.formatter._fmt # type: ignore[union-attr] + caplog.handler.setFormatter(logging.Formatter('%(id_string)s : %(message)s')) + + yield + + caplog.handler.setFormatter(logging.Formatter(format_copy)) + # Reset logger class + logging.setLoggerClass(logging.Logger) + + + +@pytest.fixture +def setup_custom_logger2(caplog: pytest.LogCaptureFixture) -> Generator[None, None, None]: + """ + Set up a custom logger. + + Parameters + ---------- + caplog : pytest.LogCaptureFixture + Pytest logging capture fixture. + + Yields + ------ + None + Yield to run test before cleanup. + """ + # Set class + logging.setLoggerClass(CustomLogger) + # Set new format + format_copy = caplog.handler.formatter._fmt # type: ignore[union-attr] + caplog.handler.setFormatter(logging.Formatter('%(id_string)s : %(message)s')) + + yield + + caplog.handler.setFormatter(logging.Formatter(format_copy)) + # Reset logger class + logging.setLoggerClass(logging.Logger) diff --git a/tests/data/numpy/parsing_errors/cases.py b/tests/data/numpy/parsing_errors/cases.py index 382feaa..36addbd 100644 --- a/tests/data/numpy/parsing_errors/cases.py +++ b/tests/data/numpy/parsing_errors/cases.py @@ -24,3 +24,13 @@ def method1(self, arg3): arg 3 """ pass + + def method2(self, arg4): + """ + Yields + ------ + Something to yield. This is not the correct docstring + format and will lead to DOC001, because the yielded + type is needed. + """ + pass diff --git a/tests/test_main.py b/tests/test_main.py index 7ef0379..cde22a1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -938,7 +938,9 @@ def testParsingErrors_google() -> None: 'DOC001: Class `A`: Potential formatting errors in docstring. Error message: ' "Expected a colon in 'arg1'.", 'DOC001: Function/method `__init__`: Potential formatting errors in ' - "docstring. Error message: Expected a colon in 'arg1'.", + "docstring. Error message: Expected a colon in 'arg1'. (Note: DOC001 could " + 'trigger other unrelated violations under this function/method too. Please ' + 'fix the docstring formatting first.)', ] assert list(map(str, violations)) == expected @@ -959,7 +961,24 @@ def testParsingErrors_numpy() -> None: argTypeHintsInSignature=False, style='numpy', ) - expected = [] # not sure how to craft docstrings with parsing errors yet + expected = [ + 'DOC001: Class `A`: Potential formatting errors in docstring. Error message: ' + "Section 'Parameters' is not empty but nothing was parsed.", + 'DOC001: Function/method `__init__`: Potential formatting errors in ' + "docstring. Error message: Section 'Parameters' is not empty but nothing was " + 'parsed. (Note: DOC001 could trigger other unrelated violations under this ' + 'function/method too. Please fix the docstring formatting first.)', + 'DOC001: Function/method `method2`: Potential formatting errors in docstring. ' + "Error message: Section 'Yields' is not empty but nothing was parsed. (Note: " + 'DOC001 could trigger other unrelated violations under this function/method ' + 'too. Please fix the docstring formatting first.)', + 'DOC101: Method `A.method2`: Docstring contains fewer arguments than in ' + 'function signature.', + 'DOC103: Method `A.method2`: Docstring arguments are different from function ' + 'arguments. (Or could be other formatting issues: ' + 'https://jsh9.github.io/pydoclint/violation_codes.html#notes-on-doc103 ). ' + 'Arguments in the function signature but not in the docstring: [arg4: ].', + ] assert list(map(str, violations)) == expected @@ -1576,6 +1595,11 @@ def testNonAscii() -> None: + ' (, line 2)' ], ), + ( + '22_PEP696_generator/case.py', + {'style': 'numpy'}, + [], + ), ], ) def testEdgeCases(