Skip to content

Commit

Permalink
✨ Get help for params from docstrings
Browse files Browse the repository at this point in the history
Resolves issue fastapi#336.
  • Loading branch information
alexreg committed Jul 31, 2022
1 parent 8bf0597 commit aecb0c2
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ test = [
"pytest-xdist ~= 1.32.0",
"rich >= 10.11.0, < 13.0.0",
"shellingham ~= 1.4.0",
"types-docutils ~= 0.18.3",
]
doc = [
"mdx-include ~= 1.4.1",
Expand All @@ -64,6 +65,7 @@ dev = [
]
all = [
"colorama ~= 0.4.4",
"docutils ~= 0.18.1",
"shellingham ~= 1.4.0",
]

Expand Down
68 changes: 68 additions & 0 deletions tests/test_help_docstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import typer
from typer.testing import CliRunner

runner = CliRunner()

app = typer.Typer()


@app.command()
def cmd(
arg1: float,
arg2: str = typer.Argument("bar"),
opt1: str = typer.Option("foo"),
opt2: bool = typer.Option(False),
opt3: int = typer.Option(1),
):
"""
Some command
:param opt1: First option
:param opt2: Second option
:param opt3:
Third
option
:param arg1: First argument
:param arg2: Second argument
"""


@app.callback()
def main(global_opt: bool = False):
"""
Callback
:param global_opt: Global option
"""


def test_help_main():
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "Callback" in result.output
assert ":param" not in result.output
assert "--global-opt / --no-global-opt" in result.output
assert "Global option" in result.output
assert "[default: no-global-opt]" in result.output


def test_help_cmd():
result = runner.invoke(app, ["cmd", "--help"])
assert result.exit_code == 0
assert "Some command" in result.output
assert ":param" not in result.output
assert "ARG1" in result.output
assert "First argument" in result.output
assert "[required]" in result.output
assert "[ARG2]" in result.output
assert "Second argument" in result.output
assert "[default: bar]" in result.output
assert "--opt1 TEXT" in result.output
assert "First option" in result.output
assert "[default: foo]" in result.output
assert "--opt2 / --no-opt2" in result.output
assert "Second option" in result.output
assert "[default: no-opt2]" in result.output
assert "--opt3 INTEGER" in result.output
assert "Third option" in result.output
assert "[default: 1]" in result.output
41 changes: 41 additions & 0 deletions typer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,45 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any:
return get_command(self)(*args, **kwargs)


def process_help_text(command: click.Command) -> None:
import docutils.nodes

from . import rst

if command.help is None:
return

doc = rst.parse(command.help)

paragraphs = (
node for node in doc.children if isinstance(node, docutils.nodes.paragraph)
)
command.help = "\n\n".join(paragraph.astext() for paragraph in paragraphs)

field_lists = doc.findall(lambda node: isinstance(node, docutils.nodes.field_list))
for field_list in field_lists:
for field in field_list.children:
assert isinstance(field, docutils.nodes.field)
field_name = field.children[0]
assert isinstance(field_name, docutils.nodes.field_name)
field_body = field.children[1]
assert isinstance(field_body, docutils.nodes.field_body)
field_name_parts = field_name.astext().split(" ", maxsplit=1)
if field_name_parts[0].casefold() == "param":
param_name = field_name_parts[1]
param_help = field_body.astext()
param = next(
(param for param in command.params if param.name == param_name),
None,
)
if isinstance(param, click.Option):
if not param.help:
param.help = param_help
elif isinstance(param, TyperArgument):
if not param.help:
param.help = param_help


def get_group(typer_instance: Typer) -> click.Group:
group = get_group_from_info(TyperInfo(typer_instance))
return group
Expand Down Expand Up @@ -485,6 +524,7 @@ def get_group_from_info(group_info: TyperInfo) -> click.Group:
hidden=solved_info.hidden,
deprecated=solved_info.deprecated,
)
process_help_text(group)
return group


Expand Down Expand Up @@ -544,6 +584,7 @@ def get_command_from_info(command_info: CommandInfo) -> click.Command:
hidden=command_info.hidden,
deprecated=command_info.deprecated,
)
process_help_text(command)
return command


Expand Down
14 changes: 14 additions & 0 deletions typer/rst.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import docutils.frontend
import docutils.nodes
import docutils.parsers.rst
import docutils.utils


def parse(text: str) -> docutils.nodes.document:
parser = docutils.parsers.rst.Parser()
settings = docutils.frontend.OptionParser(
components=(docutils.parsers.rst.Parser,)
).get_default_values()
document = docutils.utils.new_document("<rst-doc>", settings=settings)
parser.parse(text, document)
return document

0 comments on commit aecb0c2

Please sign in to comment.