From 6709682cd1c9ec665df86ba179862ee305d5bcfc Mon Sep 17 00:00:00 2001 From: sdiebolt Date: Thu, 24 Aug 2023 11:53:37 +0200 Subject: [PATCH 1/4] BUG: validator now handles decorators --- numpydoc/validate.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/numpydoc/validate.py b/numpydoc/validate.py index 922f817f..7cd37341 100644 --- a/numpydoc/validate.py +++ b/numpydoc/validate.py @@ -295,7 +295,17 @@ def source_file_def_line(self): Number of line where the object is defined in its file. """ try: - return inspect.getsourcelines(self.code_obj)[-1] + sourcelines = inspect.getsourcelines(self.code_obj) + # getsourcelines will return the line of the first decorator found for the + # current function. We have to find the def declaration after that. + def_lines = [ + i + for i, x in enumerate( + [re.match("^ *(def|class)", s) for s in sourcelines[0]] + ) + if x is not None + ] + return sourcelines[-1] + def_lines[0] except (OSError, TypeError): # In some cases the object is something complex like a cython # object that can't be easily introspected. An it's better to From 487fbbd15d9b333dc7e277c83191e2a13212998f Mon Sep 17 00:00:00 2001 From: sdiebolt Date: Tue, 29 Aug 2023 17:03:15 +0200 Subject: [PATCH 2/4] slight regex enhancement to match def/class --- numpydoc/validate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/validate.py b/numpydoc/validate.py index 7cd37341..91178649 100644 --- a/numpydoc/validate.py +++ b/numpydoc/validate.py @@ -301,7 +301,7 @@ def source_file_def_line(self): def_lines = [ i for i, x in enumerate( - [re.match("^ *(def|class)", s) for s in sourcelines[0]] + [re.match("^ *(def|class) ", s) for s in sourcelines[0]] ) if x is not None ] From d89e226d6efd8219d2aed36e66e2475e7844eee5 Mon Sep 17 00:00:00 2001 From: sdiebolt Date: Tue, 29 Aug 2023 17:50:26 +0200 Subject: [PATCH 3/4] TST: test `source_file_def_line` with decorators --- numpydoc/tests/test_validate.py | 55 +++++++++++++++++++++++++++++++++ numpydoc/validate.py | 8 ++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/numpydoc/tests/test_validate.py b/numpydoc/tests/test_validate.py index e09c4414..9e48fadf 100644 --- a/numpydoc/tests/test_validate.py +++ b/numpydoc/tests/test_validate.py @@ -1,5 +1,6 @@ import pytest import warnings +from inspect import getsourcelines from numpydoc import validate import numpydoc.tests @@ -1528,6 +1529,35 @@ def test_bad_docstrings(self, capsys, klass, func, msgs): assert msg in " ".join(err[1] for err in result["errors"]) +def decorator(x): + """Test decorator.""" + return x + + +@decorator +@decorator +class DecoratorClass: + """ + Class and methods with decorators. + + `DecoratorClass` has two decorators, `DecoratorClass.test_no_decorator` has no + decorator and `DecoratorClass.test_three_decorators` has three decorators. + `Validator.source_file_def_line` should return the `def` or `class` line number, not + the line of the first decorator. + """ + + def test_no_decorator(self): + """Test method without decorators.""" + pass + + @decorator + @decorator + @decorator + def test_three_decorators(self): + """Test method with three decorators.""" + pass + + class TestValidatorClass: @pytest.mark.parametrize("invalid_name", ["unknown_mod", "unknown_mod.MyClass"]) def test_raises_for_invalid_module_name(self, invalid_name): @@ -1544,3 +1574,28 @@ def test_raises_for_invalid_attribute_name(self, invalid_name): msg = f"'{obj_name}' has no attribute '{invalid_attr_name}'" with pytest.raises(AttributeError, match=msg): numpydoc.validate.Validator._load_obj(invalid_name) + + @pytest.mark.parametrize( + ["decorated_obj", "def_line"], + [ + [ + "numpydoc.tests.test_validate.DecoratorClass", + getsourcelines(DecoratorClass)[-1] + 2, + ], + [ + "numpydoc.tests.test_validate.DecoratorClass.test_no_decorator", + getsourcelines(DecoratorClass.test_no_decorator)[-1], + ], + [ + "numpydoc.tests.test_validate.DecoratorClass.test_three_decorators", + getsourcelines(DecoratorClass.test_three_decorators)[-1] + 3, + ], + ], + ) + def test_source_file_def_line_with_decorators(self, decorated_obj, def_line): + doc = numpydoc.validate.Validator( + numpydoc.docscrape.get_doc_object( + numpydoc.validate.Validator._load_obj(decorated_obj) + ) + ) + assert doc.source_file_def_line == def_line diff --git a/numpydoc/validate.py b/numpydoc/validate.py index 91178649..2cd11251 100644 --- a/numpydoc/validate.py +++ b/numpydoc/validate.py @@ -298,14 +298,14 @@ def source_file_def_line(self): sourcelines = inspect.getsourcelines(self.code_obj) # getsourcelines will return the line of the first decorator found for the # current function. We have to find the def declaration after that. - def_lines = [ + def_line = next( i for i, x in enumerate( - [re.match("^ *(def|class) ", s) for s in sourcelines[0]] + re.match("^ *(def|class) ", s) for s in sourcelines[0] ) if x is not None - ] - return sourcelines[-1] + def_lines[0] + ) + return sourcelines[-1] + def_line except (OSError, TypeError): # In some cases the object is something complex like a cython # object that can't be easily introspected. An it's better to From d974f2509b32ca607669ef481751f8071785ebf9 Mon Sep 17 00:00:00 2001 From: sdiebolt Date: Wed, 30 Aug 2023 11:26:05 +0200 Subject: [PATCH 4/4] fix tests for python 3.8 --- numpydoc/tests/test_validate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/numpydoc/tests/test_validate.py b/numpydoc/tests/test_validate.py index 9e48fadf..5f76cd9c 100644 --- a/numpydoc/tests/test_validate.py +++ b/numpydoc/tests/test_validate.py @@ -1,4 +1,5 @@ import pytest +import sys import warnings from inspect import getsourcelines @@ -1575,12 +1576,15 @@ def test_raises_for_invalid_attribute_name(self, invalid_name): with pytest.raises(AttributeError, match=msg): numpydoc.validate.Validator._load_obj(invalid_name) + # inspect.getsourcelines does not return class decorators for Python 3.8. This was + # fixed starting with 3.9: https://github.com/python/cpython/issues/60060 @pytest.mark.parametrize( ["decorated_obj", "def_line"], [ [ "numpydoc.tests.test_validate.DecoratorClass", - getsourcelines(DecoratorClass)[-1] + 2, + getsourcelines(DecoratorClass)[-1] + + (2 if sys.version_info.minor > 8 else 0), ], [ "numpydoc.tests.test_validate.DecoratorClass.test_no_decorator",