Skip to content

Commit

Permalink
🔥 Remove auto naming of groups added via add_typer based on the gro…
Browse files Browse the repository at this point in the history
…up's callback function name (#1052)

Co-authored-by: svlandeg <[email protected]>
Co-authored-by: Sebastián Ramírez <[email protected]>
  • Loading branch information
3 people authored Nov 28, 2024
1 parent 45cbfcd commit 2d1a598
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 75 deletions.
92 changes: 47 additions & 45 deletions docs/tutorial/subcommands/name-and-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ app.add_typer(users.app, name="users")

## Add a help text

We can also set the `help` while adding a Typer:
We can also set the `help` text while adding a Typer:

{* docs_src/subcommands/name_help/tutorial001.py hl[6] *}

Expand Down Expand Up @@ -48,7 +48,7 @@ Commands:

</div>

We can set the `name` and `help` in several places, each one taking precedence over the other, overriding the previous value.
We can set the `help` in several places, each one taking precedence over the other, overriding the previous value.

Let's see those locations.

Expand All @@ -60,9 +60,9 @@ But those are documented later in another section.

///

## Inferring name and help from callback
## Inferring help text from callback

### Inferring a command's name and help
### Inferring a command's help text

When you create a command with `@app.command()`, by default, it generates the name from the function name.

Expand All @@ -81,23 +81,15 @@ def create(item: str):

...will create a command `create` with a help text of `Create an item`.

### Inferring name and help from `@app.callback()`
### Inferring the help text from `@app.callback()`

The same way, if you define a callback in a `typer.Typer()`, the help text is extracted from the callback function's docstring.

And if that Typer app is added to another Typer app, the default name of the command is generated from the name of the callback function.

Here's an example:

{* docs_src/subcommands/name_help/tutorial002.py hl[6,9,10,11,12,13] *}

Notice that now we added the sub-Typer without specifying a `name` nor a `help`.

They are now inferred from the callback function.
{* docs_src/subcommands/name_help/tutorial002.py hl[9,10,11,12,13] *}

The command name will be the same callback function's name: `users`.

And the help text for that `users` command will be the callback function's docstring: `Manage users in the app.`.
The help text for that command will be the callback function's docstring: `Manage users in the app.`.

Check it:

Expand All @@ -107,7 +99,7 @@ Check it:
// Check the main help
$ python main.py --help

// Notice the command name "users" and the help text "Manage users in the app."
// Notice the help text "Manage users in the app."
Usage: main.py [OPTIONS] COMMAND [ARGS]...

Options:
Expand Down Expand Up @@ -135,9 +127,15 @@ Commands:

</div>

### Name and help from callback parameter in `typer.Typer()`
/// note

Before Typer 0.14.0, in addition to the help text, the command name was also inferred from the callback function name, this is no longer the case.

///

### Help from callback parameter in `typer.Typer()`

If you pass a `callback` parameter while creating a `typer.Typer(callback=some_function)` it will be used to infer the name and help text.
If you pass a `callback` parameter while creating a `typer.Typer(callback=some_function)` it will be used to infer the help text.

This has the lowest priority, we'll see later what has a higher priority and can override it.

Expand All @@ -155,7 +153,7 @@ Check it:
// Check the main help
$ python main.py --help

// Notice the command name "users" and the help text "Manage users in the app."
// Notice the help text "Manage users in the app."
Usage: main.py [OPTIONS] COMMAND [ARGS]...

Options:
Expand Down Expand Up @@ -185,11 +183,11 @@ Commands:

### Override a callback set in `typer.Typer()` with `@app.callback()`

The same as with normal **Typer** apps, if you pass a `callback` to `typer.Typer(callback=some_function)` and then override it with `@app.callback()`, the name and help text will be inferred from the new callback:
The same as with normal **Typer** apps, if you pass a `callback` to `typer.Typer(callback=some_function)` and then override it with `@app.callback()`, the help text will be inferred from the new callback:

{* docs_src/subcommands/name_help/tutorial004.py hl[16,17,18,19,20] *}

Now the name of the command will be `users` instead of `old-callback`, and the help text will be `Manage users in the app.` instead of `Old callback help.`.
Now the help text will be `Manage users in the app.` instead of `Old callback help.`.

Check it:

Expand All @@ -199,7 +197,7 @@ Check it:
// Check the main help
$ python main.py --help

// Notice the command name "users" and the help text "Manage users in the app."
// Notice the help text "Manage users in the app."
Usage: main.py [OPTIONS] COMMAND [ARGS]...

Options:
Expand Down Expand Up @@ -227,17 +225,17 @@ Commands:

</div>

### Infer name and help from callback in `app.add_typer()`
### Help from callback in `app.add_typer()`

If you override the callback in `app.add_typer()` when including a sub-app, the name and help will be inferred from this callback function.
If you override the callback in `app.add_typer()` when including a sub-app, the help will be inferred from this callback function.

This takes precedence over inferring the name and help from a callback set in `@sub_app.callback()` and `typer.Typer(callback=sub_app_callback)`.
This takes precedence over inferring the help from a callback set in `@sub_app.callback()` and `typer.Typer(callback=sub_app_callback)`.

Check the code:

{* docs_src/subcommands/name_help/tutorial005.py hl[15,16,17,18,21] *}

Now the command will be `new-users` instead of `users`. And the help text will be `I have the highland! Create some users.` instead of the previous ones.
The help text will be `I have the highland! Create some users.` instead of the previous ones.

Check it:

Expand Down Expand Up @@ -277,29 +275,29 @@ Commands:

### Enough inferring

So, when inferring a name and help text, the precedence order from lowest priority to highest is:
So, when inferring help text, the precedence order from lowest priority to highest is:

* `sub_app = typer.Typer(callback=some_function)`
* `@sub_app.callback()`
* `app.add_typer(sub_app, callback=new_function)`

That's for inferring the name and help text from functions.
That's for inferring the help text from functions.

But if you set the name and help text explicitly, that has a higher priority than these.
But if you set the help text explicitly, that has a higher priority than these.

## Set the name and help

Let's now see the places where you can set the command name and help text, from lowest priority to highest.

/// tip

Setting the name and help text explicitly always has a higher precedence than inferring from a callback function.
Setting the help text explicitly always has a higher precedence than inferring from a callback function.

///

### Name and help in `typer.Typer()`

You could have all the callbacks and overrides we defined before, but the name and help text was inferred from the function name and docstring.
You could have all the callbacks and overrides we defined before, but the help text was inferred from the function docstring.

If you set it explicitly, that takes precedence over inferring.

Expand All @@ -313,7 +311,7 @@ The rest of the callbacks and overrides are there only to show you that they don

///

We set an explicit name `exp-users`, and an explicit help `Explicit help.`.
We set an explicit help `Explicit help.`.

So that will take precedence now.

Expand Down Expand Up @@ -353,15 +351,15 @@ Commands:

</div>

### Name and help in `@app.callback()`
### Help text in `@app.callback()`

Any parameter that you use when creating a `typer.Typer()` app can be overridden in the parameters of `@app.callback()`.
Many parameters that you use when creating a `typer.Typer()` app can be overridden in the parameters of `@app.callback()`.

Continuing with the previous example, we now override the values in `@user_app.callback()`:
Continuing with the previous example, we now override the `help` in `@user_app.callback()`:

{* docs_src/subcommands/name_help/tutorial007.py hl[24] *}

And now the command name will be `call-users` and the help text will be `Help from callback for users.`.
And now the help text will be `Help from callback for users.`.

Check it:

Expand All @@ -371,7 +369,7 @@ Check it:
// Check the help
$ python main.py --help

// The command name now is call-users and the help text is "Help from callback for users.".
// The help text is now "Help from callback for users.".
Usage: main.py [OPTIONS] COMMAND [ARGS]...

Options:
Expand All @@ -380,13 +378,13 @@ Options:
--help Show this message and exit.

Commands:
call-users Help from callback for users.
users Help from callback for users.

// Check the call-users command help
$ python main.py call-users --help
// Check the users command help
$ python main.py users --help

// Notice the main help text
Usage: main.py call-users [OPTIONS] COMMAND [ARGS]...
Usage: main.py users [OPTIONS] COMMAND [ARGS]...

Help from callback for users.

Expand Down Expand Up @@ -445,13 +443,17 @@ Commands:

## Recap

The precedence to generate a command's name and help, from lowest priority to highest, is:
The precedence to generate a command's **help**, from lowest priority to highest, is:

* Implicitly inferred from `sub_app = typer.Typer(callback=some_function)`
* Implicitly inferred from the callback function under `@sub_app.callback()`
* Implicitly inferred from `app.add_typer(sub_app, callback=some_function)`
* Explicitly set on `sub_app = typer.Typer(name="some-name", help="Some help.")`
* Explicitly set on `@sub_app.callback("some-name", help="Some help.")`
* Explicitly set on `app.add_typer(sub_app, name="some-name", help="Some help.")`
* Explicitly set on `sub_app = typer.Typer(help="Some help.")`
* Explicitly set on `app.add_typer(sub_app, help="Some help.")`

And the priority to set the command's **name**, from lowest priority to highest, is:

* Explicitly set on `sub_app = typer.Typer(name="some-name")`
* Explicitly set on `app.add_typer(sub_app, name="some-name")`

So, `app.add_typer(sub_app, name="some-name", help="Some help.")` always wins.
2 changes: 1 addition & 1 deletion docs_src/subcommands/name_help/tutorial002.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
app = typer.Typer()

users_app = typer.Typer()
app.add_typer(users_app)
app.add_typer(users_app, name="users")


@users_app.callback()
Expand Down
2 changes: 1 addition & 1 deletion docs_src/subcommands/name_help/tutorial003.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def users():
"""


users_app = typer.Typer(callback=users)
users_app = typer.Typer(callback=users, name="users")
app.add_typer(users_app)


Expand Down
2 changes: 1 addition & 1 deletion docs_src/subcommands/name_help/tutorial004.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def old_callback():


users_app = typer.Typer(callback=old_callback)
app.add_typer(users_app)
app.add_typer(users_app, name="users")


@users_app.callback()
Expand Down
4 changes: 2 additions & 2 deletions docs_src/subcommands/name_help/tutorial005.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def old_callback():
"""


users_app = typer.Typer(callback=old_callback)
users_app = typer.Typer(callback=old_callback, name="users")


def new_users():
Expand All @@ -18,7 +18,7 @@ def new_users():
"""


app.add_typer(users_app, callback=new_users)
app.add_typer(users_app, callback=new_users, name="new-users")


@users_app.callback()
Expand Down
4 changes: 2 additions & 2 deletions docs_src/subcommands/name_help/tutorial007.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def old_callback():
"""


users_app = typer.Typer(callback=old_callback, name="exp-users", help="Explicit help.")
users_app = typer.Typer(callback=old_callback, name="users", help="Explicit help.")


def new_users():
Expand All @@ -21,7 +21,7 @@ def new_users():
app.add_typer(users_app, callback=new_users)


@users_app.callback("call-users", help="Help from callback for users.")
@users_app.callback(help="Help from callback for users.")
def users():
"""
Manage users in the app.
Expand Down
2 changes: 1 addition & 1 deletion docs_src/subcommands/name_help/tutorial008.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def new_users():
)


@users_app.callback("call-users", help="Help from callback for users.")
@users_app.callback(help="Help from callback for users.")
def users():
"""
Manage users in the app.
Expand Down
44 changes: 44 additions & 0 deletions tests/test_callback_warning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest
import typer
from typer.testing import CliRunner

runner = CliRunner()


def test_warns_when_callback_is_not_supported():
app = typer.Typer()

sub_app = typer.Typer()

@sub_app.callback()
def callback():
"""This help text is not used."""

app.add_typer(sub_app)

with pytest.warns(
match="The 'callback' parameter is not supported by Typer when using `add_typer` without a name"
):
try:
app()
except SystemExit:
pass


def test_warns_when_callback_is_not_supported_added_after_add_typer():
app = typer.Typer()

sub_app = typer.Typer()
app.add_typer(sub_app)

@sub_app.callback()
def callback():
"""This help text is not used."""

with pytest.warns(
match="The 'callback' parameter is not supported by Typer when using `add_typer` without a name"
):
try:
app()
except SystemExit:
pass
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ def test_help():
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "Commands" in result.output
assert "call-users" in result.output
assert "users" in result.output
assert "Help from callback for users." in result.output


def test_command_help():
result = runner.invoke(app, ["call-users", "--help"])
result = runner.invoke(app, ["users", "--help"])
assert result.exit_code == 0
assert "Help from callback for users." in result.output


def test_command():
result = runner.invoke(app, ["call-users", "create", "Camila"])
result = runner.invoke(app, ["users", "create", "Camila"])
assert result.exit_code == 0
assert "Creating user: Camila" in result.output

Expand Down
Loading

0 comments on commit 2d1a598

Please sign in to comment.