Skip to content

Commit e7c6baf

Browse files
authored
Unify CLIs (#537)
* Combine CLIs into a single one, exposing as an entrypoint. * Update tests for CLI changes * Update python -m references in the docs * Update test.yml workflow * Use older type annotations * Show help when no subcommand is provided * Refactor lint subparser and hook tests * Rename test to match subcommand
1 parent 3a21e54 commit e7c6baf

File tree

9 files changed

+235
-161
lines changed

9 files changed

+235
-161
lines changed

.github/workflows/test.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ jobs:
5757
5858
- name: Make sure CLI works
5959
run: |
60-
python -m numpydoc numpydoc.tests.test_main._capture_stdout
61-
echo '! python -m numpydoc numpydoc.tests.test_main._invalid_docstring' | bash
62-
python -m numpydoc --validate numpydoc.tests.test_main._capture_stdout
63-
echo '! python -m numpydoc --validate numpydoc.tests.test_main._docstring_with_errors' | bash
60+
numpydoc render numpydoc.tests.test_main._capture_stdout
61+
echo '! numpydoc render numpydoc.tests.test_main._invalid_docstring' | bash
62+
numpydoc validate numpydoc.tests.test_main._capture_stdout
63+
echo '! numpydoc validate numpydoc.tests.test_main._docstring_with_errors' | bash
6464
6565
- name: Setup for doc build
6666
run: |
@@ -110,10 +110,10 @@ jobs:
110110
111111
- name: Make sure CLI works
112112
run: |
113-
python -m numpydoc numpydoc.tests.test_main._capture_stdout
114-
echo '! python -m numpydoc numpydoc.tests.test_main._invalid_docstring' | bash
115-
python -m numpydoc --validate numpydoc.tests.test_main._capture_stdout
116-
echo '! python -m numpydoc --validate numpydoc.tests.test_main._docstring_with_errors' | bash
113+
numpydoc render numpydoc.tests.test_main._capture_stdout
114+
echo '! numpydoc render numpydoc.tests.test_main._invalid_docstring' | bash
115+
numpydoc validate numpydoc.tests.test_main._capture_stdout
116+
echo '! numpydoc validate numpydoc.tests.test_main._docstring_with_errors' | bash
117117
118118
- name: Setup for doc build
119119
run: |

.pre-commit-hooks.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
- id: numpydoc-validation
22
name: numpydoc-validation
33
description: This hook validates that docstrings in committed files adhere to numpydoc standards.
4-
entry: validate-docstrings
4+
entry: numpydoc lint
55
require_serial: true
66
language: python
77
types: [python]

doc/validation.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ command line options for this hook:
2222

2323
.. code-block:: bash
2424
25-
$ python -m numpydoc.hooks.validate_docstrings --help
25+
$ numpydoc lint --help
2626
2727
Using a config file provides additional customization. Both ``pyproject.toml``
2828
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:
102102

103103
.. code-block:: bash
104104
105-
$ python -m numpydoc numpy.ndarray
105+
$ numpydoc validate numpy.ndarray
106106
107107
This will validate that the docstring can be built.
108108

109109
For an exhaustive validation of the formatting of the docstring, use the
110-
``--validate`` parameter. This will report the errors detected, such as
110+
``validate`` subcommand. This will report the errors detected, such as
111111
incorrect capitalization, wrong order of the sections, and many other
112112
issues. Note that this will honor :ref:`inline ignore comments <inline_ignore_comments>`,
113113
but will not look for any configuration like the :ref:`pre-commit hook <pre_commit_hook>`

numpydoc/__main__.py

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,6 @@
22
Implementing `python -m numpydoc` functionality.
33
"""
44

5-
import sys
6-
import argparse
7-
import ast
5+
from .cli import main
86

9-
from .docscrape_sphinx import get_doc_object
10-
from .validate import validate, Validator
11-
12-
13-
def render_object(import_path, config=None):
14-
"""Test numpydoc docstring generation for a given object"""
15-
# TODO: Move Validator._load_obj to a better place than validate
16-
print(get_doc_object(Validator._load_obj(import_path), config=dict(config or [])))
17-
return 0
18-
19-
20-
def validate_object(import_path):
21-
exit_status = 0
22-
results = validate(import_path)
23-
for err_code, err_desc in results["errors"]:
24-
exit_status += 1
25-
print(":".join([import_path, err_code, err_desc]))
26-
return exit_status
27-
28-
29-
if __name__ == "__main__":
30-
ap = argparse.ArgumentParser(description=__doc__)
31-
ap.add_argument("import_path", help="e.g. numpy.ndarray")
32-
33-
def _parse_config(s):
34-
key, _, value = s.partition("=")
35-
value = ast.literal_eval(value)
36-
return key, value
37-
38-
ap.add_argument(
39-
"-c",
40-
"--config",
41-
type=_parse_config,
42-
action="append",
43-
help="key=val where val will be parsed by literal_eval, "
44-
"e.g. -c use_plots=True. Multiple -c can be used.",
45-
)
46-
ap.add_argument(
47-
"--validate", action="store_true", help="validate the object and report errors"
48-
)
49-
args = ap.parse_args()
50-
51-
if args.validate:
52-
exit_code = validate_object(args.import_path)
53-
else:
54-
exit_code = render_object(args.import_path, args.config)
55-
56-
sys.exit(exit_code)
7+
raise SystemExit(main())

numpydoc/cli.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""The CLI for numpydoc."""
2+
3+
import argparse
4+
import ast
5+
from pathlib import Path
6+
from typing import List, Sequence, Union
7+
8+
from .docscrape_sphinx import get_doc_object
9+
from .hooks import utils, validate_docstrings
10+
from .validate import ERROR_MSGS, Validator, validate
11+
12+
13+
def render_object(import_path: str, config: Union[List[str], None] = None) -> int:
14+
"""Test numpydoc docstring generation for a given object."""
15+
# TODO: Move Validator._load_obj to a better place than validate
16+
print(get_doc_object(Validator._load_obj(import_path), config=dict(config or [])))
17+
return 0
18+
19+
20+
def validate_object(import_path: str) -> int:
21+
"""Run numpydoc docstring validation for a given object."""
22+
exit_status = 0
23+
results = validate(import_path)
24+
for err_code, err_desc in results["errors"]:
25+
exit_status += 1
26+
print(":".join([import_path, err_code, err_desc]))
27+
return exit_status
28+
29+
30+
def get_parser() -> argparse.ArgumentParser:
31+
"""
32+
Build an argument parser.
33+
34+
Returns
35+
-------
36+
argparse.ArgumentParser
37+
The argument parser.
38+
"""
39+
ap = argparse.ArgumentParser(prog="numpydoc", description=__doc__)
40+
subparsers = ap.add_subparsers(title="subcommands")
41+
42+
def _parse_config(s):
43+
key, _, value = s.partition("=")
44+
value = ast.literal_eval(value)
45+
return key, value
46+
47+
render = subparsers.add_parser(
48+
"render",
49+
description="Generate an expanded RST-version of the docstring.",
50+
help="generate the RST docstring with numpydoc",
51+
)
52+
render.add_argument("import_path", help="e.g. numpy.ndarray")
53+
render.add_argument(
54+
"-c",
55+
"--config",
56+
type=_parse_config,
57+
action="append",
58+
help="key=val where val will be parsed by literal_eval, "
59+
"e.g. -c use_plots=True. Multiple -c can be used.",
60+
)
61+
render.set_defaults(func=render_object)
62+
63+
validate = subparsers.add_parser(
64+
"validate",
65+
description="Validate an object's docstring against the numpydoc standard.",
66+
help="validate the object's docstring and report errors",
67+
)
68+
validate.add_argument("import_path", help="e.g. numpy.ndarray")
69+
validate.set_defaults(func=validate_object)
70+
71+
project_root_from_cwd, config_file = utils.find_project_root(["."])
72+
config_options = validate_docstrings.parse_config(project_root_from_cwd)
73+
ignored_checks = [
74+
f"- {check}: {ERROR_MSGS[check]}"
75+
for check in set(ERROR_MSGS.keys()) - config_options["checks"]
76+
]
77+
ignored_checks_text = "\n " + "\n ".join(ignored_checks) + "\n"
78+
79+
lint_parser = subparsers.add_parser(
80+
"lint",
81+
description="Run numpydoc validation on files with option to ignore individual checks.",
82+
help="validate all docstrings in file(s) using the abstract syntax tree",
83+
formatter_class=argparse.RawTextHelpFormatter,
84+
)
85+
lint_parser.add_argument(
86+
"files", type=str, nargs="+", help="File(s) to run numpydoc validation on."
87+
)
88+
lint_parser.add_argument(
89+
"--config",
90+
type=str,
91+
help=(
92+
"Path to a directory containing a pyproject.toml or setup.cfg file.\n"
93+
"The hook will look for it in the root project directory.\n"
94+
"If both are present, only pyproject.toml will be used.\n"
95+
"Options must be placed under\n"
96+
" - [tool:numpydoc_validation] for setup.cfg files and\n"
97+
" - [tool.numpydoc_validation] for pyproject.toml files."
98+
),
99+
)
100+
lint_parser.add_argument(
101+
"--ignore",
102+
type=str,
103+
nargs="*",
104+
help=(
105+
f"""Check codes to ignore.{
106+
' Currently ignoring the following from '
107+
f'{Path(project_root_from_cwd) / config_file}: {ignored_checks_text}'
108+
'Values provided here will be in addition to the above, unless an alternate config is provided.'
109+
if ignored_checks else ''
110+
}"""
111+
),
112+
)
113+
lint_parser.set_defaults(func=validate_docstrings.run_hook)
114+
115+
return ap
116+
117+
118+
def main(argv: Union[Sequence[str], None] = None) -> int:
119+
"""CLI for numpydoc."""
120+
ap = get_parser()
121+
122+
args = vars(ap.parse_args(argv))
123+
124+
try:
125+
func = args.pop("func")
126+
return func(**args)
127+
except KeyError:
128+
ap.exit(status=2, message=ap.format_help())

numpydoc/hooks/validate_docstrings.py

Lines changed: 27 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Run numpydoc validation on contents of a file."""
22

3-
import argparse
43
import ast
54
import configparser
65
import os
@@ -13,7 +12,7 @@
1312
import tomli as tomllib
1413

1514
from pathlib import Path
16-
from typing import Sequence, Tuple, Union
15+
from typing import Any, Dict, List, Tuple, Union
1716

1817
from tabulate import tabulate
1918

@@ -341,62 +340,35 @@ def process_file(filepath: os.PathLike, config: dict) -> "list[list[str]]":
341340
return docstring_visitor.findings
342341

343342

344-
def main(argv: Union[Sequence[str], None] = None) -> int:
345-
"""Run the numpydoc validation hook."""
343+
def run_hook(
344+
files: List[str],
345+
*,
346+
config: Union[Dict[str, Any], None] = None,
347+
ignore: Union[List[str], None] = None,
348+
) -> int:
349+
"""
350+
Run the numpydoc validation hook.
346351
347-
project_root_from_cwd, config_file = find_project_root(["."])
348-
config_options = parse_config(project_root_from_cwd)
349-
ignored_checks = (
350-
"\n "
351-
+ "\n ".join(
352-
[
353-
f"- {check}: {validate.ERROR_MSGS[check]}"
354-
for check in set(validate.ERROR_MSGS.keys()) - config_options["checks"]
355-
]
356-
)
357-
+ "\n"
358-
)
359-
360-
parser = argparse.ArgumentParser(
361-
description="Run numpydoc validation on files with option to ignore individual checks.",
362-
formatter_class=argparse.RawTextHelpFormatter,
363-
)
364-
parser.add_argument(
365-
"files", type=str, nargs="+", help="File(s) to run numpydoc validation on."
366-
)
367-
parser.add_argument(
368-
"--config",
369-
type=str,
370-
help=(
371-
"Path to a directory containing a pyproject.toml or setup.cfg file.\n"
372-
"The hook will look for it in the root project directory.\n"
373-
"If both are present, only pyproject.toml will be used.\n"
374-
"Options must be placed under\n"
375-
" - [tool:numpydoc_validation] for setup.cfg files and\n"
376-
" - [tool.numpydoc_validation] for pyproject.toml files."
377-
),
378-
)
379-
parser.add_argument(
380-
"--ignore",
381-
type=str,
382-
nargs="*",
383-
help=(
384-
f"""Check codes to ignore.{
385-
' Currently ignoring the following from '
386-
f'{Path(project_root_from_cwd) / config_file}: {ignored_checks}'
387-
'Values provided here will be in addition to the above, unless an alternate config is provided.'
388-
if config_options["checks"] else ''
389-
}"""
390-
),
391-
)
392-
393-
args = parser.parse_args(argv)
394-
project_root, _ = find_project_root(args.files)
395-
config_options = parse_config(args.config or project_root)
396-
config_options["checks"] -= set(args.ignore or [])
352+
Parameters
353+
----------
354+
files : list[str]
355+
The absolute or relative paths to the files to inspect.
356+
config : Union[dict[str, Any], None], optional
357+
Configuration options for reviewing flagged issues.
358+
ignore : Union[list[str], None], optional
359+
Checks to ignore in the results.
360+
361+
Returns
362+
-------
363+
int
364+
The return status: 1 if issues were found, 0 otherwise.
365+
"""
366+
project_root, _ = find_project_root(files)
367+
config_options = parse_config(config or project_root)
368+
config_options["checks"] -= set(ignore or [])
397369

398370
findings = []
399-
for file in args.files:
371+
for file in files:
400372
findings.extend(process_file(file, config_options))
401373

402374
if findings:
@@ -411,7 +383,3 @@ def main(argv: Union[Sequence[str], None] = None) -> int:
411383
)
412384
return 1
413385
return 0
414-
415-
416-
if __name__ == "__main__":
417-
raise SystemExit(main())

0 commit comments

Comments
 (0)