Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitb88f5ee

Browse files
authored
Constructor checking for AST validator (#622)
* fix(validate.py): Considers subclass nesting when checking GL08 constructorIntroduced GL08 checking for constructors (i.e. __init__) but didn't traverse the parent class heireacy when importing the __init__ parent from a module.* test(validate.py): Added a test to check nested class docstring when checking for initialisation docstringTest written due to bug missed in#592.* fix(validate.py): Allows the validator to check AST constructor docstrings compliance with parent classThe abstract syntax tree doesn't provide any link to a parent class at node level, so special traversal is required to check constructor parent implementation of docstrings.* test(test_validate_hook.py,-example_module.py): Wrote new example_module tests for AST (AbstractSyntaxTree) discovery of constructor method parent class.* ci(test.yml): Added --pre option to prerelease job to ensure pre-release installation, and changed pytest invocation to use `numpydoc` instead of `.`, for consistency with `test` jobIn response to discussion on#591* refactor(tests): Remove `__init__.py` module status of `tests\hooks\` to match `tests\` directory* ci(test.yml): Added explicit call to hook tests to see if included in pytest execution* test(tests\hooks\test_validate_hook.py): Changed constructor validation to use AST ancestry, fixed hook tests and added Windows support for test_utils* ci(test.yml): Added file existance check for hook testsWorkflows do not seem to be running hook pytests.* ci(test.yml): Correct the workflow task name/version* ci(test.yml): Added explicit pytest call to the hooks directory* ci(test.yml): Removed file existance test, after explicit call to hooks verifies execution* fix(validate.py): switched conditional GL08 check order to avoid property class failure for Validator.nameSolves Issue#638, though lacks property support for property class attributes.* test(test_validate.py): add coverage for existing / expected functionality of property and dataclass objects* test(test_validate_hook.py): modify test strings and remove unused os.sep replacementUpdates to make this PR compatible with the recent merge from#653
1 parenteb831f4 commitb88f5ee

File tree

6 files changed

+247
-13
lines changed

6 files changed

+247
-13
lines changed

‎.github/workflows/test.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
4949
-name:Run test suite
5050
run:|
51-
pytest -v --pyargsnumpydoc
51+
pytest -v --pyargs.
5252
5353
-name:Test coverage
5454
run:|

‎numpydoc/tests/hooks/__init__.py‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎numpydoc/tests/hooks/example_module.py‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,35 @@ def create(self):
2828

2929

3030
classNewClass:
31-
pass
31+
classGoodConstructor:
32+
"""
33+
A nested class to test constructors via AST hook.
34+
35+
Implements constructor via class docstring.
36+
37+
Parameters
38+
----------
39+
name : str
40+
The name of the new class.
41+
"""
42+
43+
def__init__(self,name):
44+
self.name=name
45+
46+
classBadConstructor:
47+
"""
48+
A nested class to test constructors via AST hook.
49+
50+
Implements a bad constructor docstring despite having a good class docstring.
51+
52+
Parameters
53+
----------
54+
name : str
55+
The name of the new class.
56+
"""
57+
58+
def__init__(self,name):
59+
"""
60+
A failing constructor implementation without parameters.
61+
"""
62+
self.name=name

‎numpydoc/tests/hooks/test_validate_hook.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ def test_validate_hook(example_module, config, capsys):
6262
{example_module!s}:26: EX01 No examples section found
6363
6464
{example_module!s}:30: GL08 The object does not have a docstring
65+
66+
{example_module!s}:31: SA01 See Also section not found
67+
68+
{example_module!s}:31: EX01 No examples section found
69+
70+
{example_module!s}:46: SA01 See Also section not found
71+
72+
{example_module!s}:46: EX01 No examples section found
73+
74+
{example_module!s}:58: ES01 No extended summary found
75+
76+
{example_module!s}:58: PR01 Parameters {{'name'}} not documented
77+
78+
{example_module!s}:58: SA01 See Also section not found
79+
80+
{example_module!s}:58: EX01 No examples section found
6581
"""
6682
)
6783

@@ -89,6 +105,8 @@ def test_validate_hook_with_ignore(example_module, capsys):
89105
{example_module!s}:26: SS05 Summary must start with infinitive verb, not third person (e.g. use "Generate" instead of "Generates")
90106
91107
{example_module!s}:30: GL08 The object does not have a docstring
108+
109+
{example_module!s}:58: PR01 Parameters {{'name'}} not documented
92110
"""
93111
)
94112

‎numpydoc/tests/test_validate.py‎

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
importwarnings
22
fromcontextlibimportnullcontext
3+
fromdataclassesimportdataclass
34
fromfunctoolsimportcached_property,partial,wraps
45
frominspectimportgetsourcefile,getsourcelines
56

@@ -1305,6 +1306,109 @@ def __init__(self, param1: int):
13051306
pass
13061307

13071308

1309+
classConstructorDocumentedinEmbeddedClass:# ignore Gl08, ES01
1310+
"""
1311+
Class to test the initialisation behaviour of a embedded class.
1312+
"""
1313+
1314+
classEmbeddedClass1:# ignore GL08, ES01
1315+
"""
1316+
An additional level for embedded class documentation checking.
1317+
"""
1318+
1319+
classEmbeddedClass2:
1320+
"""
1321+
This is an embedded class.
1322+
1323+
Extended summary.
1324+
1325+
Parameters
1326+
----------
1327+
param1 : int
1328+
Description of param1.
1329+
1330+
See Also
1331+
--------
1332+
otherclass : A class that does something else.
1333+
1334+
Examples
1335+
--------
1336+
This is an example of how to use EmbeddedClass.
1337+
"""
1338+
1339+
def__init__(self,param1:int)->None:
1340+
pass
1341+
1342+
1343+
classIncompleteConstructorDocumentedinEmbeddedClass:
1344+
"""
1345+
Class to test the initialisation behaviour of a embedded class.
1346+
"""
1347+
1348+
classEmbeddedClass1:
1349+
"""
1350+
An additional level for embedded class documentation checking.
1351+
"""
1352+
1353+
classEmbeddedClass2:
1354+
"""
1355+
This is an embedded class.
1356+
1357+
Extended summary.
1358+
1359+
See Also
1360+
--------
1361+
otherclass : A class that does something else.
1362+
1363+
Examples
1364+
--------
1365+
This is an example of how to use EmbeddedClass.
1366+
"""
1367+
1368+
def__init__(self,param1:int)->None:
1369+
pass
1370+
1371+
1372+
@dataclass
1373+
classDataclassWithDocstring:
1374+
"""
1375+
A class decorated by `dataclass`.
1376+
1377+
To check the functionality of `dataclass` objects do not break the Validator.
1378+
As param1 is not documented this class should also raise PR01.
1379+
"""
1380+
1381+
param1:int
1382+
1383+
1384+
classClassWithPropertyObject:
1385+
"""
1386+
A class with a `property`.
1387+
1388+
To check the functionality of `property` objects do not break the Validator.
1389+
1390+
Parameters
1391+
----------
1392+
param1 : int
1393+
Description of param1.
1394+
"""
1395+
1396+
def__init__(self,param1:int)->None:
1397+
self._param1=param1
1398+
1399+
@property
1400+
defparam1(self)->int:
1401+
"""
1402+
Get the value of param1.
1403+
1404+
Returns
1405+
-------
1406+
int
1407+
The value of param1.
1408+
"""
1409+
returnself._param1
1410+
1411+
13081412
classTestValidator:
13091413
def_import_path(self,klass=None,func=None):
13101414
"""
@@ -1657,6 +1761,18 @@ def test_bad_docstrings(self, capsys, klass, func, msgs):
16571761
tuple(),
16581762
("PR01"),# Parameter not documented in class constructor
16591763
),
1764+
(
1765+
"ConstructorDocumentedinEmbeddedClass.EmbeddedClass1.EmbeddedClass2",
1766+
tuple(),
1767+
("GL08",),
1768+
tuple(),
1769+
),
1770+
(
1771+
"IncompleteConstructorDocumentedinEmbeddedClass.EmbeddedClass1.EmbeddedClass2",
1772+
("GL08",),
1773+
tuple(),
1774+
("PR01",),
1775+
),
16601776
],
16611777
)
16621778
deftest_constructor_docstrings(
@@ -1674,6 +1790,39 @@ def test_constructor_docstrings(
16741790
forcodeinexc_init_codes:
16751791
assertcodenotin" ".join(err[0]forerrinresult["errors"])
16761792

1793+
ifklass=="ConstructorDocumentedinEmbeddedClass":
1794+
raiseNotImplementedError(
1795+
"Test for embedded class constructor docstring not implemented yet."
1796+
)
1797+
1798+
deftest_dataclass_object(self):
1799+
# Test validator methods complete execution on dataclass objects and methods
1800+
# Test case ought to be removed if dataclass objects properly supported.
1801+
result=validate_one(self._import_path(klass="DataclassWithDocstring"))
1802+
# Check codes match as expected for dataclass objects.
1803+
errs= ["ES01","SA01","EX01","PR01"]
1804+
forerrorinresult["errors"]:
1805+
asserterror[0]inerrs
1806+
errs.remove(error[0])
1807+
1808+
# Test initialisation method (usually undocumented in dataclass) raises any errors.
1809+
init_fn=self._import_path(klass="DataclassWithDocstring",func="__init__")
1810+
result=validate_one(init_fn)
1811+
# Check that __init__ raises GL08 when the class docstring doesn't document params.
1812+
assertresult["errors"][0][0]=="GL08"
1813+
1814+
deftest_property_object(self):
1815+
# Test validator methods complete execution on class property objects
1816+
# Test case ought to be removed if property objects properly supported.
1817+
result=validate_one(
1818+
self._import_path(klass="ClassWithPropertyObject",func="param1")
1819+
)
1820+
# Check codes match as expected for property objects.
1821+
errs= ["ES01","SA01","EX01"]
1822+
forerrorinresult["errors"]:
1823+
asserterror[0]inerrs
1824+
errs.remove(error[0])
1825+
16771826

16781827
defdecorator(x):
16791828
"""Test decorator."""

‎numpydoc/validate.py‎

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,14 @@ def _check_desc(desc, code_no_desc, code_no_upper, code_no_period, **kwargs):
572572
returnerrs
573573

574574

575+
def_find_class_node(module_node:ast.AST,cls_name)->ast.ClassDef:
576+
# Find the class node within a module, when checking constructor docstrings.
577+
fornodeinast.walk(module_node):
578+
ifisinstance(node,ast.ClassDef)andnode.name==cls_name:
579+
returnnode
580+
raiseValueError(f"Could not find class node{cls_name}")
581+
582+
575583
defvalidate(obj_name,validator_cls=None,**validator_kwargs):
576584
"""
577585
Validate the docstring.
@@ -639,20 +647,49 @@ def validate(obj_name, validator_cls=None, **validator_kwargs):
639647
report_GL08:bool=True
640648
# Check if the object is a class and has a docstring in the constructor
641649
# Also check if code_obj is defined, as undefined for the AstValidator in validate_docstrings.py.
642-
if (
643-
doc.name.endswith(".__init__")
644-
anddoc.is_function_or_method
645-
andhasattr(doc,"code_obj")
646-
):
647-
cls_name=doc.code_obj.__qualname__.split(".")[0]
648-
cls=Validator._load_obj(f"{doc.code_obj.__module__}.{cls_name}")
649-
# cls = Validator._load_obj(f"{doc.name[:-9]}.{cls_name}") ## Alternative
650-
cls_doc=Validator(get_doc_object(cls))
650+
ifdoc.is_function_or_methodanddoc.name.endswith(".__init__"):
651+
# Import here at runtime to avoid circular import as
652+
# AstValidator is a subclass of Validator class without `doc_obj` attribute.
653+
fromnumpydoc.hooks.validate_docstringsimport (
654+
AstValidator,# Support abstract syntax tree hook.
655+
)
656+
657+
ifhasattr(doc,"code_obj"):# All Validator objects have this attr.
658+
cls_name=".".join(
659+
doc.code_obj.__qualname__.split(".")[:-1]
660+
)# Collect all class depths before the constructor.
661+
cls=Validator._load_obj(f"{doc.code_obj.__module__}.{cls_name}")
662+
# cls = Validator._load_obj(f"{doc.name[:-9]}.{cls_name}") ## Alternative
663+
cls_doc=Validator(get_doc_object(cls))
664+
elifisinstance(doc,AstValidator):# Supports class traversal for ASTs.
665+
ancestry=doc.ancestry
666+
iflen(ancestry)>2:# e.g. module.class.__init__
667+
parent=doc.ancestry[-1]# Get the parent
668+
cls_name=".".join(
669+
[
670+
getattr(node,"name",node.__module__)
671+
fornodeindoc.ancestry
672+
]
673+
)
674+
cls_doc=AstValidator(
675+
ast_node=parent,
676+
filename=doc.source_file_name,
677+
obj_name=cls_name,
678+
ancestry=doc.ancestry[:-1],
679+
)
680+
else:
681+
# Ignore edge case: __init__ functions that don't belong to a class.
682+
cls_doc=None
683+
else:
684+
raiseTypeError(
685+
f"Cannot load{doc.name} as a usable Validator object (Validator does not have `doc_obj` attr or type `AstValidator`)."
686+
)
651687

652688
# Parameter_mismatches, PR01, PR02, PR03 are checked for the class docstring.
653689
# If cls_doc has PR01, PR02, PR03 errors, i.e. invalid class docstring,
654690
# then we also report missing constructor docstring, GL08.
655-
report_GL08=len(cls_doc.parameter_mismatches)>0
691+
ifcls_doc:
692+
report_GL08=len(cls_doc.parameter_mismatches)>0
656693

657694
# Check if GL08 is to be ignored:
658695
if"GL08"inignore_validation_comments:

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp