From 73f9a59131036ecafe82a1921611582941156b51 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:45:03 -0400 Subject: [PATCH 1/8] Combine CLIs into a single one, exposing as an entrypoint. --- .pre-commit-hooks.yaml | 2 +- numpydoc/__main__.py | 53 +-------------------- numpydoc/cli.py | 68 +++++++++++++++++++++++++++ numpydoc/hooks/validate_docstrings.py | 61 +++++++++++++++++------- pyproject.toml | 2 +- 5 files changed, 117 insertions(+), 69 deletions(-) create mode 100644 numpydoc/cli.py diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index b88e9ed2..2244d460 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,7 +1,7 @@ - id: numpydoc-validation name: numpydoc-validation description: This hook validates that docstrings in committed files adhere to numpydoc standards. - entry: validate-docstrings + entry: numpydoc lint require_serial: true language: python types: [python] diff --git a/numpydoc/__main__.py b/numpydoc/__main__.py index 53d5c504..b3b6d159 100644 --- a/numpydoc/__main__.py +++ b/numpydoc/__main__.py @@ -2,55 +2,6 @@ Implementing `python -m numpydoc` functionality. """ -import sys -import argparse -import ast +from .cli import main -from .docscrape_sphinx import get_doc_object -from .validate import validate, Validator - - -def render_object(import_path, config=None): - """Test numpydoc docstring generation for a given object""" - # TODO: Move Validator._load_obj to a better place than validate - print(get_doc_object(Validator._load_obj(import_path), config=dict(config or []))) - return 0 - - -def validate_object(import_path): - exit_status = 0 - results = validate(import_path) - for err_code, err_desc in results["errors"]: - exit_status += 1 - print(":".join([import_path, err_code, err_desc])) - return exit_status - - -if __name__ == "__main__": - ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument("import_path", help="e.g. numpy.ndarray") - - def _parse_config(s): - key, _, value = s.partition("=") - value = ast.literal_eval(value) - return key, value - - ap.add_argument( - "-c", - "--config", - type=_parse_config, - action="append", - help="key=val where val will be parsed by literal_eval, " - "e.g. -c use_plots=True. Multiple -c can be used.", - ) - ap.add_argument( - "--validate", action="store_true", help="validate the object and report errors" - ) - args = ap.parse_args() - - if args.validate: - exit_code = validate_object(args.import_path) - else: - exit_code = render_object(args.import_path, args.config) - - sys.exit(exit_code) +raise SystemExit(main()) diff --git a/numpydoc/cli.py b/numpydoc/cli.py new file mode 100644 index 00000000..6b75ed3f --- /dev/null +++ b/numpydoc/cli.py @@ -0,0 +1,68 @@ +"""The CLI for numpydoc.""" + +import argparse +import ast +from typing import Sequence, Union + +from .docscrape_sphinx import get_doc_object +from .hooks import validate_docstrings +from .validate import Validator, validate + + +def render_object(import_path: str, config: Union[list[str], None] = None) -> int: + """Test numpydoc docstring generation for a given object.""" + # TODO: Move Validator._load_obj to a better place than validate + print(get_doc_object(Validator._load_obj(import_path), config=dict(config or []))) + return 0 + + +def validate_object(import_path: str) -> int: + """Run numpydoc docstring validation for a given object.""" + exit_status = 0 + results = validate(import_path) + for err_code, err_desc in results["errors"]: + exit_status += 1 + print(":".join([import_path, err_code, err_desc])) + return exit_status + + +def main(argv: Union[Sequence[str], None] = None) -> int: + """CLI for numpydoc.""" + ap = argparse.ArgumentParser(prog="numpydoc", description=__doc__) + subparsers = ap.add_subparsers(title="subcommands") + + def _parse_config(s): + key, _, value = s.partition("=") + value = ast.literal_eval(value) + return key, value + + render = subparsers.add_parser( + "render", + description="Test numpydoc docstring generation for a given object.", + help="generate the docstring with numpydoc", + ) + render.add_argument("import_path", help="e.g. numpy.ndarray") + render.add_argument( + "-c", + "--config", + type=_parse_config, + action="append", + help="key=val where val will be parsed by literal_eval, " + "e.g. -c use_plots=True. Multiple -c can be used.", + ) + render.set_defaults(func=render_object) + + validate = subparsers.add_parser( + "validate", + description="Validate the docstring with numpydoc.", + help="validate the object and report errors", + ) + validate.add_argument("import_path", help="e.g. numpy.ndarray") + validate.set_defaults(func=validate_object) + + lint_parser = validate_docstrings.get_parser(parent=subparsers) + lint_parser.set_defaults(func=validate_docstrings.run_hook) + + args = vars(ap.parse_args(argv)) + func = args.pop("func", render_object) + return func(**args) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index db8141d8..4705c38f 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -13,7 +13,7 @@ import tomli as tomllib from pathlib import Path -from typing import Sequence, Tuple, Union +from typing import Any, Tuple, Union from tabulate import tabulate @@ -341,9 +341,7 @@ def process_file(filepath: os.PathLike, config: dict) -> "list[list[str]]": return docstring_visitor.findings -def main(argv: Union[Sequence[str], None] = None) -> int: - """Run the numpydoc validation hook.""" - +def get_parser(parent=None): project_root_from_cwd, config_file = find_project_root(["."]) config_options = parse_config(project_root_from_cwd) ignored_checks = ( @@ -357,10 +355,21 @@ def main(argv: Union[Sequence[str], None] = None) -> int: + "\n" ) - parser = argparse.ArgumentParser( - description="Run numpydoc validation on files with option to ignore individual checks.", - formatter_class=argparse.RawTextHelpFormatter, + description = ( + "Run numpydoc validation on files with option to ignore individual checks." ) + if parent is None: + parser = argparse.ArgumentParser( + description=description, + formatter_class=argparse.RawTextHelpFormatter, + ) + else: + parser = parent.add_parser( + "lint", + description=description, + help="validate all docstrings in file(s) using the abstract syntax tree", + ) + parser.add_argument( "files", type=str, nargs="+", help="File(s) to run numpydoc validation on." ) @@ -389,14 +398,38 @@ def main(argv: Union[Sequence[str], None] = None) -> int: }""" ), ) + return parser + + +def run_hook( + files: list[str], + *, + config: Union[dict[str, Any], None] = None, + ignore: Union[list[str], None] = None, +) -> int: + """ + Run the numpydoc validation hook. + + Parameters + ---------- + files : list[str] + The absolute or relative paths to the files to inspect. + config : Union[dict[str, Any], None], optional + Configuration options for reviewing flagged issues. + ignore : Union[list[str], None], optional + Checks to ignore in the results. - args = parser.parse_args(argv) - project_root, _ = find_project_root(args.files) - config_options = parse_config(args.config or project_root) - config_options["checks"] -= set(args.ignore or []) + Returns + ------- + int + The return status: 1 if issues were found, 0 otherwise. + """ + project_root, _ = find_project_root(files) + config_options = parse_config(config or project_root) + config_options["checks"] -= set(ignore or []) findings = [] - for file in args.files: + for file in files: findings.extend(process_file(file, config_options)) if findings: @@ -411,7 +444,3 @@ def main(argv: Union[Sequence[str], None] = None) -> int: ) return 1 return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/pyproject.toml b/pyproject.toml index de570e34..f9b2c7a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ test = [ ] [project.scripts] -validate-docstrings = 'numpydoc.hooks.validate_docstrings:main' +numpydoc = 'numpydoc.cli:main' [tool.changelist] title_template = "{version}" From d1d43c1a30f6a0abe32f394ef56c59cb73f0d854 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:48:41 -0400 Subject: [PATCH 2/8] Update tests for CLI changes --- numpydoc/tests/hooks/test_validate_hook.py | 22 +++++--- numpydoc/tests/test_main.py | 60 ++++++++++++++++------ 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/numpydoc/tests/hooks/test_validate_hook.py b/numpydoc/tests/hooks/test_validate_hook.py index 5c635dfb..7788661f 100644 --- a/numpydoc/tests/hooks/test_validate_hook.py +++ b/numpydoc/tests/hooks/test_validate_hook.py @@ -5,7 +5,7 @@ import pytest -from numpydoc.hooks.validate_docstrings import main +from numpydoc.hooks.validate_docstrings import get_parser, run_hook @pytest.fixture @@ -20,6 +20,11 @@ def example_module(request): return str(fullpath.relative_to(request.config.rootdir)) +def hook_main(args): + parser = get_parser() + return run_hook(**vars(parser.parse_args(args))) + + @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.""" @@ -76,7 +81,7 @@ def test_validate_hook(example_module, config, capsys): if config: args.append(f"--{config=}") - return_code = main(args) + return_code = hook_main(args) assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected @@ -109,7 +114,8 @@ def test_validate_hook_with_ignore(example_module, capsys): """ ) - return_code = main([example_module, "--ignore", "ES01", "SA01", "EX01"]) + return_code = hook_main([example_module, "--ignore", "ES01", "SA01", "EX01"]) + assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected @@ -157,7 +163,7 @@ def test_validate_hook_with_toml_config(example_module, tmp_path, capsys): """ ) - return_code = main([example_module, "--config", str(tmp_path)]) + return_code = hook_main([example_module, "--config", str(tmp_path)]) assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected @@ -196,7 +202,7 @@ def test_validate_hook_with_setup_cfg(example_module, tmp_path, capsys): """ ) - return_code = main([example_module, "--config", str(tmp_path)]) + return_code = hook_main([example_module, "--config", str(tmp_path)]) assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected @@ -205,7 +211,7 @@ def test_validate_hook_help(capsys): """Test that help section is displaying.""" with pytest.raises(SystemExit): - return_code = main(["--help"]) + return_code = hook_main(["--help"]) assert return_code == 0 out = capsys.readouterr().out @@ -255,7 +261,7 @@ def test_validate_hook_exclude_option_pyproject(example_module, tmp_path, capsys """ ) - return_code = main([example_module, "--config", str(tmp_path)]) + return_code = hook_main([example_module, "--config", str(tmp_path)]) assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected @@ -292,6 +298,6 @@ def test_validate_hook_exclude_option_setup_cfg(example_module, tmp_path, capsys """ ) - return_code = main([example_module, "--config", str(tmp_path)]) + return_code = hook_main([example_module, "--config", str(tmp_path)]) assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected diff --git a/numpydoc/tests/test_main.py b/numpydoc/tests/test_main.py index 1f90b967..da2016fe 100644 --- a/numpydoc/tests/test_main.py +++ b/numpydoc/tests/test_main.py @@ -1,8 +1,11 @@ -import sys +import inspect import io +import sys + import pytest + import numpydoc -import numpydoc.__main__ +import numpydoc.cli def _capture_stdout(func_name, *args, **kwargs): @@ -65,41 +68,40 @@ def _invalid_docstring(): def test_renders_package_docstring(): - out = _capture_stdout(numpydoc.__main__.render_object, "numpydoc") + out = _capture_stdout(numpydoc.cli.render_object, "numpydoc") assert out.startswith("This package provides the numpydoc Sphinx") -def test_renders_module_docstring(): - out = _capture_stdout(numpydoc.__main__.render_object, "numpydoc.__main__") - assert out.startswith("Implementing `python -m numpydoc` functionality.") +def test_renders_module_docstring(capsys): + numpydoc.cli.main(["render", "numpydoc.cli"]) + out = capsys.readouterr().out.strip("\n\r") + assert out.startswith(numpydoc.cli.__doc__) def test_renders_function_docstring(): out = _capture_stdout( - numpydoc.__main__.render_object, "numpydoc.tests.test_main._capture_stdout" + numpydoc.cli.render_object, "numpydoc.tests.test_main._capture_stdout" ) assert out.startswith("Return stdout of calling") def test_render_object_returns_correct_exit_status(): - exit_status = numpydoc.__main__.render_object( - "numpydoc.tests.test_main._capture_stdout" - ) + exit_status = numpydoc.cli.render_object("numpydoc.tests.test_main._capture_stdout") assert exit_status == 0 with pytest.raises(ValueError): - numpydoc.__main__.render_object("numpydoc.tests.test_main._invalid_docstring") + numpydoc.cli.render_object("numpydoc.tests.test_main._invalid_docstring") def test_validate_detects_errors(): out = _capture_stdout( - numpydoc.__main__.validate_object, + numpydoc.cli.validate_object, "numpydoc.tests.test_main._docstring_with_errors", ) assert "SS02" in out assert "Summary does not start with a capital letter" in out - exit_status = numpydoc.__main__.validate_object( + exit_status = numpydoc.cli.validate_object( "numpydoc.tests.test_main._docstring_with_errors" ) assert exit_status > 0 @@ -107,11 +109,39 @@ def test_validate_detects_errors(): def test_validate_perfect_docstring(): out = _capture_stdout( - numpydoc.__main__.validate_object, "numpydoc.tests.test_main._capture_stdout" + numpydoc.cli.validate_object, "numpydoc.tests.test_main._capture_stdout" ) assert out == "" - exit_status = numpydoc.__main__.validate_object( + exit_status = numpydoc.cli.validate_object( "numpydoc.tests.test_main._capture_stdout" ) assert exit_status == 0 + + +@pytest.mark.parametrize("args", [[], ["--ignore", "ES01", "SA01", "EX01"]]) +def test_lint(capsys, args): + argv = ["lint", "numpydoc/__main__.py"] + args + if args: + expected = "" + expected_status = 0 + else: + expected = inspect.cleandoc( + """ + +------------------------+----------+---------+----------------------------+ + | file | item | check | description | + +========================+==========+=========+============================+ + | numpydoc/__main__.py:1 | __main__ | ES01 | No extended summary found | + +------------------------+----------+---------+----------------------------+ + | numpydoc/__main__.py:1 | __main__ | SA01 | See Also section not found | + +------------------------+----------+---------+----------------------------+ + | numpydoc/__main__.py:1 | __main__ | EX01 | No examples section found | + +------------------------+----------+---------+----------------------------+ + """ + ) + expected_status = 1 + + return_status = numpydoc.cli.main(argv) + err = capsys.readouterr().err.strip("\n\r") + assert err == expected + assert return_status == expected_status From 02f5e5dfab11f73675fee6a3c150ad5a81b7f35a Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:50:29 -0400 Subject: [PATCH 3/8] Update python -m references in the docs --- doc/validation.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/validation.rst b/doc/validation.rst index 4ce89017..858a67cc 100644 --- a/doc/validation.rst +++ b/doc/validation.rst @@ -22,7 +22,7 @@ command line options for this hook: .. code-block:: bash - $ python -m numpydoc.hooks.validate_docstrings --help + $ numpydoc lint --help Using a config file provides additional customization. Both ``pyproject.toml`` and ``setup.cfg`` are supported; however, if the project contains both @@ -102,12 +102,12 @@ can be called. For example, to do it for ``numpy.ndarray``, use: .. code-block:: bash - $ python -m numpydoc numpy.ndarray + $ numpydoc validate numpy.ndarray This will validate that the docstring can be built. For an exhaustive validation of the formatting of the docstring, use the -``--validate`` parameter. This will report the errors detected, such as +``validate`` subcommand. This will report the errors detected, such as incorrect capitalization, wrong order of the sections, and many other issues. Note that this will honor :ref:`inline ignore comments `, but will not look for any configuration like the :ref:`pre-commit hook ` From 007c2caaf3caee0eb99d15ef8c651c15d4c27a25 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:55:14 -0400 Subject: [PATCH 4/8] Update test.yml workflow --- .github/workflows/test.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 98b4d79b..405e2829 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,10 +50,10 @@ jobs: - name: Make sure CLI works run: | - python -m numpydoc numpydoc.tests.test_main._capture_stdout - echo '! python -m numpydoc numpydoc.tests.test_main._invalid_docstring' | bash - python -m numpydoc --validate numpydoc.tests.test_main._capture_stdout - echo '! python -m numpydoc --validate numpydoc.tests.test_main._docstring_with_errors' | bash + numpydoc render numpydoc.tests.test_main._capture_stdout + echo '! numpydoc render numpydoc.tests.test_main._invalid_docstring' | bash + numpydoc validate numpydoc.tests.test_main._capture_stdout + echo '! numpydoc validate numpydoc.tests.test_main._docstring_with_errors' | bash - name: Setup for doc build run: | @@ -101,10 +101,10 @@ jobs: - name: Make sure CLI works run: | - python -m numpydoc numpydoc.tests.test_main._capture_stdout - echo '! python -m numpydoc numpydoc.tests.test_main._invalid_docstring' | bash - python -m numpydoc --validate numpydoc.tests.test_main._capture_stdout - echo '! python -m numpydoc --validate numpydoc.tests.test_main._docstring_with_errors' | bash + numpydoc render numpydoc.tests.test_main._capture_stdout + echo '! numpydoc render numpydoc.tests.test_main._invalid_docstring' | bash + numpydoc validate numpydoc.tests.test_main._capture_stdout + echo '! numpydoc validate numpydoc.tests.test_main._docstring_with_errors' | bash - name: Setup for doc build run: | From f10e786d235baf4dc387e66b5ac5ceccbdc7e079 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sat, 30 Mar 2024 14:08:36 -0400 Subject: [PATCH 5/8] Use older type annotations --- numpydoc/cli.py | 4 ++-- numpydoc/hooks/validate_docstrings.py | 25 ++++++++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/numpydoc/cli.py b/numpydoc/cli.py index 6b75ed3f..4375328d 100644 --- a/numpydoc/cli.py +++ b/numpydoc/cli.py @@ -2,14 +2,14 @@ import argparse import ast -from typing import Sequence, Union +from typing import List, Sequence, Union from .docscrape_sphinx import get_doc_object from .hooks import validate_docstrings from .validate import Validator, validate -def render_object(import_path: str, config: Union[list[str], None] = None) -> int: +def render_object(import_path: str, config: Union[List[str], None] = None) -> int: """Test numpydoc docstring generation for a given object.""" # TODO: Move Validator._load_obj to a better place than validate print(get_doc_object(Validator._load_obj(import_path), config=dict(config or []))) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index 4705c38f..d0dbb011 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -13,7 +13,7 @@ import tomli as tomllib from pathlib import Path -from typing import Any, Tuple, Union +from typing import Any, Dict, List, Tuple, Union from tabulate import tabulate @@ -341,7 +341,22 @@ def process_file(filepath: os.PathLike, config: dict) -> "list[list[str]]": return docstring_visitor.findings -def get_parser(parent=None): +def get_parser( + parent: Union[argparse.ArgumentParser, None] = None, +) -> argparse.ArgumentParser: + """ + Build an argument parser. + + Parameters + ---------- + parent : argparse.ArgumentParser + A parent parser to use, if this should be a subparser. + + Returns + ------- + argparse.ArgumentParser + The argument parser. + """ project_root_from_cwd, config_file = find_project_root(["."]) config_options = parse_config(project_root_from_cwd) ignored_checks = ( @@ -402,10 +417,10 @@ def get_parser(parent=None): def run_hook( - files: list[str], + files: List[str], *, - config: Union[dict[str, Any], None] = None, - ignore: Union[list[str], None] = None, + config: Union[Dict[str, Any], None] = None, + ignore: Union[List[str], None] = None, ) -> int: """ Run the numpydoc validation hook. From e87a4d1a5b497921de2eb05a0c8f25cce7b62058 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 29 Apr 2024 18:44:16 -0400 Subject: [PATCH 6/8] Show help when no subcommand is provided --- numpydoc/cli.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/numpydoc/cli.py b/numpydoc/cli.py index 4375328d..b586f771 100644 --- a/numpydoc/cli.py +++ b/numpydoc/cli.py @@ -64,5 +64,8 @@ def _parse_config(s): lint_parser.set_defaults(func=validate_docstrings.run_hook) args = vars(ap.parse_args(argv)) - func = args.pop("func", render_object) - return func(**args) + try: + func = args.pop("func") + return func(**args) + except KeyError: + ap.exit(status=2, message=ap.format_help()) From ec69d5e4aeef71650ae2528fbaa1ae77205a2ceb Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:31:25 -0400 Subject: [PATCH 7/8] Refactor lint subparser and hook tests --- numpydoc/cli.py | 75 ++++++++++++++++++--- numpydoc/hooks/validate_docstrings.py | 76 ---------------------- numpydoc/tests/hooks/test_validate_hook.py | 35 ++-------- numpydoc/tests/test_main.py | 12 ++++ 4 files changed, 85 insertions(+), 113 deletions(-) diff --git a/numpydoc/cli.py b/numpydoc/cli.py index b586f771..de47d51d 100644 --- a/numpydoc/cli.py +++ b/numpydoc/cli.py @@ -2,11 +2,12 @@ import argparse import ast +from pathlib import Path from typing import List, Sequence, Union from .docscrape_sphinx import get_doc_object -from .hooks import validate_docstrings -from .validate import Validator, validate +from .hooks import utils, validate_docstrings +from .validate import ERROR_MSGS, Validator, validate def render_object(import_path: str, config: Union[List[str], None] = None) -> int: @@ -26,8 +27,15 @@ def validate_object(import_path: str) -> int: return exit_status -def main(argv: Union[Sequence[str], None] = None) -> int: - """CLI for numpydoc.""" +def get_parser() -> argparse.ArgumentParser: + """ + Build an argument parser. + + Returns + ------- + argparse.ArgumentParser + The argument parser. + """ ap = argparse.ArgumentParser(prog="numpydoc", description=__doc__) subparsers = ap.add_subparsers(title="subcommands") @@ -38,8 +46,8 @@ def _parse_config(s): render = subparsers.add_parser( "render", - description="Test numpydoc docstring generation for a given object.", - help="generate the docstring with numpydoc", + description="Generate an expanded RST-version of the docstring.", + help="generate the RST docstring with numpydoc", ) render.add_argument("import_path", help="e.g. numpy.ndarray") render.add_argument( @@ -54,16 +62,65 @@ def _parse_config(s): validate = subparsers.add_parser( "validate", - description="Validate the docstring with numpydoc.", - help="validate the object and report errors", + description="Validate an object's docstring against the numpydoc standard.", + help="validate the object's docstring and report errors", ) validate.add_argument("import_path", help="e.g. numpy.ndarray") validate.set_defaults(func=validate_object) - lint_parser = validate_docstrings.get_parser(parent=subparsers) + project_root_from_cwd, config_file = utils.find_project_root(["."]) + config_options = validate_docstrings.parse_config(project_root_from_cwd) + ignored_checks = [ + f"- {check}: {ERROR_MSGS[check]}" + for check in set(ERROR_MSGS.keys()) - config_options["checks"] + ] + ignored_checks_text = "\n " + "\n ".join(ignored_checks) + "\n" + + lint_parser = subparsers.add_parser( + "lint", + description="Run numpydoc validation on files with option to ignore individual checks.", + help="validate all docstrings in file(s) using the abstract syntax tree", + formatter_class=argparse.RawTextHelpFormatter, + ) + lint_parser.add_argument( + "files", type=str, nargs="+", help="File(s) to run numpydoc validation on." + ) + lint_parser.add_argument( + "--config", + type=str, + help=( + "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." + ), + ) + lint_parser.add_argument( + "--ignore", + type=str, + nargs="*", + help=( + f"""Check codes to ignore.{ + ' Currently ignoring the following from ' + f'{Path(project_root_from_cwd) / config_file}: {ignored_checks_text}' + 'Values provided here will be in addition to the above, unless an alternate config is provided.' + if ignored_checks else '' + }""" + ), + ) lint_parser.set_defaults(func=validate_docstrings.run_hook) + return ap + + +def main(argv: Union[Sequence[str], None] = None) -> int: + """CLI for numpydoc.""" + ap = get_parser() + args = vars(ap.parse_args(argv)) + try: func = args.pop("func") return func(**args) diff --git a/numpydoc/hooks/validate_docstrings.py b/numpydoc/hooks/validate_docstrings.py index d0dbb011..562a1f09 100644 --- a/numpydoc/hooks/validate_docstrings.py +++ b/numpydoc/hooks/validate_docstrings.py @@ -1,6 +1,5 @@ """Run numpydoc validation on contents of a file.""" -import argparse import ast import configparser import os @@ -341,81 +340,6 @@ def process_file(filepath: os.PathLike, config: dict) -> "list[list[str]]": return docstring_visitor.findings -def get_parser( - parent: Union[argparse.ArgumentParser, None] = None, -) -> argparse.ArgumentParser: - """ - Build an argument parser. - - Parameters - ---------- - parent : argparse.ArgumentParser - A parent parser to use, if this should be a subparser. - - Returns - ------- - argparse.ArgumentParser - The argument parser. - """ - project_root_from_cwd, config_file = find_project_root(["."]) - config_options = parse_config(project_root_from_cwd) - ignored_checks = ( - "\n " - + "\n ".join( - [ - f"- {check}: {validate.ERROR_MSGS[check]}" - for check in set(validate.ERROR_MSGS.keys()) - config_options["checks"] - ] - ) - + "\n" - ) - - description = ( - "Run numpydoc validation on files with option to ignore individual checks." - ) - if parent is None: - parser = argparse.ArgumentParser( - description=description, - formatter_class=argparse.RawTextHelpFormatter, - ) - else: - parser = parent.add_parser( - "lint", - description=description, - help="validate all docstrings in file(s) using the abstract syntax tree", - ) - - 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 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( - "--ignore", - type=str, - nargs="*", - help=( - f"""Check codes to ignore.{ - ' 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["checks"] else '' - }""" - ), - ) - return parser - - def run_hook( files: List[str], *, diff --git a/numpydoc/tests/hooks/test_validate_hook.py b/numpydoc/tests/hooks/test_validate_hook.py index 7788661f..4e79f506 100644 --- a/numpydoc/tests/hooks/test_validate_hook.py +++ b/numpydoc/tests/hooks/test_validate_hook.py @@ -5,7 +5,7 @@ import pytest -from numpydoc.hooks.validate_docstrings import get_parser, run_hook +from numpydoc.hooks.validate_docstrings import run_hook @pytest.fixture @@ -20,11 +20,6 @@ def example_module(request): return str(fullpath.relative_to(request.config.rootdir)) -def hook_main(args): - parser = get_parser() - return run_hook(**vars(parser.parse_args(args))) - - @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.""" @@ -77,11 +72,7 @@ def test_validate_hook(example_module, config, capsys): """ ) - args = [example_module] - if config: - args.append(f"--{config=}") - - return_code = hook_main(args) + return_code = run_hook([example_module], config=config) assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected @@ -114,7 +105,7 @@ def test_validate_hook_with_ignore(example_module, capsys): """ ) - return_code = hook_main([example_module, "--ignore", "ES01", "SA01", "EX01"]) + return_code = run_hook([example_module], ignore=["ES01", "SA01", "EX01"]) assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected @@ -163,7 +154,7 @@ def test_validate_hook_with_toml_config(example_module, tmp_path, capsys): """ ) - return_code = hook_main([example_module, "--config", str(tmp_path)]) + return_code = run_hook([example_module], config=tmp_path) assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected @@ -202,23 +193,11 @@ def test_validate_hook_with_setup_cfg(example_module, tmp_path, capsys): """ ) - return_code = hook_main([example_module, "--config", str(tmp_path)]) + return_code = run_hook([example_module], config=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 = hook_main(["--help"]) - assert return_code == 0 - - out = capsys.readouterr().out - assert "--ignore" in out - assert "--config" in out - - def test_validate_hook_exclude_option_pyproject(example_module, tmp_path, capsys): """ Test that a file is correctly processed with the config coming from @@ -261,7 +240,7 @@ def test_validate_hook_exclude_option_pyproject(example_module, tmp_path, capsys """ ) - return_code = hook_main([example_module, "--config", str(tmp_path)]) + return_code = run_hook([example_module], config=tmp_path) assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected @@ -298,6 +277,6 @@ def test_validate_hook_exclude_option_setup_cfg(example_module, tmp_path, capsys """ ) - return_code = hook_main([example_module, "--config", str(tmp_path)]) + return_code = run_hook([example_module], config=tmp_path) assert return_code == 1 assert capsys.readouterr().err.rstrip() == expected diff --git a/numpydoc/tests/test_main.py b/numpydoc/tests/test_main.py index da2016fe..e9bc2fa5 100644 --- a/numpydoc/tests/test_main.py +++ b/numpydoc/tests/test_main.py @@ -145,3 +145,15 @@ def test_lint(capsys, args): err = capsys.readouterr().err.strip("\n\r") assert err == expected assert return_status == expected_status + + +def test_validate_hook_help(capsys): + """Test that help section is displaying.""" + + with pytest.raises(SystemExit): + return_code = numpydoc.cli.main(["lint", "--help"]) + assert return_code == 0 + + out = capsys.readouterr().out + assert "--ignore" in out + assert "--config" in out From 66400a37253ca66118b81e9c45a8bc9903c3700f Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:41:17 -0400 Subject: [PATCH 8/8] Rename test to match subcommand --- numpydoc/tests/test_main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpydoc/tests/test_main.py b/numpydoc/tests/test_main.py index e9bc2fa5..8023e1c9 100644 --- a/numpydoc/tests/test_main.py +++ b/numpydoc/tests/test_main.py @@ -147,8 +147,8 @@ def test_lint(capsys, args): assert return_status == expected_status -def test_validate_hook_help(capsys): - """Test that help section is displaying.""" +def test_lint_help(capsys): + """Test that lint help section is displaying.""" with pytest.raises(SystemExit): return_code = numpydoc.cli.main(["lint", "--help"])