diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..6c140c4
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,8 @@
+[run]
+
+source =
+ typer
+ tests
+ docs/src
+
+parallel = True
diff --git a/.gitignore b/.gitignore
index 42ac371..bd4cbf4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,5 @@ dist
.mypy_cache
.idea
site
+.coverage
+htmlcov
diff --git a/docs/src/first_steps/tutorial006.py b/docs/src/first_steps/tutorial006.py
new file mode 100644
index 0000000..d373bbc
--- /dev/null
+++ b/docs/src/first_steps/tutorial006.py
@@ -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)
diff --git a/docs/tutorial/first-steps.md b/docs/tutorial/first-steps.md
index 9e0fa70..9231626 100644
--- a/docs/tutorial/first-steps.md
+++ b/docs/tutorial/first-steps.md
@@ -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.
```
@@ -302,6 +304,44 @@ Hello Camila Gutiérrez
+## Document your CLI app
+
+If you add a docstring 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:
+
+
+
+```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.
+```
+
+
+
+!!! 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.
diff --git a/pyproject.toml b/pyproject.toml
index ef73bc9..d72b45d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,6 +39,7 @@ Documentation = "https://typer.tiangolo.com/"
test = [
"pytest >=4.0.0",
"pytest-cov",
+ "coverage",
"mypy",
"black",
"isort"
diff --git a/tests/test_tutorial/test_first_steps/test_tutorial001.py b/tests/test_tutorial/test_first_steps/test_tutorial001.py
index d7b8473..0184c42 100644
--- a/tests/test_tutorial/test_first_steps/test_tutorial001.py
+++ b/tests/test_tutorial/test_first_steps/test_tutorial001.py
@@ -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",
diff --git a/tests/test_tutorial/test_first_steps/test_tutorial002.py b/tests/test_tutorial/test_first_steps/test_tutorial002.py
index 9c70a3e..080b4f0 100644
--- a/tests/test_tutorial/test_first_steps/test_tutorial002.py
+++ b/tests/test_tutorial/test_first_steps/test_tutorial002.py
@@ -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",
diff --git a/tests/test_tutorial/test_first_steps/test_tutorial003.py b/tests/test_tutorial/test_first_steps/test_tutorial003.py
index bf0038d..cccc5c2 100644
--- a/tests/test_tutorial/test_first_steps/test_tutorial003.py
+++ b/tests/test_tutorial/test_first_steps/test_tutorial003.py
@@ -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",
diff --git a/tests/test_tutorial/test_first_steps/test_tutorial004.py b/tests/test_tutorial/test_first_steps/test_tutorial004.py
index e00f5a2..5415f4c 100644
--- a/tests/test_tutorial/test_first_steps/test_tutorial004.py
+++ b/tests/test_tutorial/test_first_steps/test_tutorial004.py
@@ -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",
diff --git a/tests/test_tutorial/test_first_steps/test_tutorial005.py b/tests/test_tutorial/test_first_steps/test_tutorial005.py
index 4aa2e72..6d96399 100644
--- a/tests/test_tutorial/test_first_steps/test_tutorial005.py
+++ b/tests/test_tutorial/test_first_steps/test_tutorial005.py
@@ -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",
diff --git a/tests/test_tutorial/test_first_steps/test_tutorial006.py b/tests/test_tutorial/test_first_steps/test_tutorial006.py
new file mode 100644
index 0000000..12ec281
--- /dev/null
+++ b/tests/test_tutorial/test_first_steps/test_tutorial006.py
@@ -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
diff --git a/typer/completion.py b/typer/completion.py
index 4823137..bb7cba6 100644
--- a/typer/completion.py
+++ b/typer/completion.py
@@ -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
diff --git a/typer/main.py b/typer/main.py
index 8ae0efa..78e510d 100644
--- a/typer/main.py
+++ b/typer/main.py
@@ -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,