🐛 Fix and document extracting help form docstring

This commit is contained in:
Sebastián Ramírez 2019-12-28 14:54:26 +01:00 committed by GitHub
commit a8bc1b7b32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 187 additions and 22 deletions

8
.coveragerc Normal file
View file

@ -0,0 +1,8 @@
[run]
source =
typer
tests
docs/src
parallel = True

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ dist
.mypy_cache
.idea
site
.coverage
htmlcov

View file

@ -0,0 +1,17 @@
import typer
def main(name: str, lastname: str = "", formal: bool = False):
"""
Say hi to NAME, optionally with a --lastname.
If --formal is used, say hi very formally.
"""
if formal:
typer.echo(f"Good day Ms. {name} {lastname}.")
else:
typer.echo(f"Hello {name} {lastname}")
if __name__ == "__main__":
typer.run(main)

View file

@ -22,10 +22,12 @@ Hello World
// Now check the --help
$ python main.py --help
Usage: main.py [OPTIONS]
Usage: tryit.py [OPTIONS]
Options:
--help Show this message and exit.
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
```
</div>
@ -302,6 +304,44 @@ Hello Camila Gutiérrez
</div>
## Document your CLI app
If you add a <abbr title="a multi-line string as the first expression inside a function (not assigned to any variable) used for documentation">docstring</abbr> to your function it will be used in the help text:
```Python hl_lines="5 6 7 8 9"
{!./src/first_steps/tutorial006.py!}
```
Now see it with the `--help` option:
<div class="termy">
```console
$ python main.py --help
Usage: tutorial006.py [OPTIONS] NAME
Say hi to NAME, optionally with a --lastname.
If --formal is used, say hi very formally.
Options:
--lastname TEXT
--formal / --no-formal
--install-completion Install completion for the current shell.
--show-completion Show completion for the current shell, to copy it or customize the installation.
--help Show this message and exit.
```
</div>
!!! tip
You should document the *CLI arguments* in the docstring.
There is another place to document the *CLI options* that will show up next to them in the help text as with `--install-completion` or `--help`, you will learn that later in the tutorial.
But *CLI arguments* are normally used for the most necessary things, so you should document them here in the *docstring*.
## Arguments, options, parameters, optional, required
Be aware that these terms refer to multiple things depending on the context, and sadly, those "contexts" mix frequently, so it's easy to get confused.

View file

@ -39,6 +39,7 @@ Documentation = "https://typer.tiangolo.com/"
test = [
"pytest >=4.0.0",
"pytest-cov",
"coverage",
"mypy",
"black",
"isort"

View file

@ -17,7 +17,7 @@ def test_cli():
def test_script():
result = subprocess.run(
["coverage", "run", "--parallel-mode", mod.__file__, "--help"],
["coverage", "run", mod.__file__, "--help"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",

View file

@ -25,7 +25,7 @@ def test_2():
def test_script():
result = subprocess.run(
["coverage", "run", "--parallel-mode", mod.__file__, "--help"],
["coverage", "run", mod.__file__, "--help"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",

View file

@ -25,7 +25,7 @@ def test_2():
def test_script():
result = subprocess.run(
["coverage", "run", "--parallel-mode", mod.__file__, "--help"],
["coverage", "run", mod.__file__, "--help"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",

View file

@ -37,7 +37,7 @@ def test_formal_3():
def test_script():
result = subprocess.run(
["coverage", "run", "--parallel-mode", mod.__file__, "--help"],
["coverage", "run", mod.__file__, "--help"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",

View file

@ -44,7 +44,7 @@ def test_formal_1():
def test_script():
result = subprocess.run(
["coverage", "run", "--parallel-mode", mod.__file__, "--help"],
["coverage", "run", mod.__file__, "--help"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",

View file

@ -0,0 +1,52 @@
import subprocess
import typer
from typer.testing import CliRunner
from first_steps import tutorial006 as mod
runner = CliRunner()
app = typer.Typer()
app.command()(mod.main)
def test_help():
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "Say hi to NAME, optionally with a --lastname." in result.output
assert "If --formal is used, say hi very formally." in result.output
def test_1():
result = runner.invoke(app, ["Camila"])
assert result.exit_code == 0
assert "Hello Camila" in result.output
def test_option_lastname():
result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez"])
assert result.exit_code == 0
assert "Hello Camila Gutiérrez" in result.output
def test_option_lastname_2():
result = runner.invoke(app, ["--lastname", "Gutiérrez", "Camila"])
assert result.exit_code == 0
assert "Hello Camila Gutiérrez" in result.output
def test_formal_1():
result = runner.invoke(app, ["Camila", "--lastname", "Gutiérrez", "--formal"])
assert result.exit_code == 0
assert "Good day Ms. Camila Gutiérrez." in result.output
def test_script():
result = subprocess.run(
["coverage", "run", mod.__file__, "--help"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8",
)
assert "Usage" in result.stdout

View file

@ -1,3 +1,4 @@
import sys
from typing import Any
import click
@ -14,14 +15,14 @@ def install_callback(ctx: click.Context, param: click.Parameter, value: Any) ->
return value
shell, path = click_completion.core.install()
click.echo(f"{shell} completion installed in {path}")
exit(0)
sys.exit(0)
def show_callback(ctx: click.Context, param: click.Parameter, value: Any) -> Any:
if not value or ctx.resilient_parsing:
return value
click.echo(click_completion.core.get_code())
exit(0)
sys.exit(0)
# Create a fake command function to extract the completion parameters

View file

@ -259,28 +259,67 @@ def get_group_name(typer_info: TyperInfo) -> Optional[str]:
return None
def solve_typer_info_help(typer_info: TyperInfo) -> str:
# Priority 1a: Value was set in app.add_typer()
if not isinstance(typer_info.help, DefaultPlaceholder):
return inspect.cleandoc(typer_info.help or "")
# Priority 1b: Value was set in app.add_typer(), in callback docstring
if typer_info.callback:
doc = inspect.getdoc(typer_info.callback)
if doc:
return doc
try:
# Priority 2a: Value was set in @subapp.callback()
doc = typer_info.typer_instance.registered_callback.help
if not isinstance(doc, DefaultPlaceholder):
return inspect.cleandoc(doc or "")
# Priority 2b: Value was set in @subapp.callback(), in callback docstring
doc = inspect.getdoc(typer_info.typer_instance.registered_callback.callback)
if doc:
return doc
except AttributeError:
pass
try:
# Priority 3a: Value set in subapp = typer.Typer()
instance_value = typer_info.typer_instance.info.help
if not isinstance(instance_value, DefaultPlaceholder):
return inspect.cleandoc(instance_value or "")
doc = inspect.getdoc(typer_info.typer_instance.callback)
if doc:
return doc
except AttributeError:
pass
# Value not set, use the default
return typer_info.help.value
def solve_typer_info_defaults(typer_info: TyperInfo) -> TyperInfo:
values = {}
values: Dict[str, Any] = {}
values["help"] = solve_typer_info_help(typer_info)
name = None
for name, value in typer_info.__dict__.items():
# Priority 1: Value was set in app.add_typer()
if not isinstance(value, DefaultPlaceholder):
values[name] = value
continue
if typer_info.typer_instance:
if typer_info.typer_instance.registered_callback:
callback_value = getattr(
typer_info.typer_instance.registered_callback, name
)
# Priority 2: Value was set in @subapp.callback()
if not isinstance(callback_value, DefaultPlaceholder):
values[name] = callback_value
continue
instance_value = getattr(typer_info.typer_instance.info, name)
# Priority 3: Value set in subapp = typer.Typer()
# Priority 2: Value was set in @subapp.callback()
try:
callback_value = getattr(
typer_info.typer_instance.registered_callback, name # type: ignore
)
if not isinstance(callback_value, DefaultPlaceholder):
values[name] = callback_value
continue
except AttributeError:
pass
# Priority 3: Value set in subapp = typer.Typer()
try:
instance_value = getattr(typer_info.typer_instance.info, name) # type: ignore
if not isinstance(instance_value, DefaultPlaceholder):
values[name] = instance_value
continue
except AttributeError:
pass
# Value not set, use the default
values[name] = value.value
if values["name"] is None:
@ -359,6 +398,11 @@ def get_params_convertors_ctx_param_name_from_function(
def get_command_from_info(command_info: CommandInfo) -> click.Command:
assert command_info.callback, "A command must have a callback function"
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)
else:
use_help = inspect.cleandoc(use_help)
(
params,
convertors,
@ -375,7 +419,7 @@ def get_command_from_info(command_info: CommandInfo) -> click.Command:
context_param_name=context_param_name,
),
params=params, # type: ignore
help=command_info.help,
help=use_help,
epilog=command_info.epilog,
short_help=command_info.short_help,
options_metavar=command_info.options_metavar,