Skip to content

Commit df55b8a

Browse files
authored
BUG: validator now handles decorators (#496)
* BUG: validator now handles decorators * slight regex enhancement to match def/class * TST: test `source_file_def_line` with decorators * fix tests for python 3.8
1 parent 307992e commit df55b8a

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed

numpydoc/tests/test_validate.py

+59
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import pytest
2+
import sys
23
import warnings
4+
from inspect import getsourcelines
35

46
from numpydoc import validate
57
import numpydoc.tests
@@ -1528,6 +1530,35 @@ def test_bad_docstrings(self, capsys, klass, func, msgs):
15281530
assert msg in " ".join(err[1] for err in result["errors"])
15291531

15301532

1533+
def decorator(x):
1534+
"""Test decorator."""
1535+
return x
1536+
1537+
1538+
@decorator
1539+
@decorator
1540+
class DecoratorClass:
1541+
"""
1542+
Class and methods with decorators.
1543+
1544+
`DecoratorClass` has two decorators, `DecoratorClass.test_no_decorator` has no
1545+
decorator and `DecoratorClass.test_three_decorators` has three decorators.
1546+
`Validator.source_file_def_line` should return the `def` or `class` line number, not
1547+
the line of the first decorator.
1548+
"""
1549+
1550+
def test_no_decorator(self):
1551+
"""Test method without decorators."""
1552+
pass
1553+
1554+
@decorator
1555+
@decorator
1556+
@decorator
1557+
def test_three_decorators(self):
1558+
"""Test method with three decorators."""
1559+
pass
1560+
1561+
15311562
class TestValidatorClass:
15321563
@pytest.mark.parametrize("invalid_name", ["unknown_mod", "unknown_mod.MyClass"])
15331564
def test_raises_for_invalid_module_name(self, invalid_name):
@@ -1544,3 +1575,31 @@ def test_raises_for_invalid_attribute_name(self, invalid_name):
15441575
msg = f"'{obj_name}' has no attribute '{invalid_attr_name}'"
15451576
with pytest.raises(AttributeError, match=msg):
15461577
numpydoc.validate.Validator._load_obj(invalid_name)
1578+
1579+
# inspect.getsourcelines does not return class decorators for Python 3.8. This was
1580+
# fixed starting with 3.9: https://github.com/python/cpython/issues/60060
1581+
@pytest.mark.parametrize(
1582+
["decorated_obj", "def_line"],
1583+
[
1584+
[
1585+
"numpydoc.tests.test_validate.DecoratorClass",
1586+
getsourcelines(DecoratorClass)[-1]
1587+
+ (2 if sys.version_info.minor > 8 else 0),
1588+
],
1589+
[
1590+
"numpydoc.tests.test_validate.DecoratorClass.test_no_decorator",
1591+
getsourcelines(DecoratorClass.test_no_decorator)[-1],
1592+
],
1593+
[
1594+
"numpydoc.tests.test_validate.DecoratorClass.test_three_decorators",
1595+
getsourcelines(DecoratorClass.test_three_decorators)[-1] + 3,
1596+
],
1597+
],
1598+
)
1599+
def test_source_file_def_line_with_decorators(self, decorated_obj, def_line):
1600+
doc = numpydoc.validate.Validator(
1601+
numpydoc.docscrape.get_doc_object(
1602+
numpydoc.validate.Validator._load_obj(decorated_obj)
1603+
)
1604+
)
1605+
assert doc.source_file_def_line == def_line

numpydoc/validate.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,17 @@ def source_file_def_line(self):
295295
Number of line where the object is defined in its file.
296296
"""
297297
try:
298-
return inspect.getsourcelines(self.code_obj)[-1]
298+
sourcelines = inspect.getsourcelines(self.code_obj)
299+
# getsourcelines will return the line of the first decorator found for the
300+
# current function. We have to find the def declaration after that.
301+
def_line = next(
302+
i
303+
for i, x in enumerate(
304+
re.match("^ *(def|class) ", s) for s in sourcelines[0]
305+
)
306+
if x is not None
307+
)
308+
return sourcelines[-1] + def_line
299309
except (OSError, TypeError):
300310
# In some cases the object is something complex like a cython
301311
# object that can't be easily introspected. An it's better to

0 commit comments

Comments
 (0)