diff --git a/doc/format.rst b/doc/format.rst index 6579191c..ecdd1d4f 100644 --- a/doc/format.rst +++ b/doc/format.rst @@ -557,7 +557,9 @@ Class docstring Use the same sections as outlined above (all except :ref:`Returns ` are applicable). The constructor (``__init__``) should also be documented here, the :ref:`Parameters ` section of the docstring details the -constructor's parameters. +constructor's parameters. While repetition is unnecessary, a docstring for +the class constructor (``__init__``) can, optionally, be added to provide +detailed initialization documentation. An **Attributes** section, located below the :ref:`Parameters ` section, may be used to describe non-method attributes of the class:: diff --git a/doc/validation.rst b/doc/validation.rst index aa9d5236..68844c2b 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -183,6 +183,9 @@ inline comments: def __init__(self): # numpydoc ignore=GL08 pass +Note that a properly formatted :ref:`class ` docstring +silences ``G08`` for an ``__init__`` constructor without a docstring. + This is supported by the :ref:`CLI `, :ref:`pre-commit hook `, and :ref:`Sphinx extension `. diff --git a/numpydoc/tests/test_validate.py b/numpydoc/tests/test_validate.py index 8b40794f..9f0f7942 100644 --- a/numpydoc/tests/test_validate.py +++ b/numpydoc/tests/test_validate.py @@ -1198,6 +1198,113 @@ def missing_whitespace_after_comma(self): """ +class ConstructorDocumentedInClassAndInit: + """ + Class to test constructor documented via class and constructor docstrings. + + A case where both the class docstring and the constructor docstring are + defined. + + Parameters + ---------- + param1 : int + Description of param1. + + See Also + -------- + otherclass : A class that does something else. + + Examples + -------- + This is an example of how to use ConstructorDocumentedInClassAndInit. + """ + + def __init__(self, param1: int) -> None: + """ + Constructor docstring with additional information. + + Extended information. + + Parameters + ---------- + param1 : int + Description of param1 with extra details. + + See Also + -------- + otherclass : A class that does something else. + + Examples + -------- + This is an example of how to use ConstructorDocumentedInClassAndInit. + """ + + +class ConstructorDocumentedInClass: + """ + Class to test constructor documented via class docstring. + + Useful to ensure that validation of `__init__` does not signal GL08, + when the class docstring properly documents the `__init__` constructor. + + Parameters + ---------- + param1 : int + Description of param1. + + See Also + -------- + otherclass : A class that does something else. + + Examples + -------- + This is an example of how to use ConstructorDocumentedInClass. + """ + + def __init__(self, param1: int) -> None: + pass + + +class ConstructorDocumentedInClassWithNoParameters: + """ + Class to test constructor documented via class docstring with no parameters. + + Useful to ensure that validation of `__init__` does not signal GL08, + when the class docstring properly documents the `__init__` constructor. + + See Also + -------- + otherclass : A class that does something else. + + Examples + -------- + This is an example of how to use ConstructorDocumentedInClassWithNoParameters. + """ + + def __init__(self) -> None: + pass + + +class IncompleteConstructorDocumentedInClass: + """ + Class to test an incomplete constructor docstring. + + This class does not properly document parameters. + Unnecessary extended summary. + + See Also + -------- + otherclass : A class that does something else. + + Examples + -------- + This is an example of how to use IncompleteConstructorDocumentedInClass. + """ + + def __init__(self, param1: int): + pass + + class TestValidator: def _import_path(self, klass=None, func=None): """ @@ -1536,6 +1643,40 @@ def test_bad_docstrings(self, capsys, klass, func, msgs): for msg in msgs: assert msg in " ".join(err[1] for err in result["errors"]) + @pytest.mark.parametrize( + "klass,exp_init_codes,exc_init_codes,exp_klass_codes", + [ + ("ConstructorDocumentedInClass", tuple(), ("GL08",), tuple()), + ("ConstructorDocumentedInClassAndInit", tuple(), ("GL08",), tuple()), + ( + "ConstructorDocumentedInClassWithNoParameters", + tuple(), + ("GL08",), + tuple(), + ), + ( + "IncompleteConstructorDocumentedInClass", + ("GL08",), + tuple(), + ("PR01"), # Parameter not documented in class constructor + ), + ], + ) + def test_constructor_docstrings( + self, klass, exp_init_codes, exc_init_codes, exp_klass_codes + ): + # First test the class docstring itself, checking expected_klass_codes match + result = validate_one(self._import_path(klass=klass)) + for err in result["errors"]: + assert err[0] in exp_klass_codes + + # Then test the constructor docstring + result = validate_one(self._import_path(klass=klass, func="__init__")) + for code in exp_init_codes: + assert code in " ".join(err[0] for err in result["errors"]) + for code in exc_init_codes: + assert code not in " ".join(err[0] for err in result["errors"]) + def decorator(x): """Test decorator.""" diff --git a/numpydoc/validate.py b/numpydoc/validate.py index 495d7201..858a06d2 100644 --- a/numpydoc/validate.py +++ b/numpydoc/validate.py @@ -637,7 +637,29 @@ def validate(obj_name, validator_cls=None, **validator_kwargs): errs = [] if not doc.raw_doc: - if "GL08" not in ignore_validation_comments: + report_GL08: bool = True + # Check if the object is a class and has a docstring in the constructor + # Also check if code_obj is defined, as undefined for the AstValidator in validate_docstrings.py. + if ( + doc.name.endswith(".__init__") + and doc.is_function_or_method + and hasattr(doc, "code_obj") + ): + cls_name = doc.code_obj.__qualname__.split(".")[0] + cls = Validator._load_obj(f"{doc.code_obj.__module__}.{cls_name}") + # cls = Validator._load_obj(f"{doc.name[:-9]}.{cls_name}") ## Alternative + cls_doc = Validator(get_doc_object(cls)) + + # Parameter_mismatches, PR01, PR02, PR03 are checked for the class docstring. + # If cls_doc has PR01, PR02, PR03 errors, i.e. invalid class docstring, + # then we also report missing constructor docstring, GL08. + report_GL08 = len(cls_doc.parameter_mismatches) > 0 + + # Check if GL08 is to be ignored: + if "GL08" in ignore_validation_comments: + report_GL08 = False + # Add GL08 error? + if report_GL08: errs.append(error("GL08")) return { "type": doc.type,