From 2140150d17cf77fda562b82b1b5034310e965a80 Mon Sep 17 00:00:00 2001 From: Mateus Oliveira Date: Sun, 24 Jul 2022 14:47:46 -0300 Subject: [PATCH 1/3] feat: Add docstring automation to get help message to typer objects --- .python-version | 1 + .../test_docstring_automation/test_google.py | 82 +++++++ .../test_no_style.py | 42 ++++ tests/test_docstring_automation/test_numpy.py | 98 ++++++++ .../test_docstring_automation/test_sphinx.py | 88 +++++++ typer/docstring_automation.py | 232 ++++++++++++++++++ typer/main.py | 13 +- 7 files changed, 552 insertions(+), 4 deletions(-) create mode 100644 .python-version create mode 100644 tests/test_docstring_automation/test_google.py create mode 100644 tests/test_docstring_automation/test_no_style.py create mode 100644 tests/test_docstring_automation/test_numpy.py create mode 100644 tests/test_docstring_automation/test_sphinx.py create mode 100644 typer/docstring_automation.py diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..6a801ce542 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.8.13 diff --git a/tests/test_docstring_automation/test_google.py b/tests/test_docstring_automation/test_google.py new file mode 100644 index 0000000000..25976b5bed --- /dev/null +++ b/tests/test_docstring_automation/test_google.py @@ -0,0 +1,82 @@ +from typing import Optional + +import typer +import typer.core +from typer.testing import CliRunner + + +def function_to_test_google_docstring( + param1: str, param2: int, param3: Optional[str] = None +) -> str: + """ + Function to test Google style docstring. + + Args: + param1 (str): A very detailed description. + param2 (int): A small one + param3 (Optional[str], optional): A description with default value. Defaults to None. + + Returns: + str: Return information. + + """ + + +def function_to_test_priority( + param1: str = typer.Argument(...), + param2: int = typer.Argument(..., help="A complete different one."), + param3: Optional[str] = None, +) -> str: + """ + Function to test Google style docstring. + + Args: + param1 (str, optional): A very detailed description. Defaults to typer.Argument(...). + param2 (int, optional): A small one. Defaults to typer.Argument(..., help="A complete different one."). + param3 (Optional[str], optional): A description with default value. Defaults to None. + + Returns: + str: Return information. + + """ + + +runner = CliRunner() + + +def test_google_help(): + app = typer.Typer() + app.command()(function_to_test_google_docstring) + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Args:" not in result.output + assert "Returns:" not in result.output + assert "(str" not in result.output + assert "(int" not in result.output + assert "optional" not in result.output + assert " Defaults to" not in result.output + assert "Function to test Google style docstring." in result.output + assert "A small one." in result.output + assert "A very detailed description." in result.output + assert "A description with default value." in result.output + + +def test_help_priority(): + app = typer.Typer() + app.command(help="Not automated!")(function_to_test_priority) + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Args:" not in result.output + assert "Returns:" not in result.output + assert "(str" not in result.output + assert "(int" not in result.output + assert "optional" not in result.output + assert " Defaults to" not in result.output + assert "Function to test Google style docstring." not in result.output + assert "A small one." not in result.output + assert "Not automated!" in result.output + assert "A very detailed description." in result.output + assert "A complete different one." in result.output + assert "A description with default value." in result.output diff --git a/tests/test_docstring_automation/test_no_style.py b/tests/test_docstring_automation/test_no_style.py new file mode 100644 index 0000000000..47fb1c325a --- /dev/null +++ b/tests/test_docstring_automation/test_no_style.py @@ -0,0 +1,42 @@ +from typing import Optional + +import typer +import typer.core +from typer.testing import CliRunner + + +def function_to_test_no_style_docstring( + param1: str, param2: int, param3: Optional[str] = None +) -> str: + """Function to test No style docstring.""" + + +def function_to_test_priority( + param1: str = typer.Argument(...), + param2: int = typer.Argument(..., help="A complete different one."), + param3: Optional[str] = None, +) -> str: + """Function to test No style docstring.""" + + +runner = CliRunner() + + +def test_no_style_help(): + app = typer.Typer() + app.command()(function_to_test_no_style_docstring) + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Function to test No style docstring." in result.output + + +def test_help_priority(): + app = typer.Typer() + app.command(help="Not automated!")(function_to_test_priority) + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Function to test No style docstring." not in result.output + assert "Not automated!" in result.output + assert "A complete different one." in result.output diff --git a/tests/test_docstring_automation/test_numpy.py b/tests/test_docstring_automation/test_numpy.py new file mode 100644 index 0000000000..5a0f84fbb6 --- /dev/null +++ b/tests/test_docstring_automation/test_numpy.py @@ -0,0 +1,98 @@ +from typing import Optional + +import typer +import typer.core +from typer.testing import CliRunner + + +def function_to_test_numpy_docstring( + param1: str, param2: int, param3: Optional[str] = None +) -> str: + """ + Function to test Numpy style docstring. + + Parameters + ---------- + param1 : str + A very detailed description. + param2 : int + A small one + param3 : Optional[str], optional + A description with default value, by default None + + Returns + ------- + str + Return information. + + """ + + +def function_to_test_priority( + param1: str = typer.Argument(...), + param2: int = typer.Argument(..., help="A complete different one."), + param3: Optional[str] = None, +) -> str: + """ + Function to test Numpy style docstring. + + Parameters + ---------- + param1 : str, optional + A very detailed description, by default typer.Argument(...) + param2 : int, optional + A small one, by default typer.Argument(..., help="A complete different one.") + param3 : Optional[str], optional + A description with default value, by default None + + Returns + ------- + str + Return information. + + """ + + +runner = CliRunner() + + +def test_numpy_help(): + app = typer.Typer() + app.command()(function_to_test_numpy_docstring) + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Parameters" not in result.output + assert "----------" not in result.output + assert "Returns" not in result.output + assert "-------" not in result.output + assert ": str" not in result.output + assert ": int" not in result.output + assert "optional" not in result.output + assert ", by default" not in result.output + assert "Function to test Numpy style docstring." in result.output + assert "A small one." in result.output + assert "A very detailed description." in result.output + assert "A description with default value." in result.output + + +def test_help_priority(): + app = typer.Typer() + app.command(help="Not automated!")(function_to_test_priority) + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Parameters" not in result.output + assert "----------" not in result.output + assert "Returns" not in result.output + assert "-------" not in result.output + assert ": str" not in result.output + assert ": int" not in result.output + assert "optional" not in result.output + assert ", by default" not in result.output + assert "Function to test Numpy style docstring." not in result.output + assert "A small one." not in result.output + assert "Not automated!" in result.output + assert "A very detailed description." in result.output + assert "A complete different one." in result.output + assert "A description with default value." in result.output diff --git a/tests/test_docstring_automation/test_sphinx.py b/tests/test_docstring_automation/test_sphinx.py new file mode 100644 index 0000000000..e59a8a7af3 --- /dev/null +++ b/tests/test_docstring_automation/test_sphinx.py @@ -0,0 +1,88 @@ +from typing import Optional + +import typer +import typer.core +from typer.testing import CliRunner + + +def function_to_test_sphinx_docstring( + param1: str, param2: int, param3: Optional[str] = None +) -> str: + """ + Function to test Sphinx style docstring. + + :param param1: A very detailed description. + :type param1: str + :param param2: A small one + :type param2: int + :param param3: A description with default value, defaults to None + :type param3: Optional[str], optional + :return: Return information. + :rtype: str + + """ + + +def function_to_test_priority( + param1: str = typer.Argument(...), + param2: int = typer.Argument(..., help="A complete different one."), + param3: Optional[str] = None, +) -> str: + """ + Function to test Sphinx style docstring. + + :param param1: A very detailed description, defaults to typer.Argument(...) + :type param1: str, optional + :param param2: A small one, defaults to typer.Argument(..., help="A complete different one.") + :type param2: int, optional + :param param3: A description with default value, defaults to None + :type param3: Optional[str], optional + :return: Return information. + :rtype: str + + """ + + +runner = CliRunner() + + +def test_sphinx_help(): + app = typer.Typer() + app.command()(function_to_test_sphinx_docstring) + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert ":param" not in result.output + assert ":type" not in result.output + assert ":return:" not in result.output + assert ":rtype:" not in result.output + assert ": str" not in result.output + assert ": int" not in result.output + assert "optional" not in result.output + assert ", defaults to" not in result.output + assert "Function to test Sphinx style docstring." in result.output + assert "A small one." in result.output + assert "A very detailed description." in result.output + assert "A description with default value." in result.output + + +def test_help_priority(): + app = typer.Typer() + app.command(help="Not automated!")(function_to_test_priority) + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert ":param" not in result.output + assert ":type" not in result.output + assert ":return:" not in result.output + assert ":rtype:" not in result.output + assert ": str" not in result.output + assert ": int" not in result.output + assert "optional" not in result.output + assert ", defaults to" not in result.output + assert "Function to test Sphinx style docstring." not in result.output + assert "A small one." not in result.output + assert "Not automated!" in result.output + assert "A very detailed description." in result.output + assert "A complete different one." in result.output + assert "A description with default value." in result.output diff --git a/typer/docstring_automation.py b/typer/docstring_automation.py new file mode 100644 index 0000000000..7573f9a7c1 --- /dev/null +++ b/typer/docstring_automation.py @@ -0,0 +1,232 @@ +import inspect +from typing import Any, Callable, List, Tuple, Union + +import click + +NUMPY = "NUMPY" # Numpy style docstring +GOOGLE = "GOOGLE" # Google style docstring +SPHINX = "SPHINX" # reST (Sphinx) style docstring +NUMPY_PARAMS = "Parameters" +GOOGLE_PARAMS = "Args:" +SPHINX_PARAM = ":param" +SPHINX_RETURNS = ":returns" +SPHINX_RAISES = ":raises" +DOCSTRING_SECTIONS = { + NUMPY_PARAMS: True, + "Returns": True, + "Raises": True, + GOOGLE_PARAMS: True, + "Returns:": True, + "Raises:": True, +} +DOCSTRING_STYLES_PARAMS = { + NUMPY: NUMPY_PARAMS, + GOOGLE: GOOGLE_PARAMS, +} + + +def get_help_from_docstring(command: Callable[..., Any]) -> str: + """ + Get help message from callable object. + + Parameters + ---------- + command : Callable[..., Any] + Callable object (command, callback, ...). + + Returns + ------- + str + Docstring summary, if exists. + + """ + docstring = inspect.getdoc(command) + if not docstring: + return "" + docstring_lines = docstring.strip().splitlines() + help_message = "" + for line in docstring_lines: + if DOCSTRING_SECTIONS.get(line.strip()) or line.strip().startswith( + (SPHINX_PARAM, SPHINX_RETURNS, SPHINX_RAISES) + ): + break + help_message += line + "\n" if line else "" + return help_message + + +def get_index_of_sphinx_param_section( + docstring_lines: List[str], +) -> Tuple[int, str]: + """ + Get list index of Sphinx parameters section in docstring lines. + + Parameters + ---------- + docstring_lines : List[str] + Lines of the docstring. + + Returns + ------- + Tuple[int, str] + Index and docstring style. + + """ + index_param_section = 0 + docstring_style = "" + for index, line in enumerate(docstring_lines): + if line.strip().startswith(SPHINX_PARAM): + index_param_section = index - 1 + docstring_style = SPHINX + break + return index_param_section, docstring_style + + +def get_param_help_from_numpy_docstring( + docstring_lines: List[str], index_param_section: int, param_name: str +) -> str: + """ + Get parameter help message from Numpy style docstring. + + Parameters + ---------- + docstring_lines : List[str] + Lines of the docstring. + index_param_section : int + List index of the parameters section in the lines of the docstring. + param_name : str + Name of the parameter. + + Returns + ------- + str + Parameter help message, if any. + + """ + index_param_section += 1 + help_message = "" + for index, line in enumerate(docstring_lines[index_param_section:]): + if line.strip().startswith(param_name): + help_message = docstring_lines[index + index_param_section + 1] + help_message = help_message.split(", by default")[0] + if help_message[-1] != ".": + help_message += "." + break + return help_message + + +def get_param_help_from_google_docstring( + docstring_lines: List[str], index_param_section: int, param_name: str +) -> str: + """ + Get parameter help message from Google style docstring. + + Parameters + ---------- + docstring_lines : List[str] + Lines of the docstring. + index_param_section : int + List index of the parameters section in the lines of the docstring. + param_name : str + Name of the parameter. + + Returns + ------- + str + Parameter help message, if any. + + """ + help_message = "" + for index, line in enumerate(docstring_lines[index_param_section:]): + if line.strip().startswith(param_name): + help_message = docstring_lines[index + index_param_section] + help_message = help_message.split(":", maxsplit=2)[1] + help_message = help_message.split(" Defaults to", maxsplit=2)[0] + if help_message[-1] != ".": + help_message += "." + break + return help_message + + +def get_param_help_from_sphinx_docstring( + docstring_lines: List[str], index_param_section: int, param_name: str +) -> str: + """ + Get parameter help message from Sphinx style docstring. + + Parameters + ---------- + docstring_lines : List[str] + Lines of the docstring. + index_param_section : int + List index of the parameters section in the lines of the docstring. + param_name : str + Name of the parameter. + + Returns + ------- + str + Parameter help message, if any. + + """ + help_message = "" + for index, line in enumerate(docstring_lines[index_param_section:]): + if line.strip().startswith(f"{SPHINX_PARAM} {param_name}"): + help_message = docstring_lines[index + index_param_section] + help_message = help_message.split(":", maxsplit=3)[2] + help_message = help_message.split(", defaults to", maxsplit=2)[0] + if help_message[-1] != ".": + help_message += "." + break + return help_message + + +def get_param_help_from_docstring( + param: Union[click.Argument, click.Option], command: Callable[..., Any] +) -> str: + """ + Get parameter help message from from callable object's docstring. + + Parameters + ---------- + param : Union[click.Argument, click.Option] + Parameter of the command. + command : Callable[..., Any] + Callable object (command, callback, ...). + + Returns + ------- + str + Parameter help message, if any. + + """ + docstring = inspect.getdoc(command) + if not docstring: + return "" + docstring_lines = docstring.strip().splitlines() + index_param_section = 0 + docstring_style = "" + for style, param_section in DOCSTRING_STYLES_PARAMS.items(): + try: + index_param_section = docstring_lines.index(param_section) + if index_param_section: + docstring_style = style + break + except ValueError: + continue + if not docstring_style: + ( + index_param_section, + docstring_style, + ) = get_index_of_sphinx_param_section(docstring_lines) + if not docstring_style: + return "" + get_help_message = { + NUMPY: get_param_help_from_numpy_docstring, + GOOGLE: get_param_help_from_google_docstring, + SPHINX: get_param_help_from_sphinx_docstring, + } + return get_help_message[docstring_style]( + docstring_lines=docstring_lines, + index_param_section=index_param_section, + param_name=param.name, # type: ignore + ) diff --git a/typer/main.py b/typer/main.py index 8aa1cf9b30..c7ae405655 100644 --- a/typer/main.py +++ b/typer/main.py @@ -15,6 +15,7 @@ from .completion import get_completion_inspect_parameters from .core import MarkupMode, TyperArgument, TyperCommand, TyperGroup, TyperOption +from .docstring_automation import get_help_from_docstring, get_param_help_from_docstring from .models import ( AnyType, ArgumentInfo, @@ -408,14 +409,14 @@ def solve_typer_info_help(typer_info: TyperInfo) -> str: pass # Priority 4: Implicit inference from callback docstring in app.add_typer() if typer_info.callback: - doc = inspect.getdoc(typer_info.callback) + doc = get_help_from_docstring(typer_info.callback) if doc: return doc # Priority 5: Implicit inference from callback docstring in @app.callback() try: callback = typer_info.typer_instance.registered_callback.callback if not isinstance(callback, DefaultPlaceholder): - doc = inspect.getdoc(callback or "") + doc = get_help_from_docstring(callback or "") if doc: return doc except AttributeError: @@ -424,7 +425,7 @@ def solve_typer_info_help(typer_info: TyperInfo) -> str: try: instance_callback = typer_info.typer_instance.info.callback if not isinstance(instance_callback, DefaultPlaceholder): - doc = inspect.getdoc(instance_callback) + doc = get_help_from_docstring(instance_callback) if doc: return doc except AttributeError: @@ -551,6 +552,10 @@ def get_params_convertors_ctx_param_name_from_function( context_param_name = param_name continue click_param, convertor = get_click_param(param) + if not click_param.help: # type: ignore + click_param.help = get_param_help_from_docstring( # type: ignore + param=click_param, command=callback + ) if convertor: convertors[param_name] = convertor params.append(click_param) @@ -567,7 +572,7 @@ def get_command_from_info( name = command_info.name or get_command_name(command_info.callback.__name__) use_help = command_info.help if use_help is None: - use_help = inspect.getdoc(command_info.callback) + use_help = get_help_from_docstring(command_info.callback) else: use_help = inspect.cleandoc(use_help) ( From 246009e3f78ff6acb5d76fdd95eef9bc6b8111a0 Mon Sep 17 00:00:00 2001 From: Mateus Oliveira Date: Tue, 16 Aug 2022 13:11:24 -0300 Subject: [PATCH 2/3] fixup! feat: Add docstring automation to get help message to typer objects --- .python-version | 1 - tests/test_docstring_automation/__init__.py | 74 +++++++++++++++ .../test_docstring_automation/test_google.py | 81 ++++++---------- tests/test_docstring_automation/test_numpy.py | 93 ++++++------------- .../test_docstring_automation/test_sphinx.py | 83 ++++++----------- typer/docstring_automation.py | 33 +++++-- 6 files changed, 188 insertions(+), 177 deletions(-) delete mode 100644 .python-version create mode 100644 tests/test_docstring_automation/__init__.py diff --git a/.python-version b/.python-version deleted file mode 100644 index 6a801ce542..0000000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.8.13 diff --git a/tests/test_docstring_automation/__init__.py b/tests/test_docstring_automation/__init__.py new file mode 100644 index 0000000000..e303e3ca0d --- /dev/null +++ b/tests/test_docstring_automation/__init__.py @@ -0,0 +1,74 @@ +from typing import Optional + +import typer + + +def function_to_test_docstring( + param1: str, param2: int, param3: Optional[str] = None +) -> str: + """Used by docstring automation tests.""" + + +SUMMARY = "Function to test docstring styles." +PARAMS = { + "param1": "A very detailed description.", + "param2": "A small one.", + "param3": "A description with default value.", +} +DOCSTRINGS = { + "NUMPY": """ + Function to test docstring styles. + + Parameters + ---------- + param1 : str + A very detailed description. + param2 : int + A small one + param3 : Optional[str], optional + A description with default value, by default None + + Returns + ------- + str + Return information. + + """, + "GOOGLE": """ + Function to test docstring styles. + + Args: + param1 (str): A very detailed description. + param2 (int): A small one + param3 (Optional[str], optional): A description with default value. + Defaults to None. + + Returns: + str: Return information. + + """, + "SPHINX": """ + Function to test docstring styles. + + :param param1: A very detailed description. + :type param1: str + :param param2: A small one + :type param2: int + :param param3: A description with default value, defaults to None + :type param3: Optional[str], optional + :return: Return information. + :rtype: str + + """, +} + +PRIORITY_SUMMARY = "Not automated!" +PRIORITY_PARAM_DOC = "A complete different one." + + +def function_to_test_priority( + param1: str = typer.Argument(...), + param2: int = typer.Argument(..., help=PRIORITY_PARAM_DOC), + param3: Optional[str] = None, +) -> str: + """Used to test if docstring automation respects priority.""" diff --git a/tests/test_docstring_automation/test_google.py b/tests/test_docstring_automation/test_google.py index 25976b5bed..e8e361db83 100644 --- a/tests/test_docstring_automation/test_google.py +++ b/tests/test_docstring_automation/test_google.py @@ -1,52 +1,24 @@ -from typing import Optional - import typer import typer.core from typer.testing import CliRunner - -def function_to_test_google_docstring( - param1: str, param2: int, param3: Optional[str] = None -) -> str: - """ - Function to test Google style docstring. - - Args: - param1 (str): A very detailed description. - param2 (int): A small one - param3 (Optional[str], optional): A description with default value. Defaults to None. - - Returns: - str: Return information. - - """ - - -def function_to_test_priority( - param1: str = typer.Argument(...), - param2: int = typer.Argument(..., help="A complete different one."), - param3: Optional[str] = None, -) -> str: - """ - Function to test Google style docstring. - - Args: - param1 (str, optional): A very detailed description. Defaults to typer.Argument(...). - param2 (int, optional): A small one. Defaults to typer.Argument(..., help="A complete different one."). - param3 (Optional[str], optional): A description with default value. Defaults to None. - - Returns: - str: Return information. - - """ - +from . import ( + DOCSTRINGS, + PARAMS, + PRIORITY_PARAM_DOC, + PRIORITY_SUMMARY, + SUMMARY, + function_to_test_docstring, + function_to_test_priority, +) runner = CliRunner() -def test_google_help(): +def test_google_docstring_parsing(): app = typer.Typer() - app.command()(function_to_test_google_docstring) + function_to_test_docstring.__doc__ = DOCSTRINGS["GOOGLE"] + app.command()(function_to_test_docstring) result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -56,15 +28,15 @@ def test_google_help(): assert "(int" not in result.output assert "optional" not in result.output assert " Defaults to" not in result.output - assert "Function to test Google style docstring." in result.output - assert "A small one." in result.output - assert "A very detailed description." in result.output - assert "A description with default value." in result.output + + assert SUMMARY in result.output + assert all(param_doc in result.output for param_doc in PARAMS.values()) -def test_help_priority(): +def test_google_docstring_parsing_priority(): app = typer.Typer() - app.command(help="Not automated!")(function_to_test_priority) + function_to_test_priority.__doc__ = DOCSTRINGS["GOOGLE"] + app.command(help=PRIORITY_SUMMARY)(function_to_test_priority) result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -74,9 +46,14 @@ def test_help_priority(): assert "(int" not in result.output assert "optional" not in result.output assert " Defaults to" not in result.output - assert "Function to test Google style docstring." not in result.output - assert "A small one." not in result.output - assert "Not automated!" in result.output - assert "A very detailed description." in result.output - assert "A complete different one." in result.output - assert "A description with default value." in result.output + + assert SUMMARY not in result.output + assert PRIORITY_SUMMARY in result.output + + assert PARAMS["param1"] in result.output + assert PARAMS["param2"] not in result.output + assert PRIORITY_PARAM_DOC in result.output + assert PARAMS["param3"] in result.output + + +# TODO def test_google_docstring_parsing_with line_breaking_param_doc(): diff --git a/tests/test_docstring_automation/test_numpy.py b/tests/test_docstring_automation/test_numpy.py index 5a0f84fbb6..5795cf1c9f 100644 --- a/tests/test_docstring_automation/test_numpy.py +++ b/tests/test_docstring_automation/test_numpy.py @@ -1,64 +1,24 @@ -from typing import Optional - import typer import typer.core from typer.testing import CliRunner - -def function_to_test_numpy_docstring( - param1: str, param2: int, param3: Optional[str] = None -) -> str: - """ - Function to test Numpy style docstring. - - Parameters - ---------- - param1 : str - A very detailed description. - param2 : int - A small one - param3 : Optional[str], optional - A description with default value, by default None - - Returns - ------- - str - Return information. - - """ - - -def function_to_test_priority( - param1: str = typer.Argument(...), - param2: int = typer.Argument(..., help="A complete different one."), - param3: Optional[str] = None, -) -> str: - """ - Function to test Numpy style docstring. - - Parameters - ---------- - param1 : str, optional - A very detailed description, by default typer.Argument(...) - param2 : int, optional - A small one, by default typer.Argument(..., help="A complete different one.") - param3 : Optional[str], optional - A description with default value, by default None - - Returns - ------- - str - Return information. - - """ - +from . import ( + DOCSTRINGS, + PARAMS, + PRIORITY_PARAM_DOC, + PRIORITY_SUMMARY, + SUMMARY, + function_to_test_docstring, + function_to_test_priority, +) runner = CliRunner() -def test_numpy_help(): +def test_numpy_docstring_parsing(): app = typer.Typer() - app.command()(function_to_test_numpy_docstring) + function_to_test_docstring.__doc__ = DOCSTRINGS["NUMPY"] + app.command()(function_to_test_docstring) result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -70,15 +30,15 @@ def test_numpy_help(): assert ": int" not in result.output assert "optional" not in result.output assert ", by default" not in result.output - assert "Function to test Numpy style docstring." in result.output - assert "A small one." in result.output - assert "A very detailed description." in result.output - assert "A description with default value." in result.output + + assert SUMMARY in result.output + assert all(param_doc in result.output for param_doc in PARAMS.values()) -def test_help_priority(): +def test_numpy_docstring_parsing_priority(): app = typer.Typer() - app.command(help="Not automated!")(function_to_test_priority) + function_to_test_priority.__doc__ = DOCSTRINGS["NUMPY"] + app.command(help=PRIORITY_SUMMARY)(function_to_test_priority) result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -90,9 +50,14 @@ def test_help_priority(): assert ": int" not in result.output assert "optional" not in result.output assert ", by default" not in result.output - assert "Function to test Numpy style docstring." not in result.output - assert "A small one." not in result.output - assert "Not automated!" in result.output - assert "A very detailed description." in result.output - assert "A complete different one." in result.output - assert "A description with default value." in result.output + + assert SUMMARY not in result.output + assert PRIORITY_SUMMARY in result.output + + assert PARAMS["param1"] in result.output + assert PARAMS["param2"] not in result.output + assert PRIORITY_PARAM_DOC in result.output + assert PARAMS["param3"] in result.output + + +# TODO def test_numpy_docstring_parsing_with line_breaking_param_doc(): diff --git a/tests/test_docstring_automation/test_sphinx.py b/tests/test_docstring_automation/test_sphinx.py index e59a8a7af3..6d31dd8338 100644 --- a/tests/test_docstring_automation/test_sphinx.py +++ b/tests/test_docstring_automation/test_sphinx.py @@ -1,54 +1,24 @@ -from typing import Optional - import typer import typer.core from typer.testing import CliRunner - -def function_to_test_sphinx_docstring( - param1: str, param2: int, param3: Optional[str] = None -) -> str: - """ - Function to test Sphinx style docstring. - - :param param1: A very detailed description. - :type param1: str - :param param2: A small one - :type param2: int - :param param3: A description with default value, defaults to None - :type param3: Optional[str], optional - :return: Return information. - :rtype: str - - """ - - -def function_to_test_priority( - param1: str = typer.Argument(...), - param2: int = typer.Argument(..., help="A complete different one."), - param3: Optional[str] = None, -) -> str: - """ - Function to test Sphinx style docstring. - - :param param1: A very detailed description, defaults to typer.Argument(...) - :type param1: str, optional - :param param2: A small one, defaults to typer.Argument(..., help="A complete different one.") - :type param2: int, optional - :param param3: A description with default value, defaults to None - :type param3: Optional[str], optional - :return: Return information. - :rtype: str - - """ - +from . import ( + DOCSTRINGS, + PARAMS, + PRIORITY_PARAM_DOC, + PRIORITY_SUMMARY, + SUMMARY, + function_to_test_docstring, + function_to_test_priority, +) runner = CliRunner() -def test_sphinx_help(): +def test_sphinx_docstring_parsing(): app = typer.Typer() - app.command()(function_to_test_sphinx_docstring) + function_to_test_docstring.__doc__ = DOCSTRINGS["SPHINX"] + app.command()(function_to_test_docstring) result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -60,15 +30,15 @@ def test_sphinx_help(): assert ": int" not in result.output assert "optional" not in result.output assert ", defaults to" not in result.output - assert "Function to test Sphinx style docstring." in result.output - assert "A small one." in result.output - assert "A very detailed description." in result.output - assert "A description with default value." in result.output + assert SUMMARY in result.output + assert all(param_doc in result.output for param_doc in PARAMS.values()) -def test_help_priority(): + +def test_sphinx_docstring_parsing_priority(): app = typer.Typer() - app.command(help="Not automated!")(function_to_test_priority) + function_to_test_priority.__doc__ = DOCSTRINGS["SPHINX"] + app.command(help=PRIORITY_SUMMARY)(function_to_test_priority) result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -80,9 +50,14 @@ def test_help_priority(): assert ": int" not in result.output assert "optional" not in result.output assert ", defaults to" not in result.output - assert "Function to test Sphinx style docstring." not in result.output - assert "A small one." not in result.output - assert "Not automated!" in result.output - assert "A very detailed description." in result.output - assert "A complete different one." in result.output - assert "A description with default value." in result.output + + assert SUMMARY not in result.output + assert PRIORITY_SUMMARY in result.output + + assert PARAMS["param1"] in result.output + assert PARAMS["param2"] not in result.output + assert PRIORITY_PARAM_DOC in result.output + assert PARAMS["param3"] in result.output + + +# TODO def test_sphinx_docstring_parsing_with line_breaking_param_doc(): diff --git a/typer/docstring_automation.py b/typer/docstring_automation.py index 7573f9a7c1..71e03286b0 100644 --- a/typer/docstring_automation.py +++ b/typer/docstring_automation.py @@ -1,11 +1,19 @@ +"""Docstring parser for the following styles: + +- Numpy https://numpydoc.readthedocs.io/en/latest/format.html +- Google https://google.github.io/styleguide/pyguide.html#381-docstrings +- Sphinx https://thomas-cokelaer.info/tutorials/sphinx/docstring_python.html + +""" + import inspect from typing import Any, Callable, List, Tuple, Union import click -NUMPY = "NUMPY" # Numpy style docstring -GOOGLE = "GOOGLE" # Google style docstring -SPHINX = "SPHINX" # reST (Sphinx) style docstring +NUMPY = "NUMPY" +GOOGLE = "GOOGLE" +SPHINX = "SPHINX" NUMPY_PARAMS = "Parameters" GOOGLE_PARAMS = "Args:" SPHINX_PARAM = ":param" @@ -14,10 +22,22 @@ DOCSTRING_SECTIONS = { NUMPY_PARAMS: True, "Returns": True, + "Yields": True, + "Receives": True, "Raises": True, + "Warns": True, + "Warnings": True, + "See Also": True, + "References": True, + "Notes": True, + "Examples": True, + "Attributes": True, + "Methods": True, GOOGLE_PARAMS: True, "Returns:": True, + "Yields:": True, "Raises:": True, + "Attributes:": True, } DOCSTRING_STYLES_PARAMS = { NUMPY: NUMPY_PARAMS, @@ -111,7 +131,7 @@ def get_param_help_from_numpy_docstring( if help_message[-1] != ".": help_message += "." break - return help_message + return help_message.strip() def get_param_help_from_google_docstring( @@ -144,7 +164,7 @@ def get_param_help_from_google_docstring( if help_message[-1] != ".": help_message += "." break - return help_message + return help_message.strip() def get_param_help_from_sphinx_docstring( @@ -177,7 +197,7 @@ def get_param_help_from_sphinx_docstring( if help_message[-1] != ".": help_message += "." break - return help_message + return help_message.strip() def get_param_help_from_docstring( @@ -220,6 +240,7 @@ def get_param_help_from_docstring( ) = get_index_of_sphinx_param_section(docstring_lines) if not docstring_style: return "" + get_help_message = { NUMPY: get_param_help_from_numpy_docstring, GOOGLE: get_param_help_from_google_docstring, From f42c9fb7912a337aaef8885311c51dd74a70c837 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 28 Aug 2024 16:04:31 +0200 Subject: [PATCH 3/3] temp fix for test (WIP) --- tests/assets/cli/multiapp-docs.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/assets/cli/multiapp-docs.md b/tests/assets/cli/multiapp-docs.md index 67d02568db..445bd70c0f 100644 --- a/tests/assets/cli/multiapp-docs.md +++ b/tests/assets/cli/multiapp-docs.md @@ -25,6 +25,7 @@ The end Top command + **Usage**: ```console @@ -57,6 +58,7 @@ $ multiapp sub [OPTIONS] COMMAND [ARGS]... Say Hello + **Usage**: ```console @@ -73,6 +75,7 @@ $ multiapp sub hello [OPTIONS] Say Hi + **Usage**: ```console @@ -91,6 +94,7 @@ $ multiapp sub hi [OPTIONS] [USER] Say bye + **Usage**: ```console