📝 Add docs for Callbacks and One or Multiple commands, with docs, tests, bug fixes.

This commit is contained in:
Sebastián Ramírez 2020-01-05 21:50:31 +01:00 committed by GitHub
commit 439fb06fb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 585 additions and 12 deletions

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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>

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
):