diff --git a/auto_cli/parsing.py b/auto_cli/parsing.py index 3368be4..215fd41 100644 --- a/auto_cli/parsing.py +++ b/auto_cli/parsing.py @@ -1,6 +1,7 @@ import argparse import inspect -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +import re +from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple, Union from .types import Command from .utils import _print_and_quit @@ -31,7 +32,8 @@ def add_param_transformer(self, param_name: str, transform: Callable) -> None: def create_parser(command: Command) -> ArgumentParser: """Create a parser for the given command""" function = command.function - parser = ArgumentParser(description=f"{command.name}: {function.__doc__}") + function_doc = _parse_function_doc(function) + parser = ArgumentParser(description=f"{command.name}: {function_doc.description}") signature = inspect.signature(function) parameters = dict(signature.parameters) argspec = inspect.getfullargspec(function) @@ -51,7 +53,9 @@ def create_parser(command: Command) -> ArgumentParser: **_get_type_params(annotation, param_name, function), } - parser.add_argument(f"--{param_name}", **kwargs) + parser.add_argument( + f"--{param_name}", help=function_doc.param_docs.get(param_name), **kwargs + ) if kwargs.get("nargs", "+") != "+": # is_tuple: TODO(joris): refactor parser.add_param_transformer(param_name, tuple) # TODO(joris): parse documentation of param for help @@ -112,3 +116,42 @@ def _fail(message: str) -> None: return {"action": "store_true", "default": False, "required": False} return {"type": annotation} + + +class _FunctionDoc(NamedTuple): + description: str + param_docs: Dict[str, str] + + +def _parse_function_doc(function: Callable) -> _FunctionDoc: + """Parse function documentation which adheres to the Sphinx standard + https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#info-field-lists + """ + if not hasattr(function, "__doc__"): + return _FunctionDoc("", {}) + + doc = function.__doc__ + if not ":param" in doc: + # Doesn't conform to our standard, so the whole __doc__ is just + # the description of the function + return _FunctionDoc(doc, {}) + + # Now that we now doc conforms to our standard, we can parse out the + # parameter descriptions + + params_docs = {} + for line in doc.splitlines(): + line = line.strip() + if line.startswith(":param"): + line = line.lstrip(":param") + before, _, description = line.partition(":") + param_name = before.strip().split(" ")[-1] + params_docs[param_name] = description.strip() + + fn_description, _, _ = doc.partition(":param") + fn_description = _normalize_whitespace(fn_description.strip()) + return _FunctionDoc(fn_description, params_docs) + + +def _normalize_whitespace(text: str) -> str: + return re.sub(r"\s+", " ", text) diff --git a/auto_cli/tests/test_parsing.py b/auto_cli/tests/test_parsing.py index 76c1344..47a173c 100644 --- a/auto_cli/tests/test_parsing.py +++ b/auto_cli/tests/test_parsing.py @@ -1,6 +1,6 @@ from typing import List, Tuple -from auto_cli.parsing import create_parser +from auto_cli.parsing import _parse_function_doc, create_parser from auto_cli.types import Command @@ -67,3 +67,18 @@ def func_to_test(a: int, b): parser = create_parser(command) args = parser.parse(["--a", "4", "--b", "5"]) assert args == {"a": 4, "b": 5} + + +def test_parse_function_doc() -> None: + def func_to_test(a: int, b: str) -> int: + """This is a function + which does some interesting things + + :param int a: the number that is returned + :param b: ignored + """ + return a + + docs = _parse_function_doc(func_to_test) + assert docs.description == "This is a function which does some interesting things" + assert docs.param_docs == {"a": "the number that is returned", "b": "ignored"}