📝 Add docs for Callbacks and One or Multiple commands, with docs, tests, bug fixes.
This commit is contained in:
commit
439fb06fb8
17 changed files with 585 additions and 12 deletions
36
docs/src/commands/callback/tutorial001.py
Normal file
36
docs/src/commands/callback/tutorial001.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
import typer
|
||||
|
||||
app = typer.Typer()
|
||||
state = {"verbose": False}
|
||||
|
||||
|
||||
@app.command()
|
||||
def create(username: str):
|
||||
if state["verbose"]:
|
||||
typer.echo("About to create a user")
|
||||
typer.echo(f"Creating user: {username}")
|
||||
if state["verbose"]:
|
||||
typer.echo("Just created a user")
|
||||
|
||||
|
||||
@app.command()
|
||||
def delete(username: str):
|
||||
if state["verbose"]:
|
||||
typer.echo("About to delete a user")
|
||||
typer.echo(f"Deleting user: {username}")
|
||||
if state["verbose"]:
|
||||
typer.echo("Just deleted a user")
|
||||
|
||||
|
||||
@app.callback()
|
||||
def main(verbose: bool = False):
|
||||
"""
|
||||
Manage users in the awesome CLI app.
|
||||
"""
|
||||
if verbose:
|
||||
typer.echo("Will write verbose output")
|
||||
state["verbose"] = True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
17
docs/src/commands/callback/tutorial002.py
Normal file
17
docs/src/commands/callback/tutorial002.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
import typer
|
||||
|
||||
|
||||
def callback():
|
||||
typer.echo("Running a command")
|
||||
|
||||
|
||||
app = typer.Typer(callback=callback)
|
||||
|
||||
|
||||
@app.command()
|
||||
def create(name: str):
|
||||
typer.echo(f"Creating user: {name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
22
docs/src/commands/callback/tutorial003.py
Normal file
22
docs/src/commands/callback/tutorial003.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
import typer
|
||||
|
||||
|
||||
def callback():
|
||||
typer.echo("Running a command")
|
||||
|
||||
|
||||
app = typer.Typer(callback=callback)
|
||||
|
||||
|
||||
@app.callback()
|
||||
def new_callback():
|
||||
typer.echo("Override callback, running a command")
|
||||
|
||||
|
||||
@app.command()
|
||||
def create(name: str):
|
||||
typer.echo(f"Creating user: {name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
23
docs/src/commands/callback/tutorial004.py
Normal file
23
docs/src/commands/callback/tutorial004.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
import typer
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
@app.callback()
|
||||
def callback():
|
||||
"""
|
||||
Manage users CLI app.
|
||||
|
||||
Use it with the create command.
|
||||
|
||||
A new user with the given NAME will be created.
|
||||
"""
|
||||
|
||||
|
||||
@app.command()
|
||||
def create(name: str):
|
||||
typer.echo(f"Creating user: {name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
17
docs/src/commands/one_or_multiple/tutorial001.py
Normal file
17
docs/src/commands/one_or_multiple/tutorial001.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
import typer
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
@app.command()
|
||||
def create():
|
||||
typer.echo("Creating user: Hiro Hamada")
|
||||
|
||||
|
||||
@app.callback()
|
||||
def callback():
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
21
docs/src/commands/one_or_multiple/tutorial002.py
Normal file
21
docs/src/commands/one_or_multiple/tutorial002.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
import typer
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
@app.command()
|
||||
def create():
|
||||
typer.echo("Creating user: Hiro Hamada")
|
||||
|
||||
|
||||
@app.callback()
|
||||
def callback():
|
||||
"""
|
||||
Creates a single user Hiro Hamada.
|
||||
|
||||
In the next version it will create 5 users more.
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
164
docs/tutorial/commands/callback.md
Normal file
164
docs/tutorial/commands/callback.md
Normal file
|
@ -0,0 +1,164 @@
|
|||
When you create an `app = typer.Typer()` it works as a group of commands.
|
||||
|
||||
And you can create multiple commands with it.
|
||||
|
||||
Each of those commands can have their own *CLI parameters*.
|
||||
|
||||
But as those *CLI parameters* are handled by each of those commands, they don't allow us to create *CLI parameters* for the main CLI application itself.
|
||||
|
||||
But we can use `@app.callback()` for that.
|
||||
|
||||
It's very similar to `@app.command()`, but it declares the *CLI parameters* for the main CLI application (before the commands):
|
||||
|
||||
```Python hl_lines="25 26 27 28 29 30 31 32"
|
||||
{!./src/commands/callback/tutorial001.py!}
|
||||
```
|
||||
|
||||
Here we create a `callback` with a `--verbose` *CLI option*.
|
||||
|
||||
!!! tip
|
||||
After getting the `--verbose` flag, we modify a global `state`, and we use it in the other commands.
|
||||
|
||||
There are other ways to achieve the same, but this will suffice for this example.
|
||||
|
||||
And as we added a docstring to the callback function, by default it will be extracted and used as the help text.
|
||||
|
||||
Check it:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// Check the help
|
||||
$ python main.py --help
|
||||
|
||||
// Notice the main help text, extracted from the callback function: "Manage users in the awesome CLI app."
|
||||
Usage: main.py [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Manage users in the awesome CLI app.
|
||||
|
||||
Options:
|
||||
--verbose / --no-verbose
|
||||
--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.
|
||||
|
||||
Commands:
|
||||
create
|
||||
delete
|
||||
|
||||
// Check the new top level CLI option --verbose
|
||||
|
||||
// Try it normally
|
||||
$ python main.py create Camila
|
||||
|
||||
Creating user: Camila
|
||||
|
||||
// And now with --verbose
|
||||
$ python main.py --verbose create Camila
|
||||
|
||||
Will write verbose output
|
||||
About to create a user
|
||||
Creating user: Camila
|
||||
Just created a user
|
||||
|
||||
// Notice that --verbose belongs to the callback, it has to go before create or delete ⛔️
|
||||
$ python main.py create --verbose Camila
|
||||
|
||||
Usage: main.py create [OPTIONS] USERNAME
|
||||
Try "main.py create --help" for help.
|
||||
|
||||
Error: no such option: --verbose
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Adding a callback on creation
|
||||
|
||||
It's also possible to add a callback when creating the `typer.Typer()` app:
|
||||
|
||||
```Python hl_lines="4 5 8"
|
||||
{!./src/commands/callback/tutorial002.py!}
|
||||
```
|
||||
|
||||
That achieves the same as with `@app.callback()`.
|
||||
|
||||
Check it:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python main.py create Camila
|
||||
|
||||
Running a command
|
||||
Creating user: Camila
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Overriding a callback
|
||||
|
||||
If you added a callback when creating the `typer.Typer()` app, it's possible to override it with `@app.callback()`:
|
||||
|
||||
```Python hl_lines="11 12 13"
|
||||
{!./src/commands/callback/tutorial003.py!}
|
||||
```
|
||||
|
||||
Now `new_callback()` will be the one used.
|
||||
|
||||
Check it:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python main.py create Camila
|
||||
|
||||
// Notice that the message is the one from new_callback()
|
||||
Override callback, running a command
|
||||
Creating user: Camila
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Adding a callback only for documentation
|
||||
|
||||
You can also add a callback just to add the documentation in the docstring.
|
||||
|
||||
It can be convenient especially if you have several lines of text, as the indentation will be automatically handled for you:
|
||||
|
||||
```Python hl_lines="8 9 10 11 12 13 14 15 16"
|
||||
{!./src/commands/callback/tutorial004.py!}
|
||||
```
|
||||
|
||||
Now the callback will be used mainly to extract the docstring for the help text.
|
||||
|
||||
Check it:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python main.py --help
|
||||
|
||||
// Notice all the help text extracted from the callback docstring
|
||||
Usage: main.py [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Manage users CLI app.
|
||||
|
||||
Use it with the create command.
|
||||
|
||||
A new user with the given NAME will be created.
|
||||
|
||||
Options:
|
||||
--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.
|
||||
|
||||
Commands:
|
||||
create
|
||||
|
||||
// And it just works as normally
|
||||
$ python main.py create Camila
|
||||
|
||||
Creating user: Camila
|
||||
```
|
||||
|
||||
</div>
|
|
@ -1,7 +1,7 @@
|
|||
You might have noticed that if you create a single command, as in the first example:
|
||||
|
||||
```Python hl_lines="3 6 12"
|
||||
{!./src/commands/tutorial001.py!}
|
||||
{!./src/commands/index/tutorial001.py!}
|
||||
```
|
||||
|
||||
**Typer** is smart enough to create a CLI application with that single function as the main CLI application, not as a command/subcommand:
|
||||
|
@ -41,7 +41,7 @@ Options:
|
|||
But if you add multiple commands, **Typer** will create one *CLI command* for each one of them:
|
||||
|
||||
```Python hl_lines="6 11"
|
||||
{!./src/commands/tutorial002.py!}
|
||||
{!./src/commands/index/tutorial002.py!}
|
||||
```
|
||||
|
||||
Here we have 2 commands `create` and `delete`:
|
||||
|
@ -75,17 +75,77 @@ Deleting user: Hiro Hamada
|
|||
|
||||
</div>
|
||||
|
||||
!!! tip
|
||||
This is **Typer**'s behavior by default, but you could still create a CLI application with one single command, like:
|
||||
## One command and one callback
|
||||
|
||||
```
|
||||
$ python main.py main
|
||||
```
|
||||
If you want to create a CLI app with one single command but you still want it to be a command/subcommand you can just add a callback:
|
||||
|
||||
instead of just:
|
||||
```Python hl_lines="11 12 13"
|
||||
{!./src/commands/one_or_multiple/tutorial001.py!}
|
||||
```
|
||||
|
||||
```
|
||||
$ python main.py
|
||||
```
|
||||
And now your CLI program will have a single command.
|
||||
|
||||
You will learn more about this in the section about application Callbacks.
|
||||
Check it:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
// Check the help
|
||||
$ python main.py --help
|
||||
|
||||
// Notice the single command create
|
||||
Usage: main.py [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
--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.
|
||||
|
||||
Commands:
|
||||
create
|
||||
|
||||
// Try it
|
||||
$ python main.py create
|
||||
|
||||
Creating user: Hiro Hamada
|
||||
```
|
||||
|
||||
</div>
|
||||
|
||||
## Using the callback to document
|
||||
|
||||
Now that you are using a callback just to have a single command, you might as well use it to add documentation for your app:
|
||||
|
||||
```Python hl_lines="11 12 13 14 15 16 17"
|
||||
{!./src/commands/one_or_multiple/tutorial002.py!}
|
||||
```
|
||||
|
||||
And now the docstring from the callback will be used as the help text:
|
||||
|
||||
<div class="termy">
|
||||
|
||||
```console
|
||||
$ python main.py --help
|
||||
|
||||
// Notice the help text from the docstring
|
||||
Usage: main.py [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Creates a single user Hiro Hamada.
|
||||
|
||||
In the next version it will create 5 users more.
|
||||
|
||||
Options:
|
||||
--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.
|
||||
|
||||
Commands:
|
||||
create
|
||||
|
||||
// And it still works the same, the callback does nothing
|
||||
$ python main.py create
|
||||
|
||||
Creating user: Hiro Hamada
|
||||
```
|
||||
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import subprocess
|
||||
from commands.callback import tutorial001 as mod
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
app = mod.app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_help():
|
||||
result = runner.invoke(app, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Manage users in the awesome CLI app." in result.output
|
||||
assert "--verbose / --no-verbose" in result.output
|
||||
|
||||
|
||||
def test_create():
|
||||
result = runner.invoke(app, ["create", "Camila"])
|
||||
assert result.exit_code == 0
|
||||
assert "Creating user: Camila" in result.output
|
||||
|
||||
|
||||
def test_create_verbose():
|
||||
result = runner.invoke(app, ["--verbose", "create", "Camila"])
|
||||
assert result.exit_code == 0
|
||||
assert "Will write verbose output" in result.output
|
||||
assert "About to create a user" in result.output
|
||||
assert "Creating user: Camila" in result.output
|
||||
assert "Just created a user" in result.output
|
||||
|
||||
|
||||
def test_delete():
|
||||
result = runner.invoke(app, ["delete", "Camila"])
|
||||
assert result.exit_code == 0
|
||||
assert "Deleting user: Camila" in result.output
|
||||
|
||||
|
||||
def test_delete_verbose():
|
||||
result = runner.invoke(app, ["--verbose", "delete", "Camila"])
|
||||
assert result.exit_code == 0
|
||||
assert "Will write verbose output" in result.output
|
||||
assert "About to delete a user" in result.output
|
||||
assert "Deleting user: Camila" in result.output
|
||||
assert "Just deleted a user" in result.output
|
||||
|
||||
|
||||
def test_wrong_verbose():
|
||||
result = runner.invoke(app, ["delete", "--verbose", "Camila"])
|
||||
assert result.exit_code != 0
|
||||
assert "Error: no such option: --verbose" 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
|
|
@ -0,0 +1,25 @@
|
|||
import subprocess
|
||||
from commands.callback import tutorial002 as mod
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
app = mod.app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_app():
|
||||
result = runner.invoke(app, ["create", "Camila"])
|
||||
assert result.exit_code == 0
|
||||
assert "Running a command" in result.output
|
||||
assert "Creating user: Camila" 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
|
|
@ -0,0 +1,30 @@
|
|||
import subprocess
|
||||
from commands.callback import tutorial003 as mod
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
app = mod.app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_app():
|
||||
result = runner.invoke(app, ["create", "Camila"])
|
||||
assert result.exit_code == 0
|
||||
assert "Override callback, running a command" in result.output
|
||||
assert "Running a command" not in result.output
|
||||
assert "Creating user: Camila" in result.output
|
||||
|
||||
|
||||
def test_for_coverage():
|
||||
mod.callback()
|
||||
|
||||
|
||||
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
|
|
@ -0,0 +1,32 @@
|
|||
import subprocess
|
||||
from commands.callback import tutorial004 as mod
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
app = mod.app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_help():
|
||||
result = runner.invoke(app, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Manage users CLI app." in result.output
|
||||
assert "Use it with the create command." in result.output
|
||||
assert "A new user with the given NAME will be created." in result.output
|
||||
|
||||
|
||||
def test_app():
|
||||
result = runner.invoke(app, ["create", "Camila"])
|
||||
assert result.exit_code == 0
|
||||
assert "Creating user: Camila" 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
|
|
@ -0,0 +1,31 @@
|
|||
import subprocess
|
||||
from commands.one_or_multiple import tutorial001 as mod
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
app = mod.app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_help():
|
||||
result = runner.invoke(app, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Commands:" in result.output
|
||||
assert "create" in result.output
|
||||
|
||||
|
||||
def test_command():
|
||||
result = runner.invoke(app, ["create"])
|
||||
assert result.exit_code == 0
|
||||
assert "Creating user: Hiro Hamada" 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
|
|
@ -0,0 +1,33 @@
|
|||
import subprocess
|
||||
from commands.one_or_multiple import tutorial002 as mod
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
app = mod.app
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def test_help():
|
||||
result = runner.invoke(app, ["--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "Creates a single user Hiro Hamada." in result.output
|
||||
assert "In the next version it will create 5 users more." in result.output
|
||||
assert "Commands:" in result.output
|
||||
assert "create" in result.output
|
||||
|
||||
|
||||
def test_command():
|
||||
result = runner.invoke(app, ["create"])
|
||||
assert result.exit_code == 0
|
||||
assert "Creating user: Hiro Hamada" 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
|
|
@ -226,6 +226,7 @@ def get_command(typer_instance: Typer) -> click.Command:
|
|||
click_install_param, click_show_param = get_install_completion_arguments()
|
||||
if (
|
||||
typer_instance.registered_callback
|
||||
or typer_instance.info.callback
|
||||
or typer_instance.registered_groups
|
||||
or len(typer_instance.registered_commands) > 1
|
||||
):
|
||||
|
|
Loading…
Reference in a new issue