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
+```
+
+
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
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
):