diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 726859d673..e8474352be 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -57,7 +57,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index b25bbded80..d3b4918a13 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,7 +29,7 @@ jobs: with: python-version: "3.11" - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 57bbfe1f19..9705024026 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,4 +36,4 @@ jobs: TIANGOLO_BUILD_PACKAGE: ${{ matrix.package }} run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.10.3 + uses: pypa/gh-action-pypi-publish@v1.12.2 diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index de4f5c7a36..464f10aaad 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -27,7 +27,7 @@ jobs: with: python-version: '3.9' - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.15" enable-cache: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 133b4ffaa1..e339bd75bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.15" enable-cache: true @@ -83,7 +83,7 @@ jobs: with: python-version: '3.8' - name: Setup uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v4 with: version: "0.4.15" enable-cache: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3869328a72..080c58e259 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.3 + rev: v0.8.0 hooks: - id: ruff args: diff --git a/docs/release-notes.md b/docs/release-notes.md index d0dcf6d4c3..f390406a2b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,72 @@ ## Latest Changes +## 0.14.0 + +### Breaking Changes + +* 🔥 Remove auto naming of groups added via `add_typer` based on the group's callback function name. PR [#1052](https://github.com/fastapi/typer/pull/1052) by [@patrick91](https://github.com/patrick91). + +Before, it was supported to infer the name of a command group from the callback function name in the sub-app, so, in this code: + +```python +import typer + +app = typer.Typer() +users_app = typer.Typer() + +app.add_typer(users_app) + + +@users_app.callback() +def users(): # <-- This was the inferred command group name + """ + Manage users in the app. + """ + + +@users_app.command() +def create(name: str): + print(f"Creating user: {name}") +``` + +...the command group would be named `users`, based on the name of the function `def users()`. + +Now you need to set it explicitly: + +```python +import typer + +app = typer.Typer() +users_app = typer.Typer() + +app.add_typer(users_app, name="users") # <-- Explicitly set the command group name + + +@users_app.callback() +def users(): + """ + Manage users in the app. + """ + + +@users_app.command() +def create(name: str): + print(f"Creating user: {name}") +``` + +Updated docs [SubCommand Name and Help](https://typer.tiangolo.com/tutorial/subcommands/name-and-help/). + +**Note**: this change will enable important features in the next release. 🤩 + +### Internal + +* ⬆ Bump pypa/gh-action-pypi-publish from 1.10.3 to 1.12.2. PR [#1043](https://github.com/fastapi/typer/pull/1043) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mkdocs-material from 9.5.44 to 9.5.46. PR [#1062](https://github.com/fastapi/typer/pull/1062) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump ruff from 0.7.4 to 0.8.0. PR [#1059](https://github.com/fastapi/typer/pull/1059) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump astral-sh/setup-uv from 3 to 4. PR [#1061](https://github.com/fastapi/typer/pull/1061) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#1053](https://github.com/fastapi/typer/pull/1053) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). + ## 0.13.1 ### Features diff --git a/docs/tutorial/subcommands/name-and-help.md b/docs/tutorial/subcommands/name-and-help.md index 47eced3dd3..b50fc6db74 100644 --- a/docs/tutorial/subcommands/name-and-help.md +++ b/docs/tutorial/subcommands/name-and-help.md @@ -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] *} @@ -48,7 +48,7 @@ Commands: -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. @@ -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. @@ -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: @@ -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: @@ -135,9 +127,15 @@ Commands: -### 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. @@ -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: @@ -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: @@ -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: @@ -227,17 +225,17 @@ Commands: -### 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: @@ -277,15 +275,15 @@ 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 @@ -293,13 +291,13 @@ Let's now see the places where you can set the command name and help text, from /// 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. @@ -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. @@ -353,15 +351,15 @@ Commands: -### 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: @@ -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: @@ -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. @@ -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. diff --git a/docs_src/subcommands/name_help/tutorial002.py b/docs_src/subcommands/name_help/tutorial002.py index df48e075d4..ae9f1036b5 100644 --- a/docs_src/subcommands/name_help/tutorial002.py +++ b/docs_src/subcommands/name_help/tutorial002.py @@ -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() diff --git a/docs_src/subcommands/name_help/tutorial003.py b/docs_src/subcommands/name_help/tutorial003.py index c24d0e187f..b6c10857dc 100644 --- a/docs_src/subcommands/name_help/tutorial003.py +++ b/docs_src/subcommands/name_help/tutorial003.py @@ -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) diff --git a/docs_src/subcommands/name_help/tutorial004.py b/docs_src/subcommands/name_help/tutorial004.py index 92fbc376aa..f1f5aed5c0 100644 --- a/docs_src/subcommands/name_help/tutorial004.py +++ b/docs_src/subcommands/name_help/tutorial004.py @@ -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() diff --git a/docs_src/subcommands/name_help/tutorial005.py b/docs_src/subcommands/name_help/tutorial005.py index 6cedabfef4..50730171d7 100644 --- a/docs_src/subcommands/name_help/tutorial005.py +++ b/docs_src/subcommands/name_help/tutorial005.py @@ -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(): @@ -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() diff --git a/docs_src/subcommands/name_help/tutorial007.py b/docs_src/subcommands/name_help/tutorial007.py index 15544d89ee..aaf7aec9fc 100644 --- a/docs_src/subcommands/name_help/tutorial007.py +++ b/docs_src/subcommands/name_help/tutorial007.py @@ -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(): @@ -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. diff --git a/docs_src/subcommands/name_help/tutorial008.py b/docs_src/subcommands/name_help/tutorial008.py index 1063da2b7a..ddfc265051 100644 --- a/docs_src/subcommands/name_help/tutorial008.py +++ b/docs_src/subcommands/name_help/tutorial008.py @@ -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. diff --git a/requirements-docs.txt b/requirements-docs.txt index 4f2c9b2f7b..a89870603f 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -mkdocs-material==9.5.44 +mkdocs-material==9.5.46 mdx-include >=1.4.1,<2.0.0 mkdocs-redirects>=1.2.1,<1.3.0 pyyaml >=5.3.1,<7.0.0 diff --git a/requirements-tests.txt b/requirements-tests.txt index 264fe0becc..0d588af1db 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -6,7 +6,7 @@ coverage[toml] >=6.2,<8.0 pytest-xdist >=1.32.0,<4.0.0 pytest-sugar >=0.9.4,<1.1.0 mypy ==1.4.1 -ruff ==0.7.4 +ruff ==0.8.0 # Needed explicitly by typer-slim rich >=10.11.0 shellingham >=1.3.0 diff --git a/tests/test_callback_warning.py b/tests/test_callback_warning.py new file mode 100644 index 0000000000..af2d9724f1 --- /dev/null +++ b/tests/test_callback_warning.py @@ -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 diff --git a/tests/test_tutorial/test_subcommands/test_name_help/test_tutorial007.py b/tests/test_tutorial/test_subcommands/test_name_help/test_tutorial007.py index 3935dee2da..f0632fb5eb 100644 --- a/tests/test_tutorial/test_subcommands/test_name_help/test_tutorial007.py +++ b/tests/test_tutorial/test_subcommands/test_name_help/test_tutorial007.py @@ -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 diff --git a/typer/__init__.py b/typer/__init__.py index cb24d1c768..fca4e505ea 100644 --- a/typer/__init__.py +++ b/typer/__init__.py @@ -1,6 +1,6 @@ """Typer, build great CLIs. Easy to code. Based on Python type hints.""" -__version__ = "0.13.1" +__version__ = "0.14.0" from shutil import get_terminal_size as get_terminal_size diff --git a/typer/main.py b/typer/main.py index 55d865c780..a23c3c0f5d 100644 --- a/typer/main.py +++ b/typer/main.py @@ -184,7 +184,6 @@ def __init__( def callback( self, - name: Optional[str] = Default(None), *, cls: Optional[Type[TyperGroup]] = Default(None), invoke_without_command: bool = Default(False), @@ -206,7 +205,6 @@ def callback( ) -> Callable[[CommandFunctionType], CommandFunctionType]: def decorator(f: CommandFunctionType) -> CommandFunctionType: self.registered_callback = TyperInfo( - name=name, cls=cls, invoke_without_command=invoke_without_command, no_args_is_help=no_args_is_help, @@ -389,21 +387,6 @@ def get_command(typer_instance: Typer) -> click.Command: ) # pragma: no cover -def get_group_name(typer_info: TyperInfo) -> Optional[str]: - if typer_info.callback: - # Priority 1: Callback passed in app.add_typer() - return get_command_name(typer_info.callback.__name__) - if typer_info.typer_instance: - registered_callback = typer_info.typer_instance.registered_callback - if registered_callback: - if registered_callback.callback: - # Priority 2: Callback passed in @subapp.callback() - return get_command_name(registered_callback.callback.__name__) - if typer_info.typer_instance.info.callback: - return get_command_name(typer_info.typer_instance.info.callback.__name__) - return None - - def solve_typer_info_help(typer_info: TyperInfo) -> str: # Priority 1: Explicit value was set in app.add_typer() if not isinstance(typer_info.help, DefaultPlaceholder): @@ -480,8 +463,6 @@ def solve_typer_info_defaults(typer_info: TyperInfo) -> TyperInfo: pass # Value not set, use the default values[name] = value.value - if values["name"] is None: - values["name"] = get_group_name(typer_info) values["help"] = solve_typer_info_help(typer_info) return TyperInfo(**values) @@ -512,6 +493,13 @@ def get_group_from_info( ) if sub_group.name: commands[sub_group.name] = sub_group + elif sub_group.callback: + import warnings + + warnings.warn( + "The 'callback' parameter is not supported by Typer when using `add_typer` without a name", + stacklevel=5, + ) solved_info = solve_typer_info_defaults(group_info) ( params,