Skip to content

Commit

Permalink
Unify CLIs (#537)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
stefmolin committed Apr 30, 2024
1 parent 3a21e54 commit e7c6baf
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 161 deletions.
16 changes: 8 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,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: |
Expand Down Expand Up @@ -110,10 +110,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: |
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -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]
6 changes: 3 additions & 3 deletions doc/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <inline_ignore_comments>`,
but will not look for any configuration like the :ref:`pre-commit hook <pre_commit_hook>`
Expand Down
53 changes: 2 additions & 51 deletions numpydoc/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
128 changes: 128 additions & 0 deletions numpydoc/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""The CLI for numpydoc."""

import argparse
import ast
from pathlib import Path
from typing import List, Sequence, Union

from .docscrape_sphinx import get_doc_object
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:
"""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 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")

def _parse_config(s):
key, _, value = s.partition("=")
value = ast.literal_eval(value)
return key, value

render = subparsers.add_parser(
"render",
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(
"-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 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)

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)
except KeyError:
ap.exit(status=2, message=ap.format_help())
86 changes: 27 additions & 59 deletions numpydoc/hooks/validate_docstrings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Run numpydoc validation on contents of a file."""

import argparse
import ast
import configparser
import os
Expand All @@ -13,7 +12,7 @@
import tomli as tomllib

from pathlib import Path
from typing import Sequence, Tuple, Union
from typing import Any, Dict, List, Tuple, Union

from tabulate import tabulate

Expand Down Expand Up @@ -341,62 +340,35 @@ 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 run_hook(
files: List[str],
*,
config: Union[Dict[str, Any], None] = None,
ignore: Union[List[str], None] = None,
) -> int:
"""
Run the numpydoc validation hook.
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"
)

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(
"--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 ''
}"""
),
)

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 [])
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.
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:
Expand All @@ -411,7 +383,3 @@ def main(argv: Union[Sequence[str], None] = None) -> int:
)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading

0 comments on commit e7c6baf

Please sign in to comment.