Skip to content

Constructor checking for AST validator #622

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d8f05e6
fix(validate.py): Considers subclass nesting when checking GL08 const…
mattgebert Apr 5, 2025
c7da072
test(validate.py): Added a test to check nested class docstring when …
mattgebert Apr 6, 2025
2703b22
Merge branch 'numpy:main' into ConstructorChecking_ASTValidator
mattgebert May 11, 2025
92d8305
fix(validate.py): Allows the validator to check AST constructor docst…
mattgebert May 11, 2025
4b09325
Merge branch 'ConstructorChecking_ASTValidator' of https://github.com…
mattgebert May 11, 2025
9f38b98
test(test_validate_hook.py,-example_module.py): Wrote new example_mod…
mattgebert May 11, 2025
af861c3
ci(test.yml): Added --pre option to prerelease job to ensure pre-rele…
mattgebert May 11, 2025
c9d2384
refactor(tests): Remove `__init__.py` module status of `tests\hooks\`…
mattgebert May 11, 2025
b62c21f
ci(test.yml): Added explicit call to hook tests to see if included in…
mattgebert May 11, 2025
af84d77
merge: Merge branch 'main' into ConstructorChecking_ASTValidator, ens…
mattgebert Jun 23, 2025
becbaeb
test(tests\hooks\test_validate_hook.py): Changed constructor validati…
mattgebert Jun 23, 2025
39544d2
ci(test.yml): Added file existance check for hook tests
mattgebert Jun 23, 2025
c14b2e8
ci(test.yml): Correct the workflow task name/version
mattgebert Jun 23, 2025
48f8974
ci(test.yml): Added explicit pytest call to the hooks directory
mattgebert Jun 23, 2025
c2d16fa
ci(test.yml): Removed file existance test, after explicit call to hoo…
mattgebert Jun 23, 2025
405ef2d
Merge branch 'numpy:main' into ConstructorChecking_ASTValidator
mattgebert Jun 24, 2025
9114b37
Merge branch 'numpy:main' into ConstructorChecking_ASTValidator
mattgebert Jun 26, 2025
752ab57
fix(validate.py): switched conditional GL08 check order to avoid prop…
mattgebert Jul 19, 2025
0095ca0
Merge branch 'ConstructorChecking_ASTValidator' of https://github.com…
mattgebert Jul 19, 2025
bc88622
test(test_validate.py): add coverage for existing / expected function…
mattgebert Jul 22, 2025
9f70d0f
Merge branch 'main' into ConstructorChecking_ASTValidator
mattgebert Jul 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
- name: Run test suite
run: |
pytest -v --pyargs numpydoc
pytest -v --pyargs numpydoc/tests/hooks

- name: Test coverage
run: |
Expand Down Expand Up @@ -95,12 +96,13 @@ jobs:

- name: Install
run: |
python -m pip install . --group test --group doc
python -m pip install . --pre --group test --group doc
pip list

- name: Run test suite
run: |
pytest -v --pyargs .
pytest -v --pyargs numpydoc
pytest -v --pyargs numpydoc/tests/hooks

- name: Test coverage
run: |
Expand Down
1 change: 0 additions & 1 deletion numpydoc/tests/hooks/__init__.py

This file was deleted.

33 changes: 32 additions & 1 deletion numpydoc/tests/hooks/example_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,35 @@ def create(self):


class NewClass:
pass
class GoodConstructor:
"""
A nested class to test constructors via AST hook.

Implements constructor via class docstring.

Parameters
----------
name : str
The name of the new class.
"""

def __init__(self, name):
self.name = name

class BadConstructor:
"""
A nested class to test constructors via AST hook.

Implements a bad constructor docstring despite having a good class docstring.

Parameters
----------
name : str
The name of the new class.
"""

def __init__(self, name):
"""
A failing constructor implementation without parameters.
"""
self.name = name
4 changes: 3 additions & 1 deletion numpydoc/tests/hooks/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ def test_find_project_root(tmp_path, request, reason_file, files, expected_reaso
(tmp_path / reason_file).touch()

if files:
expected_dir = Path("/") if expected_reason == "file system root" else tmp_path
expected_dir = (
Path(tmp_path.anchor) if expected_reason == "file system root" else tmp_path
)
for file in files:
(tmp_path / file).touch()
else:
Expand Down
31 changes: 25 additions & 6 deletions numpydoc/tests/hooks/test_validate_hook.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test the numpydoc validate pre-commit hook."""

import inspect
import os
from pathlib import Path

import pytest
Expand Down Expand Up @@ -61,8 +62,24 @@ def test_validate_hook(example_module, config, capsys):
numpydoc/tests/hooks/example_module.py:26: EX01 No examples section found

numpydoc/tests/hooks/example_module.py:30: GL08 The object does not have a docstring

numpydoc/tests/hooks/example_module.py:31: SA01 See Also section not found

numpydoc/tests/hooks/example_module.py:31: EX01 No examples section found

numpydoc/tests/hooks/example_module.py:46: SA01 See Also section not found

numpydoc/tests/hooks/example_module.py:46: EX01 No examples section found

numpydoc/tests/hooks/example_module.py:58: ES01 No extended summary found

numpydoc/tests/hooks/example_module.py:58: PR01 Parameters {'name'} not documented

numpydoc/tests/hooks/example_module.py:58: SA01 See Also section not found

numpydoc/tests/hooks/example_module.py:58: EX01 No examples section found
"""
)
).replace("/", os.sep)

return_code = run_hook([example_module], config=config)
assert return_code == 1
Expand All @@ -88,8 +105,10 @@ def test_validate_hook_with_ignore(example_module, capsys):
numpydoc/tests/hooks/example_module.py:26: SS05 Summary must start with infinitive verb, not third person (e.g. use "Generate" instead of "Generates")

numpydoc/tests/hooks/example_module.py:30: GL08 The object does not have a docstring

numpydoc/tests/hooks/example_module.py:58: PR01 Parameters {'name'} not documented
"""
)
).replace("/", os.sep)

return_code = run_hook([example_module], ignore=["ES01", "SA01", "EX01"])

Expand Down Expand Up @@ -132,7 +151,7 @@ def test_validate_hook_with_toml_config(example_module, tmp_path, capsys):

numpydoc/tests/hooks/example_module.py:30: GL08 The object does not have a docstring
"""
)
).replace("/", os.sep)

return_code = run_hook([example_module], config=tmp_path)
assert return_code == 1
Expand Down Expand Up @@ -167,7 +186,7 @@ def test_validate_hook_with_setup_cfg(example_module, tmp_path, capsys):

numpydoc/tests/hooks/example_module.py:30: GL08 The object does not have a docstring
"""
)
).replace("/", os.sep)

return_code = run_hook([example_module], config=tmp_path)
assert return_code == 1
Expand Down Expand Up @@ -208,7 +227,7 @@ def test_validate_hook_exclude_option_pyproject(example_module, tmp_path, capsys

numpydoc/tests/hooks/example_module.py:30: GL08 The object does not have a docstring
"""
)
).replace("/", os.sep)

return_code = run_hook([example_module], config=tmp_path)
assert return_code == 1
Expand Down Expand Up @@ -241,7 +260,7 @@ def test_validate_hook_exclude_option_setup_cfg(example_module, tmp_path, capsys

numpydoc/tests/hooks/example_module.py:17: PR07 Parameter "*args" has no description
"""
)
).replace("/", os.sep)

return_code = run_hook([example_module], config=tmp_path)
assert return_code == 1
Expand Down
149 changes: 149 additions & 0 deletions numpydoc/tests/test_validate.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import warnings
from contextlib import nullcontext
from dataclasses import dataclass
from functools import cached_property, partial, wraps
from inspect import getsourcefile, getsourcelines

Expand Down Expand Up @@ -1305,6 +1306,109 @@ def __init__(self, param1: int):
pass


class ConstructorDocumentedinEmbeddedClass: # ignore Gl08, ES01
"""
Class to test the initialisation behaviour of a embedded class.
"""

class EmbeddedClass1: # ignore GL08, ES01
"""
An additional level for embedded class documentation checking.
"""

class EmbeddedClass2:
"""
This is an embedded class.

Extended summary.

Parameters
----------
param1 : int
Description of param1.

See Also
--------
otherclass : A class that does something else.

Examples
--------
This is an example of how to use EmbeddedClass.
"""

def __init__(self, param1: int) -> None:
pass


class IncompleteConstructorDocumentedinEmbeddedClass:
"""
Class to test the initialisation behaviour of a embedded class.
"""

class EmbeddedClass1:
"""
An additional level for embedded class documentation checking.
"""

class EmbeddedClass2:
"""
This is an embedded class.

Extended summary.

See Also
--------
otherclass : A class that does something else.

Examples
--------
This is an example of how to use EmbeddedClass.
"""

def __init__(self, param1: int) -> None:
pass


@dataclass
class DataclassWithDocstring:
"""
A class decorated by `dataclass`.

To check the functionality of `dataclass` objects do not break the Validator.
As param1 is not documented this class should also raise PR01.
"""

param1: int


class ClassWithPropertyObject:
"""
A class with a `property`.

To check the functionality of `property` objects do not break the Validator.

Parameters
----------
param1 : int
Description of param1.
"""

def __init__(self, param1: int) -> None:
self._param1 = param1

@property
def param1(self) -> int:
"""
Get the value of param1.

Returns
-------
int
The value of param1.
"""
return self._param1


class TestValidator:
def _import_path(self, klass=None, func=None):
"""
Expand Down Expand Up @@ -1660,6 +1764,18 @@ def test_bad_docstrings(self, capsys, klass, func, msgs):
tuple(),
("PR01"), # Parameter not documented in class constructor
),
(
"ConstructorDocumentedinEmbeddedClass.EmbeddedClass1.EmbeddedClass2",
tuple(),
("GL08",),
tuple(),
),
(
"IncompleteConstructorDocumentedinEmbeddedClass.EmbeddedClass1.EmbeddedClass2",
("GL08",),
tuple(),
("PR01",),
),
],
)
def test_constructor_docstrings(
Expand All @@ -1677,6 +1793,39 @@ def test_constructor_docstrings(
for code in exc_init_codes:
assert code not in " ".join(err[0] for err in result["errors"])

if klass == "ConstructorDocumentedinEmbeddedClass":
raise NotImplementedError(
"Test for embedded class constructor docstring not implemented yet."
)

def test_dataclass_object(self):
# Test validator methods complete execution on dataclass objects and methods
# Test case ought to be removed if dataclass objects properly supported.
result = validate_one(self._import_path(klass="DataclassWithDocstring"))
# Check codes match as expected for dataclass objects.
errs = ["ES01", "SA01", "EX01", "PR01"]
for error in result["errors"]:
assert error[0] in errs
errs.remove(error[0])

# Test initialisation method (usually undocumented in dataclass) raises any errors.
init_fn = self._import_path(klass="DataclassWithDocstring", func="__init__")
result = validate_one(init_fn)
# Check that __init__ raises GL08 when the class docstring doesn't document params.
assert result["errors"][0][0] == "GL08"

def test_property_object(self):
# Test validator methods complete execution on class property objects
# Test case ought to be removed if property objects properly supported.
result = validate_one(
self._import_path(klass="ClassWithPropertyObject", func="param1")
)
# Check codes match as expected for property objects.
errs = ["ES01", "SA01", "EX01"]
for error in result["errors"]:
assert error[0] in errs
errs.remove(error[0])


def decorator(x):
"""Test decorator."""
Expand Down
Loading
Loading