From 56bb2576c4758cde889faedd030b70e1418a58f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 5 Jan 2020 21:37:44 +0100 Subject: [PATCH 1/3] :memo: Add docs for callbacks and one or multiple commands --- docs/src/commands/callback/tutorial001.py | 36 ++++ docs/src/commands/callback/tutorial002.py | 17 ++ docs/src/commands/callback/tutorial003.py | 22 +++ docs/src/commands/callback/tutorial004.py | 23 +++ .../commands/one_or_multiple/tutorial001.py | 17 ++ .../commands/one_or_multiple/tutorial002.py | 21 +++ docs/tutorial/commands/callback.md | 164 ++++++++++++++++++ docs/tutorial/commands/one-or-multiple.md | 84 +++++++-- 8 files changed, 372 insertions(+), 12 deletions(-) create mode 100644 docs/src/commands/callback/tutorial001.py create mode 100644 docs/src/commands/callback/tutorial002.py create mode 100644 docs/src/commands/callback/tutorial003.py create mode 100644 docs/src/commands/callback/tutorial004.py create mode 100644 docs/src/commands/one_or_multiple/tutorial001.py create mode 100644 docs/src/commands/one_or_multiple/tutorial002.py create mode 100644 docs/tutorial/commands/callback.md diff --git a/docs/src/commands/callback/tutorial001.py b/docs/src/commands/callback/tutorial001.py new file mode 100644 index 0000000..26eff8b --- /dev/null +++ b/docs/src/commands/callback/tutorial001.py @@ -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() diff --git a/docs/src/commands/callback/tutorial002.py b/docs/src/commands/callback/tutorial002.py new file mode 100644 index 0000000..4ab4d53 --- /dev/null +++ b/docs/src/commands/callback/tutorial002.py @@ -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() diff --git a/docs/src/commands/callback/tutorial003.py b/docs/src/commands/callback/tutorial003.py new file mode 100644 index 0000000..72975de --- /dev/null +++ b/docs/src/commands/callback/tutorial003.py @@ -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() diff --git a/docs/src/commands/callback/tutorial004.py b/docs/src/commands/callback/tutorial004.py new file mode 100644 index 0000000..4bc2b0c --- /dev/null +++ b/docs/src/commands/callback/tutorial004.py @@ -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() diff --git a/docs/src/commands/one_or_multiple/tutorial001.py b/docs/src/commands/one_or_multiple/tutorial001.py new file mode 100644 index 0000000..d0ad115 --- /dev/null +++ b/docs/src/commands/one_or_multiple/tutorial001.py @@ -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() diff --git a/docs/src/commands/one_or_multiple/tutorial002.py b/docs/src/commands/one_or_multiple/tutorial002.py new file mode 100644 index 0000000..27ac47f --- /dev/null +++ b/docs/src/commands/one_or_multiple/tutorial002.py @@ -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() diff --git a/docs/tutorial/commands/callback.md b/docs/tutorial/commands/callback.md new file mode 100644 index 0000000..904291b --- /dev/null +++ b/docs/tutorial/commands/callback.md @@ -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: + +
+ +```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 +``` + +
+ +## 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: + +
+ +```console +$ python main.py create Camila + +Running a command +Creating user: Camila +``` + +
+ +## 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: + +
+ +```console +$ python main.py create Camila + +// Notice that the message is the one from new_callback() +Override callback, running a command +Creating user: Camila +``` + +
+ +## 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: + +
+ +```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 +``` + +
diff --git a/docs/tutorial/commands/one-or-multiple.md b/docs/tutorial/commands/one-or-multiple.md index cab0da3..bf80faa 100644 --- a/docs/tutorial/commands/one-or-multiple.md +++ b/docs/tutorial/commands/one-or-multiple.md @@ -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 -!!! 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: + +
+ +```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 +``` + +
+ +## 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: + +
+ +```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 +``` + +
From d0ee82ba82a52aeef4d122d097277a33f396a5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 5 Jan 2020 21:38:57 +0100 Subject: [PATCH 2/3] :white_check_mark: Add tests for commands: callbacks --- .../test_commands/test_callback/__init__.py | 0 .../test_callback/test_tutorial001.py | 61 +++++++++++++++++++ .../test_callback/test_tutorial002.py | 25 ++++++++ .../test_callback/test_tutorial003.py | 30 +++++++++ .../test_callback/test_tutorial004.py | 32 ++++++++++ .../test_one_or_multiple/__init__.py | 0 .../test_one_or_multiple/test_tutorial001.py | 31 ++++++++++ .../test_one_or_multiple/test_tutorial002.py | 33 ++++++++++ 8 files changed, 212 insertions(+) create mode 100644 tests/test_tutorial/test_commands/test_callback/__init__.py create mode 100644 tests/test_tutorial/test_commands/test_callback/test_tutorial001.py create mode 100644 tests/test_tutorial/test_commands/test_callback/test_tutorial002.py create mode 100644 tests/test_tutorial/test_commands/test_callback/test_tutorial003.py create mode 100644 tests/test_tutorial/test_commands/test_callback/test_tutorial004.py create mode 100644 tests/test_tutorial/test_commands/test_one_or_multiple/__init__.py create mode 100644 tests/test_tutorial/test_commands/test_one_or_multiple/test_tutorial001.py create mode 100644 tests/test_tutorial/test_commands/test_one_or_multiple/test_tutorial002.py diff --git a/tests/test_tutorial/test_commands/test_callback/__init__.py b/tests/test_tutorial/test_commands/test_callback/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_commands/test_callback/test_tutorial001.py b/tests/test_tutorial/test_commands/test_callback/test_tutorial001.py new file mode 100644 index 0000000..ef62c49 --- /dev/null +++ b/tests/test_tutorial/test_commands/test_callback/test_tutorial001.py @@ -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 diff --git a/tests/test_tutorial/test_commands/test_callback/test_tutorial002.py b/tests/test_tutorial/test_commands/test_callback/test_tutorial002.py new file mode 100644 index 0000000..9df721f --- /dev/null +++ b/tests/test_tutorial/test_commands/test_callback/test_tutorial002.py @@ -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 diff --git a/tests/test_tutorial/test_commands/test_callback/test_tutorial003.py b/tests/test_tutorial/test_commands/test_callback/test_tutorial003.py new file mode 100644 index 0000000..7097e7c --- /dev/null +++ b/tests/test_tutorial/test_commands/test_callback/test_tutorial003.py @@ -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 diff --git a/tests/test_tutorial/test_commands/test_callback/test_tutorial004.py b/tests/test_tutorial/test_commands/test_callback/test_tutorial004.py new file mode 100644 index 0000000..33c4166 --- /dev/null +++ b/tests/test_tutorial/test_commands/test_callback/test_tutorial004.py @@ -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 diff --git a/tests/test_tutorial/test_commands/test_one_or_multiple/__init__.py b/tests/test_tutorial/test_commands/test_one_or_multiple/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_tutorial/test_commands/test_one_or_multiple/test_tutorial001.py b/tests/test_tutorial/test_commands/test_one_or_multiple/test_tutorial001.py new file mode 100644 index 0000000..582e16e --- /dev/null +++ b/tests/test_tutorial/test_commands/test_one_or_multiple/test_tutorial001.py @@ -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 diff --git a/tests/test_tutorial/test_commands/test_one_or_multiple/test_tutorial002.py b/tests/test_tutorial/test_commands/test_one_or_multiple/test_tutorial002.py new file mode 100644 index 0000000..02b60c8 --- /dev/null +++ b/tests/test_tutorial/test_commands/test_one_or_multiple/test_tutorial002.py @@ -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 From 6ccdbb85389f8d083d9320da441a0c1e98f5e7dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 5 Jan 2020 21:42:35 +0100 Subject: [PATCH 3/3] :bug: Fix getting callback from instance creation --- typer/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/typer/main.py b/typer/main.py index 4c6a4cc..57655ba 100644 --- a/typer/main.py +++ b/typer/main.py @@ -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 ):