Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unify CLIs #537

Merged
merged 11 commits into from
Apr 30, 2024
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())
stefmolin marked this conversation as resolved.
Show resolved Hide resolved
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