From 6e3b468078731239a5d70c4043d6b28383141f82 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 15:03:24 -0500 Subject: [PATCH 01/33] Add numpydoc.hooks directory. --- numpydoc/hooks/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 numpydoc/hooks/__init__.py diff --git a/numpydoc/hooks/__init__.py b/numpydoc/hooks/__init__.py new file mode 100644 index 00000000..f05d7fca --- /dev/null +++ b/numpydoc/hooks/__init__.py @@ -0,0 +1 @@ +"""Pre-commit hooks using numpydoc.""" From 6d5b54ece23516869fb11d46fc4083b2f2bd8fd5 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 15:04:17 -0500 Subject: [PATCH 02/33] Add numpydoc.hooks to packages. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 09cb946e..c0342de0 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def read(fname): setup( name="numpydoc", - packages=["numpydoc"], + packages=["numpydoc", "numpydoc.hooks"], version=version, description="Sphinx extension to support docstrings in Numpy format", long_description=read("README.rst"), From 3d4ba71467c8a0299db3ada64441177fd88786e8 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 15:08:01 -0500 Subject: [PATCH 03/33] Add tabulate dependency for use in validate hook. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c0342de0..87a20179 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ def read(fname): author_email="pav@iki.fi", url="https://numpydoc.readthedocs.io", license="BSD", - install_requires=["sphinx>=4.2", "Jinja2>=2.10"], + install_requires=["sphinx>=4.2", "Jinja2>=2.10", "tabulate>=0.8.10"], python_requires=">=3.7", extras_require={ "testing": [ From 2b0a9628f1905adb029aa59ed11a5b0e00b09e8c Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 15:54:11 -0500 Subject: [PATCH 04/33] Add option to pass a Validator class to validate() function. --- numpydoc/validate.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/numpydoc/validate.py b/numpydoc/validate.py index b975f831..8c7f9b83 100644 --- a/numpydoc/validate.py +++ b/numpydoc/validate.py @@ -450,7 +450,7 @@ def _check_desc(desc, code_no_desc, code_no_upper, code_no_period, **kwargs): return errs -def validate(obj_name): +def validate(obj_name, validator_cls=None, **validator_kwargs): """ Validate the docstring. @@ -461,6 +461,11 @@ def validate(obj_name): 'pandas.read_csv'. The string must include the full, unabbreviated package/module names, i.e. 'pandas.read_csv', not 'pd.read_csv' or 'read_csv'. + validator_cls : Validator, optional + The Validator class to use. By default, :class:`Validator` will be used. + **validator_kwargs + Keyword arguments to pass to ``validator_cls`` upon initialization. + Note that ``obj_name`` will be passed as a named argument as well. Returns ------- @@ -493,10 +498,13 @@ def validate(obj_name): they are validated, are not documented more than in the source code of this function. """ - if isinstance(obj_name, str): - doc = Validator(get_doc_object(Validator._load_obj(obj_name))) + if not validator_cls: + if isinstance(obj_name, str): + doc = Validator(get_doc_object(Validator._load_obj(obj_name))) + else: + doc = Validator(obj_name) else: - doc = Validator(obj_name) + doc = validator_cls(obj_name=obj_name, **validator_kwargs) errs = [] if not doc.raw_doc: From ed9ba3c6e6b5ba1ab02f652e24cf88c18e09d6b3 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 15:57:55 -0500 Subject: [PATCH 05/33] Add AST-based validation logic to review files. --- numpydoc/hooks/validate_docstrings.py | 288 ++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 numpydoc/hooks/validate_docstrings.py diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py new file mode 100644 index 00000000..6bef83b6 --- /dev/null +++ b/numpydoc/hooks/validate_docstrings.py @@ -0,0 +1,288 @@ +"""Run numpydoc validation on contents of a file.""" + +import argparse +import ast +import configparser +import os +import sys +from pathlib import Path + +from tabulate import tabulate + +from .. import docscrape, validate + + +class AstValidator(validate.Validator): + """ + Overrides the :class:`Validator` to work entirely with the AST. + + Parameters + ---------- + ast_node : ast.AST + The node under inspection. + filename : path-like + The file where the node is defined. + obj_name : str + A name for the node to use in the listing of issues for the file as a whole. + """ + + def __init__(self, *, ast_node, filename, obj_name): + self.node = ast_node + self.raw_doc = ast.get_docstring(self.node, clean=False) or "" + self.clean_doc = ast.get_docstring(self.node, clean=True) + self.doc = docscrape.NumpyDocString(self.raw_doc) + + self._source_file = os.path.abspath(filename) + self._name = obj_name + + self.is_class = isinstance(ast_node, ast.ClassDef) + self.is_module = isinstance(ast_node, ast.Module) + + @staticmethod + def _load_obj(name): + raise NotImplementedError("AstValidator does not support this method.") + + @property + def name(self): + return self._name + + @property + def is_function_or_method(self): + return isinstance(self.node, (ast.FunctionDef, ast.AsyncFunctionDef)) + + @property + def is_generator_function(self): + if not self.is_function_or_method: + return False + for child in ast.iter_child_nodes(self.node): + if isinstance(child, ast.Expr) and isinstance(child.value, ast.Yield): + return True + return False + + @property + def type(self): + if self.is_function_or_method: + return "function" + if self.is_class: + return "type" + if self.is_module: + return "module" + raise ValueError("Unknown type.") + + @property + def source_file_name(self): + return self._source_file + + @property + def source_file_def_line(self): + return self.node.lineno if not self.is_module else 0 + + @property + def signature_parameters(self): + def extract_signature(node): + args_node = node.args + params = [] + for arg_type in ["posonlyargs", "args", "vararg", "kwonlyargs", "kwarg"]: + entries = getattr(args_node, arg_type) + if arg_type in ["vararg", "kwarg"]: + if entries and arg_type == "vararg": + params.append(f"*{entries}") + if entries and arg_type == "kwarg": + params.append(f"**{entries}") + else: + params.extend([arg.arg for arg in entries]) + params = tuple(params) + if params and params[0] in {"self", "cls"}: + return params[1:] + return params + + params = tuple() + if self.is_function_or_method: + params = extract_signature(self.node) + elif self.is_class: + for child in self.node.body: + if isinstance(child, ast.FunctionDef) and child.name == "__init__": + params = extract_signature(child) + return params + + @property + def method_source(self): + with open(self.source_file_name) as file: + source = ast.get_source_segment(file.read(), self.node) + return source + + +class DocstringVisitor(ast.NodeVisitor): + """ + Visits nodes in the AST from a given module and reporting numpydoc issues. + + Parameters + ---------- + filepath : path-like + The absolute or relative path to the file to inspect. + ignore : list[str] + A list of check codes to ignore, if desired. + """ + + def __init__(self, filepath, ignore): + self.findings = [] + self.parent = None + self.filepath = filepath.replace("../", "") + self.name = self.filepath.replace("/", ".").replace(".py", "") + self.ignore = ignore + + def _get_numpydoc_issues(self, name, node): + """ + Get numpydoc validation issues. + + Parameters + ---------- + name : str + The full name of the node under inspection. + node : ast.AST + The node under inspection. + """ + report = validate.validate( + name, AstValidator, ast_node=node, filename=self.filepath + ) + self.findings.extend( + [ + [name, check, description] + for check, description in report["errors"] + if check not in self.ignore + ] + ) + + def visit(self, node): + """ + Visit a node in the AST and report on numpydoc validation issues. + + Parameters + ---------- + node : ast.AST + The node to visit. + """ + if isinstance(node, ast.Module): + self._get_numpydoc_issues(self.name, node) + self.parent = self.name + self.generic_visit(node) + + elif isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + node_name = f"{self.parent}.{node.name}" + self._get_numpydoc_issues(node_name, node) + if isinstance(node, ast.ClassDef): + self.parent = node_name + self.generic_visit(node) + + +def parse_config(): + """ + Parse config information from setup.cfg, if present. + + Returns + ------- + dict + Config options for the numpydoc validation hook. + """ + filename = "setup.cfg" + options = {"exclusions": []} + config = configparser.ConfigParser() + if Path(filename).exists(): + config.read(filename) + numpydoc_validation_config_section = "tool:numpydoc_validation" + try: + try: + options["exclusions"] = config.get( + numpydoc_validation_config_section, "ignore" + ).split(",") + except configparser.NoOptionError: + pass + except configparser.NoSectionError: + pass + return options + + +def process_file(filepath, ignore): + """ + Run numpydoc validation on a file. + + Parameters + ---------- + filepath : path-like + The absolute or relative path to the file to inspect. + ignore : list[str] + A list of check codes to ignore, if desired. + + Returns + ------- + dict + Config options for the numpydoc validation hook. + """ + with open(filepath) as file: + module_node = ast.parse(file.read(), filepath) + + docstring_visitor = DocstringVisitor(filepath=filepath, ignore=ignore) + docstring_visitor.visit(module_node) + + return docstring_visitor.findings + + +def main(argv=None): + """Run the numpydoc validation hook.""" + + config_options = parse_config() + ignored_checks = ( + "\n " + + "\n ".join( + [ + f"- {check}: {validate.ERROR_MSGS[check]}" + for check in config_options["exclusions"] + ] + ) + + "\n" + ) + + parser = argparse.ArgumentParser( + description="Run numpydoc validation on files with option to ignore individual checks.", + formatter_class=argparse.RawTextHelpFormatter, + ) + parser.add_argument( + "files", type=str, nargs="+", help="File(s) to run numpydoc validation on." + ) + parser.add_argument( + "--ignore", + type=str, + nargs="*", + help=( + f"""Check codes to ignore.{ + ' Currently ignoring the following from setup.cfg:' + f'{ignored_checks}' + 'Values provided here will be in addition to the above.' + if config_options["exclusions"] else '' + }""" + ), + ) + + args = parser.parse_args(argv) + + ignore = config_options["exclusions"] + (args.ignore or []) + findings = [] + for file in args.files: + findings.extend(process_file(file, ignore)) + + if findings: + print( + tabulate( + findings, + headers=["item", "check", "description"], + tablefmt="grid", + maxcolwidths=80, + ), + file=sys.stderr, + ) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From fb53cee4907d94cf59d2f8d5027cb21c89d11107 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 15:59:14 -0500 Subject: [PATCH 06/33] Add entry point for validation hook. --- setup.cfg | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.cfg b/setup.cfg index a1f52209..8ce12840 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,7 @@ addopts = --showlocals --doctest-modules -ra --cov-report= --cov=numpydoc --junit-xml=junit-results.xml --ignore=doc/ junit_family = xunit2 + +[options.entry_points] +console_scripts = + validate-docstrings = numpydoc.hooks.validate_docstrings:main From c18c990688f28b174ea3ceaa2e3bcf95b35a492c Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 16:00:00 -0500 Subject: [PATCH 07/33] Add pre-commit hook configuration. --- .pre-commit-hooks.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .pre-commit-hooks.yaml diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 00000000..b88e9ed2 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,7 @@ +- id: numpydoc-validation + name: numpydoc-validation + description: This hook validates that docstrings in committed files adhere to numpydoc standards. + entry: validate-docstrings + require_serial: true + language: python + types: [python] From f7df258579567e1b460e8ae06b4cd3609e97ad17 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 16:45:09 -0500 Subject: [PATCH 08/33] Add SS05 match pattern for allowed verbs; add some type annotations. --- numpydoc/hooks/validate_docstrings.py | 113 +++++++++++++++++--------- 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 6bef83b6..b528447b 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -4,8 +4,10 @@ import ast import configparser import os +import re import sys from pathlib import Path +from typing import Sequence, Tuple, Union from tabulate import tabulate @@ -26,32 +28,34 @@ class AstValidator(validate.Validator): A name for the node to use in the listing of issues for the file as a whole. """ - def __init__(self, *, ast_node, filename, obj_name): - self.node = ast_node - self.raw_doc = ast.get_docstring(self.node, clean=False) or "" - self.clean_doc = ast.get_docstring(self.node, clean=True) - self.doc = docscrape.NumpyDocString(self.raw_doc) + def __init__( + self, *, ast_node: ast.AST, filename: os.PathLike, obj_name: str + ) -> None: + self.node: ast.AST = ast_node + self.raw_doc: str = ast.get_docstring(self.node, clean=False) or "" + self.clean_doc: str = ast.get_docstring(self.node, clean=True) + self.doc: docscrape.NumpyDocString = docscrape.NumpyDocString(self.raw_doc) - self._source_file = os.path.abspath(filename) - self._name = obj_name + self._source_file: os.PathLike = os.path.abspath(filename) + self._name: str = obj_name - self.is_class = isinstance(ast_node, ast.ClassDef) - self.is_module = isinstance(ast_node, ast.Module) + self.is_class: bool = isinstance(ast_node, ast.ClassDef) + self.is_module: bool = isinstance(ast_node, ast.Module) @staticmethod def _load_obj(name): raise NotImplementedError("AstValidator does not support this method.") @property - def name(self): + def name(self) -> str: return self._name @property - def is_function_or_method(self): + def is_function_or_method(self) -> bool: return isinstance(self.node, (ast.FunctionDef, ast.AsyncFunctionDef)) @property - def is_generator_function(self): + def is_generator_function(self) -> bool: if not self.is_function_or_method: return False for child in ast.iter_child_nodes(self.node): @@ -60,7 +64,7 @@ def is_generator_function(self): return False @property - def type(self): + def type(self) -> str: if self.is_function_or_method: return "function" if self.is_class: @@ -70,15 +74,15 @@ def type(self): raise ValueError("Unknown type.") @property - def source_file_name(self): + def source_file_name(self) -> str: return self._source_file @property - def source_file_def_line(self): + def source_file_def_line(self) -> int: return self.node.lineno if not self.is_module else 0 @property - def signature_parameters(self): + def signature_parameters(self) -> Tuple[str]: def extract_signature(node): args_node = node.args params = [] @@ -106,7 +110,7 @@ def extract_signature(node): return params @property - def method_source(self): + def method_source(self) -> str: with open(self.source_file_name) as file: source = ast.get_source_segment(file.read(), self.node) return source @@ -118,20 +122,44 @@ class DocstringVisitor(ast.NodeVisitor): Parameters ---------- - filepath : path-like + filepath : str The absolute or relative path to the file to inspect. - ignore : list[str] - A list of check codes to ignore, if desired. + config : dict + Configuration options for reviewing flagged issues. """ - def __init__(self, filepath, ignore): - self.findings = [] - self.parent = None - self.filepath = filepath.replace("../", "") - self.name = self.filepath.replace("/", ".").replace(".py", "") - self.ignore = ignore + def __init__(self, filepath: str, config: dict) -> None: + self.findings: list = [] + self.parent: str = None + self.filepath: str = filepath.replace("../", "") + self.name: str = self.filepath.replace("/", ".").replace(".py", "") + self.config: dict = config - def _get_numpydoc_issues(self, name, node): + def _ignore_issue(self, node: ast.AST, check: str) -> bool: + """ + Check whether the issue should be ignored. + + Parameters + ---------- + node : ast.AST + The node under inspection. + check : str + The code for the check being evaluated. + + Return + ------ + bool + Whether the issue should be exluded from the report. + """ + if check in self.config["exclusions"]: + return True + if check == "SS05": + pattern = self.config.get("SS05_override") + if pattern: + return re.match(pattern, ast.get_docstring(node)) is not None + return False + + def _get_numpydoc_issues(self, name: str, node: ast.AST) -> None: """ Get numpydoc validation issues. @@ -149,11 +177,11 @@ def _get_numpydoc_issues(self, name, node): [ [name, check, description] for check, description in report["errors"] - if check not in self.ignore + if not self._ignore_issue(node, check) ] ) - def visit(self, node): + def visit(self, node: ast.AST) -> None: """ Visit a node in the AST and report on numpydoc validation issues. @@ -166,7 +194,6 @@ def visit(self, node): self._get_numpydoc_issues(self.name, node) self.parent = self.name self.generic_visit(node) - elif isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): node_name = f"{self.parent}.{node.name}" self._get_numpydoc_issues(node_name, node) @@ -175,7 +202,7 @@ def visit(self, node): self.generic_visit(node) -def parse_config(): +def parse_config() -> dict: """ Parse config information from setup.cfg, if present. @@ -197,12 +224,18 @@ def parse_config(): ).split(",") except configparser.NoOptionError: pass + try: + options["SS05_override"] = re.compile( + config.get(numpydoc_validation_config_section, "SS05_override") + ) + except configparser.NoOptionError: + pass except configparser.NoSectionError: pass return options -def process_file(filepath, ignore): +def process_file(filepath: os.PathLike, config: dict) -> "list[list[str]]": """ Run numpydoc validation on a file. @@ -210,24 +243,24 @@ def process_file(filepath, ignore): ---------- filepath : path-like The absolute or relative path to the file to inspect. - ignore : list[str] - A list of check codes to ignore, if desired. + config : dict + Configuration options for reviewing flagged issues. Returns ------- - dict - Config options for the numpydoc validation hook. + list[list[str]] + A list of [name, check, description] lists for flagged issues. """ with open(filepath) as file: module_node = ast.parse(file.read(), filepath) - docstring_visitor = DocstringVisitor(filepath=filepath, ignore=ignore) + docstring_visitor = DocstringVisitor(filepath=str(filepath), config=config) docstring_visitor.visit(module_node) return docstring_visitor.findings -def main(argv=None): +def main(argv: Union[Sequence[str], None] = None) -> int: """Run the numpydoc validation hook.""" config_options = parse_config() @@ -264,11 +297,11 @@ def main(argv=None): ) args = parser.parse_args(argv) + config_options["exclusions"].extend(args.ignore or []) - ignore = config_options["exclusions"] + (args.ignore or []) findings = [] for file in args.files: - findings.extend(process_file(file, ignore)) + findings.extend(process_file(file, config_options)) if findings: print( From ee49b0db875c72fffe32152b3bd402cf6be5fca5 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 17:09:11 -0500 Subject: [PATCH 09/33] Add config option to override GL08 by name. --- numpydoc/hooks/validate_docstrings.py | 29 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index b528447b..36788f5b 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -153,10 +153,19 @@ def _ignore_issue(self, node: ast.AST, check: str) -> bool: """ if check in self.config["exclusions"]: return True - if check == "SS05": - pattern = self.config.get("SS05_override") - if pattern: - return re.match(pattern, ast.get_docstring(node)) is not None + if self.config["overrides"]: + try: + if check == "GL08": + pattern = self.config["overrides"].get("GL08") + if pattern: + return re.match(pattern, node.name) + except AttributeError: # ast.Module nodes don't have a name + pass + + if check == "SS05": + pattern = self.config["overrides"].get("SS05") + if pattern: + return re.match(pattern, ast.get_docstring(node)) is not None return False def _get_numpydoc_issues(self, name: str, node: ast.AST) -> None: @@ -212,7 +221,7 @@ def parse_config() -> dict: Config options for the numpydoc validation hook. """ filename = "setup.cfg" - options = {"exclusions": []} + options = {"exclusions": [], "overrides": {}} config = configparser.ConfigParser() if Path(filename).exists(): config.read(filename) @@ -225,8 +234,14 @@ def parse_config() -> dict: except configparser.NoOptionError: pass try: - options["SS05_override"] = re.compile( - config.get(numpydoc_validation_config_section, "SS05_override") + options["overrides"]["SS05"] = re.compile( + config.get(numpydoc_validation_config_section, "override_SS05") + ) + except configparser.NoOptionError: + pass + try: + options["overrides"]["GL08"] = re.compile( + config.get(numpydoc_validation_config_section, "override_GL08") ) except configparser.NoOptionError: pass From 2409e43e9c1d9726c75883526fcb6d86cac244d2 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 17:15:48 -0500 Subject: [PATCH 10/33] Add option specify a path to a config file. --- numpydoc/hooks/validate_docstrings.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 36788f5b..f4455342 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -211,16 +211,22 @@ def visit(self, node: ast.AST) -> None: self.generic_visit(node) -def parse_config() -> dict: +def parse_config(filepath: os.PathLike = None) -> dict: """ - Parse config information from setup.cfg, if present. + Parse config information from a .cfg file. + + Parameters + ---------- + filepath : os.PathLike + An absolute or relative path to a .cfg file specifying + a [tool:numpydoc_validation] section. Returns ------- dict Config options for the numpydoc validation hook. """ - filename = "setup.cfg" + filename = filepath or "setup.cfg" options = {"exclusions": [], "overrides": {}} config = configparser.ConfigParser() if Path(filename).exists(): @@ -297,6 +303,14 @@ def main(argv: Union[Sequence[str], None] = None) -> int: parser.add_argument( "files", type=str, nargs="+", help="File(s) to run numpydoc validation on." ) + parser.add_argument( + "--config", + type=str, + help=( + "Path to a .cfg file if not in the current directory. " + "Options must be placed under [tool:numpydoc_validation]." + ), + ) parser.add_argument( "--ignore", type=str, @@ -312,6 +326,9 @@ def main(argv: Union[Sequence[str], None] = None) -> int: ) args = parser.parse_args(argv) + if args.config: # an alternative config file was provided + config_options = parse_config(args.config) + config_options["exclusions"].extend(args.ignore or []) findings = [] From a4fdc71fa58b7fb9c51b1e088074e3429ce6b3fe Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 17:43:46 -0500 Subject: [PATCH 11/33] Add information on the hook to the docs. --- doc/validation.rst | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/doc/validation.rst b/doc/validation.rst index ee6e08d2..c69f34c7 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -2,6 +2,54 @@ Validation ========== +Docstring Validation using Pre-Commit Hook +------------------------------------------ + +To enable validation of docstrings as you commit files, add the +following to your ``.pre-commit-config.yaml`` file:: + + - repo: https://github.com/numpy/numpydoc + rev: + hooks: + - id: numpydoc-validation + +After installing ``numpydoc``, run the following to see available +command line options for this hook: + +.. code-block:: + + $ python -m numpydoc.hooks.validate_docstrings --help + +Using a ``setup.cfg`` file provides additional customization. +Options must be placed under the ``[tool:numpydoc_validation]`` section. +The example below configures the pre-commit hook to ignore three checks +and specifies exceptions to the checks ``SS05`` (allow docstrings to +start with "Process ", "Assess ", or "Access ") and ``GL08`` (allow +the class/method/function with name "__init__" to not have a docstring):: + + [tool:numpydoc_validation] + ignore = EX01,SA01,ES01 + override_SS05 = ^(Process|Assess|Access ) + override_GL08 = ^(__init__)$ + +If any issues are found when commiting, a report is printed out and the +commit is stopped:: + + numpydoc-validation......................................................Failed + - hook id: numpydoc-validation + - exit code: 1 + + +----------------------------+---------+--------------------------------------+ + | item | check | description | + +============================+=========+======================================+ + | pkg.module | GL08 | The object does not have a docstring | + | pkg.module.function | GL08 | The object does not have a docstring | + | pkg.module.class | RT03 | Return value has no description | + | pkg.module.class.method | PR04 | Parameter "field" has no type | + +----------------------------+---------+--------------------------------------+ + +See below for a full listing of checks. + Docstring Validation using Python --------------------------------- From 749feeed8360f1ec92523d52c43b53f8ce659bc1 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 18:18:15 -0500 Subject: [PATCH 12/33] Grab arg name off ast arg nodes for *args/**kwargs; don't alter filepath. --- numpydoc/hooks/validate_docstrings.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index f4455342..e594967b 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -90,9 +90,9 @@ def extract_signature(node): entries = getattr(args_node, arg_type) if arg_type in ["vararg", "kwarg"]: if entries and arg_type == "vararg": - params.append(f"*{entries}") + params.append(f"*{entries.arg}") if entries and arg_type == "kwarg": - params.append(f"**{entries}") + params.append(f"**{entries.arg}") else: params.extend([arg.arg for arg in entries]) params = tuple(params) @@ -131,8 +131,10 @@ class DocstringVisitor(ast.NodeVisitor): def __init__(self, filepath: str, config: dict) -> None: self.findings: list = [] self.parent: str = None - self.filepath: str = filepath.replace("../", "") - self.name: str = self.filepath.replace("/", ".").replace(".py", "") + self.filepath: str = filepath + self.name: str = ( + self.filepath.replace("../", "").replace("/", ".").replace(".py", "") + ) self.config: dict = config def _ignore_issue(self, node: ast.AST, check: str) -> bool: From 8f4457bc260df3c6bc8c175ab623f807ae89794a Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 18:20:55 -0500 Subject: [PATCH 13/33] Update spacing in example. --- doc/validation.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/validation.rst b/doc/validation.rst index c69f34c7..f2924620 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -39,14 +39,14 @@ commit is stopped:: - hook id: numpydoc-validation - exit code: 1 - +----------------------------+---------+--------------------------------------+ - | item | check | description | - +============================+=========+======================================+ - | pkg.module | GL08 | The object does not have a docstring | - | pkg.module.function | GL08 | The object does not have a docstring | - | pkg.module.class | RT03 | Return value has no description | - | pkg.module.class.method | PR04 | Parameter "field" has no type | - +----------------------------+---------+--------------------------------------+ + +-------------------------+---------+--------------------------------------+ + | item | check | description | + +=========================+=========+======================================+ + | pkg.module | GL08 | The object does not have a docstring | + | pkg.module.function | GL08 | The object does not have a docstring | + | pkg.module.class | RT03 | Return value has no description | + | pkg.module.class.method | PR04 | Parameter "field" has no type | + +-------------------------+---------+--------------------------------------+ See below for a full listing of checks. From 34061f3f741e4e8df77ec316a2126161c4149349 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 19 Feb 2023 18:38:11 -0500 Subject: [PATCH 14/33] Reduce maxcolwidths in report of findings by hook. --- numpydoc/hooks/validate_docstrings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index e594967b..046e7d2d 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -343,7 +343,7 @@ def main(argv: Union[Sequence[str], None] = None) -> int: findings, headers=["item", "check", "description"], tablefmt="grid", - maxcolwidths=80, + maxcolwidths=50, ), file=sys.stderr, ) From 056bcd841b0b7f01bcc7390f1663dc0dc27742c9 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 20 Feb 2023 14:37:10 -0500 Subject: [PATCH 15/33] Show file in a separate column of the findings; item is from module onward. --- numpydoc/hooks/validate_docstrings.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 046e7d2d..b4e827cc 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -132,9 +132,7 @@ def __init__(self, filepath: str, config: dict) -> None: self.findings: list = [] self.parent: str = None self.filepath: str = filepath - self.name: str = ( - self.filepath.replace("../", "").replace("/", ".").replace(".py", "") - ) + self.module_name: str = os.path.splitext(os.path.basename(self.filepath))[0] self.config: dict = config def _ignore_issue(self, node: ast.AST, check: str) -> bool: @@ -186,7 +184,7 @@ def _get_numpydoc_issues(self, name: str, node: ast.AST) -> None: ) self.findings.extend( [ - [name, check, description] + [self.filepath, name, check, description] for check, description in report["errors"] if not self._ignore_issue(node, check) ] @@ -202,8 +200,8 @@ def visit(self, node: ast.AST) -> None: The node to visit. """ if isinstance(node, ast.Module): - self._get_numpydoc_issues(self.name, node) - self.parent = self.name + self._get_numpydoc_issues(self.module_name, node) + self.parent = self.module_name self.generic_visit(node) elif isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): node_name = f"{self.parent}.{node.name}" @@ -341,7 +339,7 @@ def main(argv: Union[Sequence[str], None] = None) -> int: print( tabulate( findings, - headers=["item", "check", "description"], + headers=["file", "item", "check", "description"], tablefmt="grid", maxcolwidths=50, ), From 56f6320fc89cfe3e01e8fe9c317df1f99c8f59a7 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 20 Feb 2023 14:37:47 -0500 Subject: [PATCH 16/33] Update example in docs for new column. --- doc/validation.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/validation.rst b/doc/validation.rst index f2924620..2935ab58 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -39,14 +39,14 @@ commit is stopped:: - hook id: numpydoc-validation - exit code: 1 - +-------------------------+---------+--------------------------------------+ - | item | check | description | - +=========================+=========+======================================+ - | pkg.module | GL08 | The object does not have a docstring | - | pkg.module.function | GL08 | The object does not have a docstring | - | pkg.module.class | RT03 | Return value has no description | - | pkg.module.class.method | PR04 | Parameter "field" has no type | - +-------------------------+---------+--------------------------------------+ + +-----------------------+--------------------------+---------+--------------------------------------+ + | file | item | check | description | + +=======================+==========================+=========+======================================+ + | src/pkg/utils.py | utils | GL08 | The object does not have a docstring | + | src/pkg/utils.py | utils.normalize | PR04 | Parameter "field" has no type | + | src/pkg/module_one.py | module_one.MyClass | GL08 | The object does not have a docstring | + | src/pkg/module_one.py | module_one.MyClass.parse | RT03 | Return value has no description | + +-----------------------+--------------------------+---------+--------------------------------------+ See below for a full listing of checks. From 26ac09ef39ed1e39a600169ca511881b8bc8d7af Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 20 Feb 2023 14:37:47 -0500 Subject: [PATCH 17/33] Update example in docs for new column. --- doc/validation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/validation.rst b/doc/validation.rst index 2935ab58..6a48a135 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -29,7 +29,7 @@ the class/method/function with name "__init__" to not have a docstring):: [tool:numpydoc_validation] ignore = EX01,SA01,ES01 - override_SS05 = ^(Process|Assess|Access ) + override_SS05 = ^((Process|Assess|Access) ) override_GL08 = ^(__init__)$ If any issues are found when commiting, a report is printed out and the From 5d9584d01cbbb53e33b12c09a60cadaebcf6d488 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:21:19 -0500 Subject: [PATCH 18/33] Switch to a stack for visiting. --- numpydoc/hooks/validate_docstrings.py | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index b4e827cc..460be5c4 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -129,11 +129,11 @@ class DocstringVisitor(ast.NodeVisitor): """ def __init__(self, filepath: str, config: dict) -> None: - self.findings: list = [] - self.parent: str = None + self.config: dict = config self.filepath: str = filepath self.module_name: str = os.path.splitext(os.path.basename(self.filepath))[0] - self.config: dict = config + self.stack: list[str] = [] + self.findings: list = [] def _ignore_issue(self, node: ast.AST, check: str) -> bool: """ @@ -168,17 +168,16 @@ def _ignore_issue(self, node: ast.AST, check: str) -> bool: return re.match(pattern, ast.get_docstring(node)) is not None return False - def _get_numpydoc_issues(self, name: str, node: ast.AST) -> None: + def _get_numpydoc_issues(self, node: ast.AST) -> None: """ Get numpydoc validation issues. Parameters ---------- - name : str - The full name of the node under inspection. node : ast.AST The node under inspection. """ + name = ".".join(self.stack) report = validate.validate( name, AstValidator, ast_node=node, filename=self.filepath ) @@ -199,16 +198,15 @@ def visit(self, node: ast.AST) -> None: node : ast.AST The node to visit. """ - if isinstance(node, ast.Module): - self._get_numpydoc_issues(self.module_name, node) - self.parent = self.module_name - self.generic_visit(node) - elif isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): - node_name = f"{self.parent}.{node.name}" - self._get_numpydoc_issues(node_name, node) - if isinstance(node, ast.ClassDef): - self.parent = node_name + if isinstance( + node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef) + ): + self.stack.append( + self.module_name if isinstance(node, ast.Module) else node.name + ) + self._get_numpydoc_issues(node) self.generic_visit(node) + _ = self.stack.pop() def parse_config(filepath: os.PathLike = None) -> dict: From afa250950700d7b6ae837c77edc9cab96622dcfe Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 20 Feb 2023 21:12:03 -0500 Subject: [PATCH 19/33] Add line to file column; show module lineno as 1. --- doc/validation.rst | 16 ++++++++-------- numpydoc/hooks/validate_docstrings.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/validation.rst b/doc/validation.rst index 6a48a135..eea49c0f 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -39,14 +39,14 @@ commit is stopped:: - hook id: numpydoc-validation - exit code: 1 - +-----------------------+--------------------------+---------+--------------------------------------+ - | file | item | check | description | - +=======================+==========================+=========+======================================+ - | src/pkg/utils.py | utils | GL08 | The object does not have a docstring | - | src/pkg/utils.py | utils.normalize | PR04 | Parameter "field" has no type | - | src/pkg/module_one.py | module_one.MyClass | GL08 | The object does not have a docstring | - | src/pkg/module_one.py | module_one.MyClass.parse | RT03 | Return value has no description | - +-----------------------+--------------------------+---------+--------------------------------------+ + +--------------------------+--------------------------+---------+--------------------------------------+ + | file | item | check | description | + +==========================+==========================+=========+======================================+ + | src/pkg/utils.py:1 | utils | GL08 | The object does not have a docstring | + | src/pkg/utils.py:90 | utils.normalize | PR04 | Parameter "field" has no type | + | src/pkg/module_one.py:12 | module_one.MyClass | GL08 | The object does not have a docstring | + | src/pkg/module_one.py:33 | module_one.MyClass.parse | RT03 | Return value has no description | + +--------------------------+--------------------------+---------+--------------------------------------+ See below for a full listing of checks. diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 460be5c4..bc260b2e 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -79,7 +79,7 @@ def source_file_name(self) -> str: @property def source_file_def_line(self) -> int: - return self.node.lineno if not self.is_module else 0 + return self.node.lineno if not self.is_module else 1 @property def signature_parameters(self) -> Tuple[str]: @@ -183,7 +183,7 @@ def _get_numpydoc_issues(self, node: ast.AST) -> None: ) self.findings.extend( [ - [self.filepath, name, check, description] + [f'{self.filepath}:{report["file_line"]}', name, check, description] for check, description in report["errors"] if not self._ignore_issue(node, check) ] From cead4ae0776a2c47b26ab0d5fff88bcb110235b6 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Thu, 23 Feb 2023 20:54:31 -0500 Subject: [PATCH 20/33] Expand user on config file path. --- numpydoc/hooks/validate_docstrings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index bc260b2e..181b5681 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -224,7 +224,7 @@ def parse_config(filepath: os.PathLike = None) -> dict: dict Config options for the numpydoc validation hook. """ - filename = filepath or "setup.cfg" + filename = Path(filepath or "setup.cfg").expanduser() options = {"exclusions": [], "overrides": {}} config = configparser.ConfigParser() if Path(filename).exists(): From 195d35b60acc15c78434dac3bb6af7125d5491b9 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 25 Feb 2023 16:48:20 -0500 Subject: [PATCH 21/33] Use Path operations. --- numpydoc/hooks/validate_docstrings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 181b5681..59ae40db 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -36,7 +36,7 @@ def __init__( self.clean_doc: str = ast.get_docstring(self.node, clean=True) self.doc: docscrape.NumpyDocString = docscrape.NumpyDocString(self.raw_doc) - self._source_file: os.PathLike = os.path.abspath(filename) + self._source_file: os.PathLike = Path(filename).resolve() self._name: str = obj_name self.is_class: bool = isinstance(ast_node, ast.ClassDef) @@ -131,7 +131,7 @@ class DocstringVisitor(ast.NodeVisitor): def __init__(self, filepath: str, config: dict) -> None: self.config: dict = config self.filepath: str = filepath - self.module_name: str = os.path.splitext(os.path.basename(self.filepath))[0] + self.module_name: str = Path(self.filepath).stem self.stack: list[str] = [] self.findings: list = [] From 9d73d6a862d4071ae6fc1a3ab52a25991b75337c Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Feb 2023 18:14:07 -0500 Subject: [PATCH 22/33] Add support for using inline comments to ignore specific checks. --- numpydoc/hooks/validate_docstrings.py | 46 +++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 59ae40db..7b55d80e 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -6,6 +6,7 @@ import os import re import sys +import tokenize from pathlib import Path from typing import Sequence, Tuple, Union @@ -14,6 +15,10 @@ from .. import docscrape, validate +# inline comments that can suppress individual checks per line +IGNORE_COMMENT_PATTERN = re.compile("(?:.* numpydoc ignore[=|:] ?)(.+)") + + class AstValidator(validate.Validator): """ Overrides the :class:`Validator` to work entirely with the AST. @@ -126,10 +131,19 @@ class DocstringVisitor(ast.NodeVisitor): The absolute or relative path to the file to inspect. config : dict Configuration options for reviewing flagged issues. + numpydoc_ignore_comments : dict + A mapping of line number to checks to ignore. + Derived from comments in the source code. """ - def __init__(self, filepath: str, config: dict) -> None: + def __init__( + self, + filepath: str, + config: dict, + numpydoc_ignore_comments: dict, + ) -> None: self.config: dict = config + self.numpydoc_ignore_comments = numpydoc_ignore_comments self.filepath: str = filepath self.module_name: str = Path(self.filepath).stem self.stack: list[str] = [] @@ -153,19 +167,27 @@ def _ignore_issue(self, node: ast.AST, check: str) -> bool: """ if check in self.config["exclusions"]: return True + if self.config["overrides"]: try: if check == "GL08": pattern = self.config["overrides"].get("GL08") - if pattern: - return re.match(pattern, node.name) + if pattern and re.match(pattern, node.name): + return True except AttributeError: # ast.Module nodes don't have a name pass if check == "SS05": pattern = self.config["overrides"].get("SS05") - if pattern: - return re.match(pattern, ast.get_docstring(node)) is not None + if pattern and re.match(pattern, ast.get_docstring(node)) is not None: + return True + + try: + if check in self.numpydoc_ignore_comments[getattr(node, "lineno", 1)]: + return True + except KeyError: + pass + return False def _get_numpydoc_issues(self, node: ast.AST) -> None: @@ -273,7 +295,19 @@ def process_file(filepath: os.PathLike, config: dict) -> "list[list[str]]": with open(filepath) as file: module_node = ast.parse(file.read(), filepath) - docstring_visitor = DocstringVisitor(filepath=str(filepath), config=config) + with open(filepath) as file: + numpydoc_ignore_comments = { + token.start[0]: rules.group(1).split(",") + for token in tokenize.generate_tokens(file.readline) + if token.type == tokenize.COMMENT + and (rules := re.match(IGNORE_COMMENT_PATTERN, token.string)) + } + + docstring_visitor = DocstringVisitor( + filepath=str(filepath), + config=config, + numpydoc_ignore_comments=numpydoc_ignore_comments, + ) docstring_visitor.visit(module_node) return docstring_visitor.findings From 8e5953583c84619af54b77c32558ab99d9d241d1 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Feb 2023 18:41:36 -0500 Subject: [PATCH 23/33] Add support for comments at the end of a multiline declaration. --- numpydoc/hooks/validate_docstrings.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 7b55d80e..cbfe12b7 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -3,6 +3,7 @@ import argparse import ast import configparser +from nis import match import os import re import sys @@ -296,12 +297,17 @@ def process_file(filepath: os.PathLike, config: dict) -> "list[list[str]]": module_node = ast.parse(file.read(), filepath) with open(filepath) as file: - numpydoc_ignore_comments = { - token.start[0]: rules.group(1).split(",") - for token in tokenize.generate_tokens(file.readline) - if token.type == tokenize.COMMENT - and (rules := re.match(IGNORE_COMMENT_PATTERN, token.string)) - } + numpydoc_ignore_comments = {} + last_declaration = 1 + declarations = ["async def", "def", "class"] + for token in tokenize.generate_tokens(file.readline): + if token.type == tokenize.NAME and token.string in declarations: + last_declaration = token.start[0] + if token.type == tokenize.COMMENT: + match = re.match(IGNORE_COMMENT_PATTERN, token.string) + if match: + rules = match.group(1).split(",") + numpydoc_ignore_comments[last_declaration] = rules docstring_visitor = DocstringVisitor( filepath=str(filepath), From 629c65c4e1a393676ccdc091f2adca68d1bf70ef Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Feb 2023 18:46:35 -0500 Subject: [PATCH 24/33] Add a note on inline option to docs. --- doc/validation.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/validation.rst b/doc/validation.rst index eea49c0f..745de1b4 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -32,6 +32,15 @@ the class/method/function with name "__init__" to not have a docstring):: override_SS05 = ^((Process|Assess|Access) ) override_GL08 = ^(__init__)$ +For more fine-tuned control, you can also include inline comments to tell the +validation hook to ignore certain checks:: + + class SomeClass: # numpydoc ignore=EX01,SA01,ES01 + """This is the docstring for SomeClass.""" + + def __init__(self): # numpydoc ignore=GL08 + pass + If any issues are found when commiting, a report is printed out and the commit is stopped:: From 405affb212791a3670bafa9d731a7b9c3a65aeb4 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Feb 2023 18:57:56 -0500 Subject: [PATCH 25/33] Simplify check for the declaration start. --- numpydoc/hooks/validate_docstrings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index cbfe12b7..716cf5bd 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -299,7 +299,7 @@ def process_file(filepath: os.PathLike, config: dict) -> "list[list[str]]": with open(filepath) as file: numpydoc_ignore_comments = {} last_declaration = 1 - declarations = ["async def", "def", "class"] + declarations = ["def", "class"] for token in tokenize.generate_tokens(file.readline): if token.type == tokenize.NAME and token.string in declarations: last_declaration = token.start[0] From e2e42baf6773fa00152e11a1873e72cd49a1ebdb Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 4 Mar 2023 08:09:54 -0500 Subject: [PATCH 26/33] Add support for reading hook config from pyproject.toml --- doc/validation.rst | 22 +++++++++-- numpydoc/hooks/validate_docstrings.py | 54 ++++++++++++++++++++------- setup.py | 7 +++- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/doc/validation.rst b/doc/validation.rst index 745de1b4..641d5647 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -20,12 +20,26 @@ command line options for this hook: $ python -m numpydoc.hooks.validate_docstrings --help -Using a ``setup.cfg`` file provides additional customization. -Options must be placed under the ``[tool:numpydoc_validation]`` section. +Using a config file provides additional customization. Both +``pyproject.toml`` and ``setup.cfg`` are supported; however, if the +project contains both you must use the ``pyproject.toml`` file. The example below configures the pre-commit hook to ignore three checks and specifies exceptions to the checks ``SS05`` (allow docstrings to start with "Process ", "Assess ", or "Access ") and ``GL08`` (allow -the class/method/function with name "__init__" to not have a docstring):: +the class/method/function with name "__init__" to not have a docstring). + +``pyproject.toml``:: + + [tool.numpydoc_validation] + ignore = [ + "EX01", + "SA01", + "ES01", + ] + override_SS05 = '^((Process|Assess|Access) )' + override_GL08 = '^(__init__)$' + +``setup.cfg``:: [tool:numpydoc_validation] ignore = EX01,SA01,ES01 @@ -42,7 +56,7 @@ validation hook to ignore certain checks:: pass If any issues are found when commiting, a report is printed out and the -commit is stopped:: +commit is halted:: numpydoc-validation......................................................Failed - hook id: numpydoc-validation diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 716cf5bd..3b2552da 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -3,11 +3,16 @@ import argparse import ast import configparser -from nis import match import os import re import sys import tokenize + +try: + import tomllib +except ImportError: + import tomli as tomllib + from pathlib import Path from typing import Sequence, Tuple, Union @@ -232,26 +237,46 @@ def visit(self, node: ast.AST) -> None: _ = self.stack.pop() -def parse_config(filepath: os.PathLike = None) -> dict: +def parse_config(dir_path: os.PathLike = None) -> dict: """ - Parse config information from a .cfg file. + Parse config information from a pyproject.toml or setup.cfg file. + + This function looks in the provided directory path first for a + pyproject.toml file. If it finds that, it won't look for a setup.cfg + file. Parameters ---------- - filepath : os.PathLike - An absolute or relative path to a .cfg file specifying - a [tool:numpydoc_validation] section. + dir_path : os.PathLike + An absolute or relative path to a directory containing + either a pyproject.toml file specifying a + [tool.numpydoc_validation] section or a setup.cfg file + specifying a [tool:numpydoc_validation] section. + For example, ``~/my_project``. Returns ------- dict Config options for the numpydoc validation hook. """ - filename = Path(filepath or "setup.cfg").expanduser() options = {"exclusions": [], "overrides": {}} - config = configparser.ConfigParser() - if Path(filename).exists(): - config.read(filename) + dir_path = Path(dir_path or ".").expanduser().resolve() + + toml_path = dir_path / "pyproject.toml" + cfg_path = dir_path / "setup.cfg" + + if toml_path.is_file(): + with open(toml_path, "rb") as toml_file: + pyproject_toml = tomllib.load(toml_file) + config = pyproject_toml.get("tool", {}).get("numpydoc_validation", {}) + options["exclusions"] = config.get("ignore", []) + for check in ["SS05", "GL08"]: + regex = config.get(f"override_{check}") + if regex: + options["overrides"][check] = re.compile(regex) + elif cfg_path.is_file(): + config = configparser.ConfigParser() + config.read(cfg_path) numpydoc_validation_config_section = "tool:numpydoc_validation" try: try: @@ -274,6 +299,7 @@ def parse_config(filepath: os.PathLike = None) -> dict: pass except configparser.NoSectionError: pass + return options @@ -345,8 +371,11 @@ def main(argv: Union[Sequence[str], None] = None) -> int: "--config", type=str, help=( - "Path to a .cfg file if not in the current directory. " - "Options must be placed under [tool:numpydoc_validation]." + "Path to a directory containing a pyproject.toml or setup.cfg file\n" + "if not in the current directory. If both are present, only\n" + "pyproject.toml will be used. Options must be placed under\n" + "[tool:numpydoc_validation] for setup.cfg files and\n" + "[tool.numpydoc_validation] for pyproject.toml files." ), ) parser.add_argument( @@ -366,7 +395,6 @@ def main(argv: Union[Sequence[str], None] = None) -> int: args = parser.parse_args(argv) if args.config: # an alternative config file was provided config_options = parse_config(args.config) - config_options["exclusions"].extend(args.ignore or []) findings = [] diff --git a/setup.py b/setup.py index 87a20179..fc16a751 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,12 @@ def read(fname): author_email="pav@iki.fi", url="https://numpydoc.readthedocs.io", license="BSD", - install_requires=["sphinx>=4.2", "Jinja2>=2.10", "tabulate>=0.8.10"], + install_requires=[ + "sphinx>=4.2", + "Jinja2>=2.10", + "tabulate>=0.8.10", + "tomli>=1.1.0;python_version<'3.11'", + ], python_requires=">=3.7", extras_require={ "testing": [ From 4e0707bbf006fe977a1dc36cf7069d1c5fdfb77a Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 4 Mar 2023 08:54:22 -0500 Subject: [PATCH 27/33] Add some initial tests for the validate hook. --- .../tests/validate-hook/example_module.py | 26 ++++++ .../tests/validate-hook/test_validate_hook.py | 89 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 numpydoc/tests/validate-hook/example_module.py create mode 100644 numpydoc/tests/validate-hook/test_validate_hook.py diff --git a/numpydoc/tests/validate-hook/example_module.py b/numpydoc/tests/validate-hook/example_module.py new file mode 100644 index 00000000..9f7b7094 --- /dev/null +++ b/numpydoc/tests/validate-hook/example_module.py @@ -0,0 +1,26 @@ +"""Test module for hook.""" # numpydoc ignore=ES01,SA01 + + +def some_function(name): + """Welcome to some function.""" + pass + + +class MyClass: + """This is MyClass.""" + + def __init__(self): + pass + + def __repr__(self): # numpydoc ignore=GL08 + pass + + def do_something(self, *args, **kwargs): + """ + Do something. + + Parameters + ---------- + *args + """ + pass diff --git a/numpydoc/tests/validate-hook/test_validate_hook.py b/numpydoc/tests/validate-hook/test_validate_hook.py new file mode 100644 index 00000000..10f189e7 --- /dev/null +++ b/numpydoc/tests/validate-hook/test_validate_hook.py @@ -0,0 +1,89 @@ +"""Test the numpydoc validate pre-commit hook.""" + +import inspect +from pathlib import Path + +import pytest + +from numpydoc.hooks.validate_docstrings import main + + +@pytest.fixture +def example_module(request): + fullpath = ( + Path(request.config.rootdir) + / "numpydoc" + / "tests" + / "validate-hook" + / "example_module.py" + ) + return str(fullpath.relative_to(request.config.rootdir)) + + +def test_validate_hook(example_module, capsys): + """Test that a file is correctly processed in the absence of config files.""" + + expected = inspect.cleandoc( + """ + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | file | item | check | description | + +===================================================+=====================================+=========+========================================+ + | numpydoc/tests/validate-hook/example_module.py:1 | example_module | EX01 | No examples section found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:3 | example_module.some_function | ES01 | No extended summary found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:3 | example_module.some_function | PR01 | Parameters {'name'} not documented | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:3 | example_module.some_function | SA01 | See Also section not found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:3 | example_module.some_function | EX01 | No examples section found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:7 | example_module.MyClass | ES01 | No extended summary found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:7 | example_module.MyClass | SA01 | See Also section not found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:7 | example_module.MyClass | EX01 | No examples section found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:10 | example_module.MyClass.__init__ | GL08 | The object does not have a docstring | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | ES01 | No extended summary found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | SA01 | See Also section not found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | EX01 | No examples section found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + """ + ) + + main([example_module]) + assert capsys.readouterr().err.rstrip() == expected + + +def test_validate_hook_with_ignore(example_module, capsys): + """ + Test that a file is correctly processed in the absence of config files + with command line ignore options. + """ + + expected = inspect.cleandoc( + """ + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | file | item | check | description | + +===================================================+=====================================+=========+========================================+ + | numpydoc/tests/validate-hook/example_module.py:3 | example_module.some_function | PR01 | Parameters {'name'} not documented | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:10 | example_module.MyClass.__init__ | GL08 | The object does not have a docstring | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ + """ + ) + + main([example_module, "--ignore", "ES01", "SA01", "EX01"]) + assert capsys.readouterr().err.rstrip() == expected From 97fc2dd17772c69e9e5dc640260b21eec6f3dc11 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 4 Mar 2023 12:44:09 -0500 Subject: [PATCH 28/33] Tweak docstring. --- numpydoc/hooks/validate_docstrings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 3b2552da..3bdd152d 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -33,7 +33,7 @@ class AstValidator(validate.Validator): ---------- ast_node : ast.AST The node under inspection. - filename : path-like + filename : os.PathLike The file where the node is defined. obj_name : str A name for the node to use in the listing of issues for the file as a whole. From 223ed2d845ac863c060fffc1ec8078f4d6c36dbd Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 4 Mar 2023 12:44:45 -0500 Subject: [PATCH 29/33] Add tests for hook using config files. --- .../tests/validate-hook/example_module.py | 4 + .../tests/validate-hook/test_validate_hook.py | 183 ++++++++++++++---- 2 files changed, 151 insertions(+), 36 deletions(-) diff --git a/numpydoc/tests/validate-hook/example_module.py b/numpydoc/tests/validate-hook/example_module.py index 9f7b7094..6cfa4d47 100644 --- a/numpydoc/tests/validate-hook/example_module.py +++ b/numpydoc/tests/validate-hook/example_module.py @@ -24,3 +24,7 @@ def do_something(self, *args, **kwargs): *args """ pass + + def process(self): + """Process stuff.""" + pass diff --git a/numpydoc/tests/validate-hook/test_validate_hook.py b/numpydoc/tests/validate-hook/test_validate_hook.py index 10f189e7..b5ba6b18 100644 --- a/numpydoc/tests/validate-hook/test_validate_hook.py +++ b/numpydoc/tests/validate-hook/test_validate_hook.py @@ -20,70 +20,181 @@ def example_module(request): return str(fullpath.relative_to(request.config.rootdir)) -def test_validate_hook(example_module, capsys): +@pytest.mark.parametrize("config", [None, "fake_dir"]) +def test_validate_hook(example_module, config, capsys): """Test that a file is correctly processed in the absence of config files.""" + expected = inspect.cleandoc( + """ + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | file | item | check | description | + +===================================================+=====================================+=========+====================================================+ + | numpydoc/tests/validate-hook/example_module.py:1 | example_module | EX01 | No examples section found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | ES01 | No extended summary found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | SA01 | See Also section not found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | EX01 | No examples section found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:9 | example_module.MyClass | ES01 | No extended summary found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:9 | example_module.MyClass | SA01 | See Also section not found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:9 | example_module.MyClass | EX01 | No examples section found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:12 | example_module.MyClass.__init__ | GL08 | The object does not have a docstring | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | ES01 | No extended summary found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | SA01 | See Also section not found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | EX01 | No examples section found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:28 | example_module.MyClass.process | SS05 | Summary must start with infinitive verb, not third | + | | | | person (e.g. use "Generate" instead of | + | | | | "Generates") | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:28 | example_module.MyClass.process | ES01 | No extended summary found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:28 | example_module.MyClass.process | SA01 | See Also section not found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:28 | example_module.MyClass.process | EX01 | No examples section found | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + """ + ) + + args = [example_module] + if config: + args.append(f"--{config=}") + + return_code = main(args) + assert return_code == 1 + assert capsys.readouterr().err.rstrip() == expected + + +def test_validate_hook_with_ignore(example_module, capsys): + """ + Test that a file is correctly processed in the absence of config files + with command line ignore options. + """ + + expected = inspect.cleandoc( + """ + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | file | item | check | description | + +===================================================+=====================================+=========+====================================================+ + | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:12 | example_module.MyClass.__init__ | GL08 | The object does not have a docstring | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/validate-hook/example_module.py:28 | example_module.MyClass.process | SS05 | Summary must start with infinitive verb, not third | + | | | | person (e.g. use "Generate" instead of | + | | | | "Generates") | + +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + """ + ) + + return_code = main([example_module, "--ignore", "ES01", "SA01", "EX01"]) + assert return_code == 1 + assert capsys.readouterr().err.rstrip() == expected + + +def test_validate_hook_with_toml_config(example_module, tmp_path, capsys): + """ + Test that a file is correctly processed with the config coming from + a pyproject.toml file. + """ + + with open(tmp_path / "pyproject.toml", "w") as config_file: + config_file.write( + inspect.cleandoc( + """ + [tool.numpydoc_validation] + ignore = [ + "EX01", + "SA01", + "ES01", + ] + override_SS05 = '^((Process|Assess|Access) )' + override_GL08 = '^(__init__)$' + """ + ) + ) + expected = inspect.cleandoc( """ +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ | file | item | check | description | +===================================================+=====================================+=========+========================================+ - | numpydoc/tests/validate-hook/example_module.py:1 | example_module | EX01 | No examples section found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:3 | example_module.some_function | ES01 | No extended summary found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:3 | example_module.some_function | PR01 | Parameters {'name'} not documented | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:3 | example_module.some_function | SA01 | See Also section not found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:3 | example_module.some_function | EX01 | No examples section found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:7 | example_module.MyClass | ES01 | No extended summary found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:7 | example_module.MyClass | SA01 | See Also section not found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:7 | example_module.MyClass | EX01 | No examples section found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:10 | example_module.MyClass.__init__ | GL08 | The object does not have a docstring | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | ES01 | No extended summary found | + | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | SA01 | See Also section not found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | EX01 | No examples section found | + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ """ ) - main([example_module]) + return_code = main([example_module, "--config", str(tmp_path)]) + assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected -def test_validate_hook_with_ignore(example_module, capsys): +def test_validate_hook_with_setup_cfg(example_module, tmp_path, capsys): """ - Test that a file is correctly processed in the absence of config files - with command line ignore options. + Test that a file is correctly processed with the config coming from + a setup.cfg file. """ + with open(tmp_path / "setup.cfg", "w") as config_file: + config_file.write( + inspect.cleandoc( + """ + [tool:numpydoc_validation] + ignore = EX01,SA01,ES01 + override_SS05 = ^((Process|Assess|Access) ) + override_GL08 = ^(__init__)$ + """ + ) + ) + expected = inspect.cleandoc( """ +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ | file | item | check | description | +===================================================+=====================================+=========+========================================+ - | numpydoc/tests/validate-hook/example_module.py:3 | example_module.some_function | PR01 | Parameters {'name'} not documented | + | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:10 | example_module.MyClass.__init__ | GL08 | The object does not have a docstring | + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:16 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | + | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ """ ) - main([example_module, "--ignore", "ES01", "SA01", "EX01"]) + return_code = main([example_module, "--config", str(tmp_path)]) + assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected + + +def test_validate_hook_help(capsys): + """Test that help section is displaying.""" + + with pytest.raises(SystemExit): + return_code = main(["--help"]) + assert return_code == 0 + + out = capsys.readouterr().out + assert "--ignore" in out + assert "--config" in out From f4663d346c102e7c1daae737a6d53b3bcdc2118c Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 4 Mar 2023 14:10:38 -0500 Subject: [PATCH 30/33] Add finding of project root. --- numpydoc/hooks/utils.py | 48 +++++ numpydoc/hooks/validate_docstrings.py | 30 +-- numpydoc/tests/hooks/__init__.py | 1 + .../example_module.py | 0 numpydoc/tests/hooks/test_utils.py | 34 +++ numpydoc/tests/hooks/test_validate_hook.py | 200 ++++++++++++++++++ .../tests/validate-hook/test_validate_hook.py | 200 ------------------ 7 files changed, 300 insertions(+), 213 deletions(-) create mode 100644 numpydoc/hooks/utils.py create mode 100644 numpydoc/tests/hooks/__init__.py rename numpydoc/tests/{validate-hook => hooks}/example_module.py (100%) create mode 100644 numpydoc/tests/hooks/test_utils.py create mode 100644 numpydoc/tests/hooks/test_validate_hook.py delete mode 100644 numpydoc/tests/validate-hook/test_validate_hook.py diff --git a/numpydoc/hooks/utils.py b/numpydoc/hooks/utils.py new file mode 100644 index 00000000..96bd08ad --- /dev/null +++ b/numpydoc/hooks/utils.py @@ -0,0 +1,48 @@ +"""Utility functions for pre-commit hooks.""" + +import itertools +import os +from pathlib import Path +from typing import Sequence + + +def find_project_root(srcs: Sequence[str]): + """ + Return a directory containing .git, .hg, pyproject.toml, or setup.cfg. + + That directory can be one of the directories passed in ``srcs`` or their + common parent. If no directory in the tree contains a marker that would + specify it's the project root, the root of the file system is returned. + + Parameters + ---------- + srcs : Sequence[str] + The filepaths to run the hook on. + + Returns + ------- + str + The project root directory. + + See Also + -------- + black.find_project_root : + This function was adapted from + `Black `_. + """ + if not srcs: + return Path(".").resolve(), "current directory" + + common_path = Path( + os.path.commonpath([Path(src).expanduser().resolve() for src in srcs]) + ) + + for dir in itertools.chain([common_path], common_path.parents): + if (dir / "pyproject.toml").is_file(): + return dir, "pyproject.toml" + if (dir / "setup.cfg").is_file(): + return dir, "setup.cfg" + if (dir / ".git").exists() or (dir / ".hg").is_dir(): + return dir, "version control" + + return dir, "file system root" diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 3bdd152d..9570ba18 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -19,6 +19,7 @@ from tabulate import tabulate from .. import docscrape, validate +from .utils import find_project_root # inline comments that can suppress individual checks per line @@ -252,7 +253,8 @@ def parse_config(dir_path: os.PathLike = None) -> dict: either a pyproject.toml file specifying a [tool.numpydoc_validation] section or a setup.cfg file specifying a [tool:numpydoc_validation] section. - For example, ``~/my_project``. + For example, ``~/my_project``. If not provided, the hook + will try to find the project root directory. Returns ------- @@ -260,7 +262,7 @@ def parse_config(dir_path: os.PathLike = None) -> dict: Config options for the numpydoc validation hook. """ options = {"exclusions": [], "overrides": {}} - dir_path = Path(dir_path or ".").expanduser().resolve() + dir_path = Path(dir_path).expanduser().resolve() toml_path = dir_path / "pyproject.toml" cfg_path = dir_path / "setup.cfg" @@ -348,7 +350,8 @@ def process_file(filepath: os.PathLike, config: dict) -> "list[list[str]]": def main(argv: Union[Sequence[str], None] = None) -> int: """Run the numpydoc validation hook.""" - config_options = parse_config() + project_root_from_cwd, config_file = find_project_root(["."]) + config_options = parse_config(project_root_from_cwd) ignored_checks = ( "\n " + "\n ".join( @@ -371,11 +374,12 @@ def main(argv: Union[Sequence[str], None] = None) -> int: "--config", type=str, help=( - "Path to a directory containing a pyproject.toml or setup.cfg file\n" - "if not in the current directory. If both are present, only\n" - "pyproject.toml will be used. Options must be placed under\n" - "[tool:numpydoc_validation] for setup.cfg files and\n" - "[tool.numpydoc_validation] for pyproject.toml files." + "Path to a directory containing a pyproject.toml or setup.cfg file.\n" + "The hook will look for it in the root project directory.\n" + "If both are present, only pyproject.toml will be used.\n" + "Options must be placed under\n" + " - [tool:numpydoc_validation] for setup.cfg files and\n" + " - [tool.numpydoc_validation] for pyproject.toml files." ), ) parser.add_argument( @@ -384,17 +388,17 @@ def main(argv: Union[Sequence[str], None] = None) -> int: nargs="*", help=( f"""Check codes to ignore.{ - ' Currently ignoring the following from setup.cfg:' - f'{ignored_checks}' - 'Values provided here will be in addition to the above.' + ' Currently ignoring the following from ' + f'{Path(project_root_from_cwd) / config_file}: {ignored_checks}' + 'Values provided here will be in addition to the above, unless an alternate config is provided.' if config_options["exclusions"] else '' }""" ), ) args = parser.parse_args(argv) - if args.config: # an alternative config file was provided - config_options = parse_config(args.config) + project_root, _ = find_project_root(args.files) + config_options = parse_config(args.config or project_root) config_options["exclusions"].extend(args.ignore or []) findings = [] diff --git a/numpydoc/tests/hooks/__init__.py b/numpydoc/tests/hooks/__init__.py new file mode 100644 index 00000000..f2d333a5 --- /dev/null +++ b/numpydoc/tests/hooks/__init__.py @@ -0,0 +1 @@ +"""Tests for hooks.""" diff --git a/numpydoc/tests/validate-hook/example_module.py b/numpydoc/tests/hooks/example_module.py similarity index 100% rename from numpydoc/tests/validate-hook/example_module.py rename to numpydoc/tests/hooks/example_module.py diff --git a/numpydoc/tests/hooks/test_utils.py b/numpydoc/tests/hooks/test_utils.py new file mode 100644 index 00000000..5db63762 --- /dev/null +++ b/numpydoc/tests/hooks/test_utils.py @@ -0,0 +1,34 @@ +"""Test utility functions for hooks.""" + +from pathlib import Path + +import pytest + +from numpydoc.hooks import utils + + +@pytest.mark.parametrize( + ["reason_file", "files", "expected_reason"], + [ + (None, None, "current directory"), + (None, ["x.py"], "file system root"), + (".git", ["x.py"], "version control"), + ("pyproject.toml", ["x.py"], "pyproject.toml"), + ("setup.cfg", ["x.py"], "setup.cfg"), + ], +) +def test_find_project_root(tmp_path, request, reason_file, files, expected_reason): + """Test the process of finding the project root.""" + if reason_file: + (tmp_path / reason_file).touch() + + if files: + expected_dir = Path("/") if expected_reason == "file system root" else tmp_path + for file in files: + (tmp_path / file).touch() + else: + expected_dir = request.config.rootdir + + root_dir, reason = utils.find_project_root(files if not files else [tmp_path]) + assert reason == expected_reason + assert root_dir == expected_dir diff --git a/numpydoc/tests/hooks/test_validate_hook.py b/numpydoc/tests/hooks/test_validate_hook.py new file mode 100644 index 00000000..7c8b8997 --- /dev/null +++ b/numpydoc/tests/hooks/test_validate_hook.py @@ -0,0 +1,200 @@ +"""Test the numpydoc validate pre-commit hook.""" + +import inspect +from pathlib import Path + +import pytest + +from numpydoc.hooks.validate_docstrings import main + + +@pytest.fixture +def example_module(request): + fullpath = ( + Path(request.config.rootdir) + / "numpydoc" + / "tests" + / "hooks" + / "example_module.py" + ) + return str(fullpath.relative_to(request.config.rootdir)) + + +@pytest.mark.parametrize("config", [None, "fake_dir"]) +def test_validate_hook(example_module, config, capsys): + """Test that a file is correctly processed in the absence of config files.""" + + expected = inspect.cleandoc( + """ + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | file | item | check | description | + +===========================================+=====================================+=========+====================================================+ + | numpydoc/tests/hooks/example_module.py:1 | example_module | EX01 | No examples section found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:4 | example_module.some_function | ES01 | No extended summary found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:4 | example_module.some_function | SA01 | See Also section not found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:4 | example_module.some_function | EX01 | No examples section found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:9 | example_module.MyClass | ES01 | No extended summary found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:9 | example_module.MyClass | SA01 | See Also section not found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:9 | example_module.MyClass | EX01 | No examples section found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:12 | example_module.MyClass.__init__ | GL08 | The object does not have a docstring | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | ES01 | No extended summary found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | SA01 | See Also section not found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | EX01 | No examples section found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:28 | example_module.MyClass.process | SS05 | Summary must start with infinitive verb, not third | + | | | | person (e.g. use "Generate" instead of | + | | | | "Generates") | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:28 | example_module.MyClass.process | ES01 | No extended summary found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:28 | example_module.MyClass.process | SA01 | See Also section not found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:28 | example_module.MyClass.process | EX01 | No examples section found | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + """ + ) + + args = [example_module] + if config: + args.append(f"--{config=}") + + return_code = main(args) + assert return_code == 1 + assert capsys.readouterr().err.rstrip() == expected + + +def test_validate_hook_with_ignore(example_module, capsys): + """ + Test that a file is correctly processed in the absence of config files + with command line ignore options. + """ + + expected = inspect.cleandoc( + """ + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | file | item | check | description | + +===========================================+=====================================+=========+====================================================+ + | numpydoc/tests/hooks/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:12 | example_module.MyClass.__init__ | GL08 | The object does not have a docstring | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + | numpydoc/tests/hooks/example_module.py:28 | example_module.MyClass.process | SS05 | Summary must start with infinitive verb, not third | + | | | | person (e.g. use "Generate" instead of | + | | | | "Generates") | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ + """ + ) + + return_code = main([example_module, "--ignore", "ES01", "SA01", "EX01"]) + assert return_code == 1 + assert capsys.readouterr().err.rstrip() == expected + + +def test_validate_hook_with_toml_config(example_module, tmp_path, capsys): + """ + Test that a file is correctly processed with the config coming from + a pyproject.toml file. + """ + + with open(tmp_path / "pyproject.toml", "w") as config_file: + config_file.write( + inspect.cleandoc( + """ + [tool.numpydoc_validation] + ignore = [ + "EX01", + "SA01", + "ES01", + ] + override_SS05 = '^((Process|Assess|Access) )' + override_GL08 = '^(__init__)$' + """ + ) + ) + + expected = inspect.cleandoc( + """ + +-------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | file | item | check | description | + +===========================================+=====================================+=========+========================================+ + | numpydoc/tests/hooks/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------+ + """ + ) + + return_code = main([example_module, "--config", str(tmp_path)]) + assert return_code == 1 + assert capsys.readouterr().err.rstrip() == expected + + +def test_validate_hook_with_setup_cfg(example_module, tmp_path, capsys): + """ + Test that a file is correctly processed with the config coming from + a setup.cfg file. + """ + + with open(tmp_path / "setup.cfg", "w") as config_file: + config_file.write( + inspect.cleandoc( + """ + [tool:numpydoc_validation] + ignore = EX01,SA01,ES01 + override_SS05 = ^((Process|Assess|Access) ) + override_GL08 = ^(__init__)$ + """ + ) + ) + + expected = inspect.cleandoc( + """ + +-------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | file | item | check | description | + +===========================================+=====================================+=========+========================================+ + | numpydoc/tests/hooks/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------+ + | numpydoc/tests/hooks/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | + +-------------------------------------------+-------------------------------------+---------+----------------------------------------+ + """ + ) + + return_code = main([example_module, "--config", str(tmp_path)]) + assert return_code == 1 + assert capsys.readouterr().err.rstrip() == expected + + +def test_validate_hook_help(capsys): + """Test that help section is displaying.""" + + with pytest.raises(SystemExit): + return_code = main(["--help"]) + assert return_code == 0 + + out = capsys.readouterr().out + assert "--ignore" in out + assert "--config" in out diff --git a/numpydoc/tests/validate-hook/test_validate_hook.py b/numpydoc/tests/validate-hook/test_validate_hook.py deleted file mode 100644 index b5ba6b18..00000000 --- a/numpydoc/tests/validate-hook/test_validate_hook.py +++ /dev/null @@ -1,200 +0,0 @@ -"""Test the numpydoc validate pre-commit hook.""" - -import inspect -from pathlib import Path - -import pytest - -from numpydoc.hooks.validate_docstrings import main - - -@pytest.fixture -def example_module(request): - fullpath = ( - Path(request.config.rootdir) - / "numpydoc" - / "tests" - / "validate-hook" - / "example_module.py" - ) - return str(fullpath.relative_to(request.config.rootdir)) - - -@pytest.mark.parametrize("config", [None, "fake_dir"]) -def test_validate_hook(example_module, config, capsys): - """Test that a file is correctly processed in the absence of config files.""" - - expected = inspect.cleandoc( - """ - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | file | item | check | description | - +===================================================+=====================================+=========+====================================================+ - | numpydoc/tests/validate-hook/example_module.py:1 | example_module | EX01 | No examples section found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | ES01 | No extended summary found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | SA01 | See Also section not found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | EX01 | No examples section found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:9 | example_module.MyClass | ES01 | No extended summary found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:9 | example_module.MyClass | SA01 | See Also section not found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:9 | example_module.MyClass | EX01 | No examples section found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:12 | example_module.MyClass.__init__ | GL08 | The object does not have a docstring | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | ES01 | No extended summary found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | SA01 | See Also section not found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | EX01 | No examples section found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:28 | example_module.MyClass.process | SS05 | Summary must start with infinitive verb, not third | - | | | | person (e.g. use "Generate" instead of | - | | | | "Generates") | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:28 | example_module.MyClass.process | ES01 | No extended summary found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:28 | example_module.MyClass.process | SA01 | See Also section not found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:28 | example_module.MyClass.process | EX01 | No examples section found | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - """ - ) - - args = [example_module] - if config: - args.append(f"--{config=}") - - return_code = main(args) - assert return_code == 1 - assert capsys.readouterr().err.rstrip() == expected - - -def test_validate_hook_with_ignore(example_module, capsys): - """ - Test that a file is correctly processed in the absence of config files - with command line ignore options. - """ - - expected = inspect.cleandoc( - """ - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | file | item | check | description | - +===================================================+=====================================+=========+====================================================+ - | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:12 | example_module.MyClass.__init__ | GL08 | The object does not have a docstring | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:28 | example_module.MyClass.process | SS05 | Summary must start with infinitive verb, not third | - | | | | person (e.g. use "Generate" instead of | - | | | | "Generates") | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------------------+ - """ - ) - - return_code = main([example_module, "--ignore", "ES01", "SA01", "EX01"]) - assert return_code == 1 - assert capsys.readouterr().err.rstrip() == expected - - -def test_validate_hook_with_toml_config(example_module, tmp_path, capsys): - """ - Test that a file is correctly processed with the config coming from - a pyproject.toml file. - """ - - with open(tmp_path / "pyproject.toml", "w") as config_file: - config_file.write( - inspect.cleandoc( - """ - [tool.numpydoc_validation] - ignore = [ - "EX01", - "SA01", - "ES01", - ] - override_SS05 = '^((Process|Assess|Access) )' - override_GL08 = '^(__init__)$' - """ - ) - ) - - expected = inspect.cleandoc( - """ - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | file | item | check | description | - +===================================================+=====================================+=========+========================================+ - | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - """ - ) - - return_code = main([example_module, "--config", str(tmp_path)]) - assert return_code == 1 - assert capsys.readouterr().err.rstrip() == expected - - -def test_validate_hook_with_setup_cfg(example_module, tmp_path, capsys): - """ - Test that a file is correctly processed with the config coming from - a setup.cfg file. - """ - - with open(tmp_path / "setup.cfg", "w") as config_file: - config_file.write( - inspect.cleandoc( - """ - [tool:numpydoc_validation] - ignore = EX01,SA01,ES01 - override_SS05 = ^((Process|Assess|Access) ) - override_GL08 = ^(__init__)$ - """ - ) - ) - - expected = inspect.cleandoc( - """ - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | file | item | check | description | - +===================================================+=====================================+=========+========================================+ - | numpydoc/tests/validate-hook/example_module.py:4 | example_module.some_function | PR01 | Parameters {'name'} not documented | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR01 | Parameters {'**kwargs'} not documented | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - | numpydoc/tests/validate-hook/example_module.py:18 | example_module.MyClass.do_something | PR07 | Parameter "*args" has no description | - +---------------------------------------------------+-------------------------------------+---------+----------------------------------------+ - """ - ) - - return_code = main([example_module, "--config", str(tmp_path)]) - assert return_code == 1 - assert capsys.readouterr().err.rstrip() == expected - - -def test_validate_hook_help(capsys): - """Test that help section is displaying.""" - - with pytest.raises(SystemExit): - return_code = main(["--help"]) - assert return_code == 0 - - out = capsys.readouterr().out - assert "--ignore" in out - assert "--config" in out From 583b6b19f486d56916d9d96c97c88d6c29d2cdd1 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 4 Mar 2023 14:28:47 -0500 Subject: [PATCH 31/33] Update link in docstring. --- numpydoc/hooks/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/hooks/utils.py b/numpydoc/hooks/utils.py index 96bd08ad..4f1d82aa 100644 --- a/numpydoc/hooks/utils.py +++ b/numpydoc/hooks/utils.py @@ -28,7 +28,7 @@ def find_project_root(srcs: Sequence[str]): -------- black.find_project_root : This function was adapted from - `Black `_. + `Black `_. """ if not srcs: return Path(".").resolve(), "current directory" From 281c04def07da3280b7a9847492d7fc87830610a Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 4 Mar 2023 14:34:27 -0500 Subject: [PATCH 32/33] Tweak code blocks in docs. --- doc/validation.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/validation.rst b/doc/validation.rst index 641d5647..f40d67b2 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -6,7 +6,9 @@ Docstring Validation using Pre-Commit Hook ------------------------------------------ To enable validation of docstrings as you commit files, add the -following to your ``.pre-commit-config.yaml`` file:: +following to your ``.pre-commit-config.yaml`` file: + +.. code-block:: yaml - repo: https://github.com/numpy/numpydoc rev: @@ -16,7 +18,7 @@ following to your ``.pre-commit-config.yaml`` file:: After installing ``numpydoc``, run the following to see available command line options for this hook: -.. code-block:: +.. code-block:: bash $ python -m numpydoc.hooks.validate_docstrings --help @@ -47,7 +49,9 @@ the class/method/function with name "__init__" to not have a docstring). override_GL08 = ^(__init__)$ For more fine-tuned control, you can also include inline comments to tell the -validation hook to ignore certain checks:: +validation hook to ignore certain checks: + +.. code-block:: python class SomeClass: # numpydoc ignore=EX01,SA01,ES01 """This is the docstring for SomeClass.""" From 6a1a6063c4d92d2acb9c7a078a00581097206bb0 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 4 Mar 2023 15:03:26 -0500 Subject: [PATCH 33/33] Shorten table to avoid scroll in docs. --- doc/validation.rst | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/doc/validation.rst b/doc/validation.rst index f40d67b2..09196ea5 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -60,20 +60,22 @@ validation hook to ignore certain checks: pass If any issues are found when commiting, a report is printed out and the -commit is halted:: +commit is halted: + +.. code-block:: output numpydoc-validation......................................................Failed - hook id: numpydoc-validation - exit code: 1 - +--------------------------+--------------------------+---------+--------------------------------------+ - | file | item | check | description | - +==========================+==========================+=========+======================================+ - | src/pkg/utils.py:1 | utils | GL08 | The object does not have a docstring | - | src/pkg/utils.py:90 | utils.normalize | PR04 | Parameter "field" has no type | - | src/pkg/module_one.py:12 | module_one.MyClass | GL08 | The object does not have a docstring | - | src/pkg/module_one.py:33 | module_one.MyClass.parse | RT03 | Return value has no description | - +--------------------------+--------------------------+---------+--------------------------------------+ + +----------------------+----------------------+---------+--------------------------------------+ + | file | item | check | description | + +======================+======================+=========+======================================+ + | src/pkg/utils.py:1 | utils | GL08 | The object does not have a docstring | + | src/pkg/utils.py:90 | utils.normalize | PR04 | Parameter "field" has no type | + | src/pkg/module.py:12 | module.MyClass | GL08 | The object does not have a docstring | + | src/pkg/module.py:33 | module.MyClass.parse | RT03 | Return value has no description | + +----------------------+----------------------+---------+--------------------------------------+ See below for a full listing of checks.