From 633589fd58d6912fa8e548e2c3953f69773cb3fb Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Sun, 23 Nov 2025 22:22:11 +0330 Subject: [PATCH 01/17] =?UTF-8?q?=E2=9C=A8=20Enhance=20command=20alias=20h?= =?UTF-8?q?andling=20and=20display=20in=20Typer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added support for positional and additional aliases in the command decorator. - Updated `format_commands` to display command names along with their visible aliases. - Improved command listing to maintain original order and avoid duplicates. - Enhanced command info structure to include aliases and hidden aliases. This improves the usability and clarity of command help output. --- tests/test_commands_aliases.py | 142 +++++++++++++++++++++++++++++++++ typer/core.py | 30 ++++++- typer/main.py | 18 ++++- typer/models.py | 4 + typer/rich_utils.py | 31 +++++-- 5 files changed, 216 insertions(+), 9 deletions(-) create mode 100644 tests/test_commands_aliases.py diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py new file mode 100644 index 0000000000..fd266aa8ab --- /dev/null +++ b/tests/test_commands_aliases.py @@ -0,0 +1,142 @@ +import typer +from typer.testing import CliRunner + +runner = CliRunner() + + +def test_command_aliases_positional(): + app = typer.Typer() + + @app.command("list", "ls") + def list_items(): + print("listed") + + @app.command("remove") + def remove_items(): + pass + + result = runner.invoke(app, ["list"]) + assert result.exit_code == 0 + assert "listed" in result.stdout + + result = runner.invoke(app, ["ls"]) + assert result.exit_code == 0 + assert "listed" in result.stdout + + +def test_command_aliases_keyword(): + app = typer.Typer() + + @app.command("list", aliases=["ls", "l"]) + def list_items(): + print("listed") + + @app.command("remove") + def remove_items(): + pass + + result = runner.invoke(app, ["list"]) + assert result.exit_code == 0 + assert "listed" in result.stdout + + result = runner.invoke(app, ["ls"]) + assert result.exit_code == 0 + assert "listed" in result.stdout + + result = runner.invoke(app, ["l"]) + assert result.exit_code == 0 + assert "listed" in result.stdout + + +def test_command_aliases_combined(): + app = typer.Typer() + + @app.command("list", "ls", aliases=["l"]) + def list_items(): + print("listed") + + @app.command("remove") + def remove_items(): + pass + + result = runner.invoke(app, ["list"]) + assert result.exit_code == 0 + assert "listed" in result.stdout + + result = runner.invoke(app, ["ls"]) + assert result.exit_code == 0 + assert "listed" in result.stdout + + result = runner.invoke(app, ["l"]) + assert result.exit_code == 0 + assert "listed" in result.stdout + + +def test_command_aliases_help_output(): + app = typer.Typer() + + @app.command("list", "ls") + def list_items(): + pass + + @app.command("remove", aliases=["rm", "delete"]) + def remove_items(): + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "list, ls" in result.stdout or "ls, list" in result.stdout + assert "remove, rm, delete" in result.stdout or "rm, delete, remove" in result.stdout + + +def test_command_hidden_aliases(): + app = typer.Typer() + + @app.command("list", "ls", hidden_aliases=["secretlist"]) + def list_items(): + pass + + @app.command("remove") + def remove_items(): + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "list, ls" in result.stdout or "ls, list" in result.stdout + assert "secretlist" not in result.stdout + + result = runner.invoke(app, ["secretlist"]) + assert result.exit_code == 0 + + +def test_command_aliases_subcommands(): + app = typer.Typer() + + @app.command("versions", "ver", "v") + def show_versions(): + print("versions") + + @app.command("documents", aliases=["docs"]) + def show_documents(): + print("documents") + + result = runner.invoke(app, ["versions"]) + assert result.exit_code == 0 + assert "versions" in result.stdout + + result = runner.invoke(app, ["ver"]) + assert result.exit_code == 0 + assert "versions" in result.stdout + + result = runner.invoke(app, ["v"]) + assert result.exit_code == 0 + assert "versions" in result.stdout + + result = runner.invoke(app, ["documents"]) + assert result.exit_code == 0 + assert "documents" in result.stdout + + result = runner.invoke(app, ["docs"]) + assert result.exit_code == 0 + assert "documents" in result.stdout + diff --git a/typer/core.py b/typer/core.py index e9631e56cf..baf79a23bd 100644 --- a/typer/core.py +++ b/typer/core.py @@ -764,6 +764,28 @@ def format_options( _typer_format_options(self, ctx=ctx, formatter=formatter) self.format_commands(ctx, formatter) + def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: + commands = [] + for name in self.list_commands(ctx): + cmd = self.get_command(ctx, name) + if cmd is None or cmd.hidden: + continue + aliases = getattr(cmd, "_typer_aliases", []) + hidden_aliases = getattr(cmd, "_typer_hidden_aliases", []) + visible_aliases = [a for a in aliases if a not in hidden_aliases] + + if visible_aliases: + cmd_name = ", ".join([name] + visible_aliases) + else: + cmd_name = name + + help_text = cmd.short_help or cmd.help or "" + commands.append((cmd_name, help_text)) + + if commands: + with formatter.section(_("Commands")): + formatter.write_dl(commands) + def _main_shell_completion( self, ctx_args: MutableMapping[str, Any], @@ -826,4 +848,10 @@ def list_commands(self, ctx: click.Context) -> List[str]: """Returns a list of subcommand names. Note that in Click's Group class, these are sorted. In Typer, we wish to maintain the original order of creation (cf Issue #933)""" - return [n for n, c in self.commands.items()] + seen = set() + result = [] + for name, cmd in self.commands.items(): + if cmd.name and cmd.name not in seen: + seen.add(cmd.name) + result.append(cmd.name) + return result diff --git a/typer/main.py b/typer/main.py index 71a25e6c4b..80c872bf41 100644 --- a/typer/main.py +++ b/typer/main.py @@ -216,7 +216,7 @@ def decorator(f: CommandFunctionType) -> CommandFunctionType: def command( self, name: Optional[str] = None, - *, + *positional_aliases: str, cls: Optional[Type[TyperCommand]] = None, context_settings: Optional[Dict[Any, Any]] = None, help: Optional[str] = None, @@ -227,12 +227,20 @@ def command( no_args_is_help: bool = False, hidden: bool = False, deprecated: bool = False, + aliases: Optional[Sequence[str]] = None, + hidden_aliases: Optional[Sequence[str]] = None, # Rich settings rich_help_panel: Union[str, None] = Default(None), ) -> Callable[[CommandFunctionType], CommandFunctionType]: if cls is None: cls = TyperCommand + all_aliases_list: List[str] = [] + if positional_aliases: + all_aliases_list.extend(positional_aliases) + if aliases: + all_aliases_list.extend(aliases) + def decorator(f: CommandFunctionType) -> CommandFunctionType: self.registered_commands.append( CommandInfo( @@ -248,6 +256,8 @@ def decorator(f: CommandFunctionType) -> CommandFunctionType: no_args_is_help=no_args_is_help, hidden=hidden, deprecated=deprecated, + aliases=all_aliases_list if all_aliases_list else None, + hidden_aliases=hidden_aliases, # Rich settings rich_help_panel=rich_help_panel, ) @@ -474,6 +484,10 @@ def get_group_from_info( ) if command.name: commands[command.name] = command + cmd_aliases = getattr(command, "_typer_aliases", []) + cmd_hidden_aliases = getattr(command, "_typer_hidden_aliases", []) + for alias in cmd_aliases + cmd_hidden_aliases: + commands[alias] = command for sub_group_info in group_info.typer_instance.registered_groups: sub_group = get_group_from_info( sub_group_info, @@ -598,6 +612,8 @@ def get_command_from_info( # Rich settings rich_help_panel=command_info.rich_help_panel, ) + command._typer_aliases = command_info.aliases or [] # type: ignore + command._typer_hidden_aliases = command_info.hidden_aliases or [] # type: ignore return command diff --git a/typer/models.py b/typer/models.py index e0bddb965b..1eabc0fd31 100644 --- a/typer/models.py +++ b/typer/models.py @@ -98,6 +98,8 @@ def __init__( no_args_is_help: bool = False, hidden: bool = False, deprecated: bool = False, + aliases: Optional[Sequence[str]] = None, + hidden_aliases: Optional[Sequence[str]] = None, # Rich settings rich_help_panel: Union[str, None] = None, ): @@ -113,6 +115,8 @@ def __init__( self.no_args_is_help = no_args_is_help self.hidden = hidden self.deprecated = deprecated + self.aliases = aliases or [] + self.hidden_aliases = hidden_aliases or [] # Rich settings self.rich_help_panel = rich_help_panel diff --git a/typer/rich_utils.py b/typer/rich_utils.py index d4c3676aea..a55718200a 100644 --- a/typer/rich_utils.py +++ b/typer/rich_utils.py @@ -515,12 +515,21 @@ def _print_commands_panel( deprecated_rows: List[Union[RenderableType, None]] = [] for command in commands: helptext = command.short_help or command.help or "" - command_name = command.name or "" + cmd_name = command.name or "" + aliases = getattr(command, "_typer_aliases", []) + hidden_aliases = getattr(command, "_typer_hidden_aliases", []) + visible_aliases = [a for a in aliases if a not in hidden_aliases] + + if visible_aliases: + cmd_name_display = ", ".join([cmd_name] + visible_aliases) + else: + cmd_name_display = cmd_name + if command.deprecated: - command_name_text = Text(f"{command_name}", style=STYLE_DEPRECATED_COMMAND) + command_name_text = Text(f"{cmd_name_display}", style=STYLE_DEPRECATED_COMMAND) deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)) else: - command_name_text = Text(command_name) + command_name_text = Text(cmd_name_display) deprecated_rows.append(None) rows.append( [ @@ -651,12 +660,20 @@ def rich_format_help( ) panel_to_commands[panel_name].append(command) - # Identify the longest command name in all panels max_cmd_len = max( [ - len(command.name or "") - for commands in panel_to_commands.values() - for command in commands + len( + ", ".join( + [cmd.name or ""] + + [ + a + for a in getattr(cmd, "_typer_aliases", []) + if a not in getattr(cmd, "_typer_hidden_aliases", []) + ] + ) + ) + for cmds in panel_to_commands.values() + for cmd in cmds ], default=0, ) From bd594d30e83bf57ec368c504b2a5672666f86fec Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:56:49 +0000 Subject: [PATCH 02/17] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_commands_aliases.py | 5 +++-- typer/core.py | 4 +++- typer/rich_utils.py | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index fd266aa8ab..b433ae1ed7 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -86,7 +86,9 @@ def remove_items(): result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 assert "list, ls" in result.stdout or "ls, list" in result.stdout - assert "remove, rm, delete" in result.stdout or "rm, delete, remove" in result.stdout + assert ( + "remove, rm, delete" in result.stdout or "rm, delete, remove" in result.stdout + ) def test_command_hidden_aliases(): @@ -139,4 +141,3 @@ def show_documents(): result = runner.invoke(app, ["docs"]) assert result.exit_code == 0 assert "documents" in result.stdout - diff --git a/typer/core.py b/typer/core.py index baf79a23bd..4c0e340f66 100644 --- a/typer/core.py +++ b/typer/core.py @@ -764,7 +764,9 @@ def format_options( _typer_format_options(self, ctx=ctx, formatter=formatter) self.format_commands(ctx, formatter) - def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: + def format_commands( + self, ctx: click.Context, formatter: click.HelpFormatter + ) -> None: commands = [] for name in self.list_commands(ctx): cmd = self.get_command(ctx, name) diff --git a/typer/rich_utils.py b/typer/rich_utils.py index a55718200a..67230eb157 100644 --- a/typer/rich_utils.py +++ b/typer/rich_utils.py @@ -526,7 +526,9 @@ def _print_commands_panel( cmd_name_display = cmd_name if command.deprecated: - command_name_text = Text(f"{cmd_name_display}", style=STYLE_DEPRECATED_COMMAND) + command_name_text = Text( + f"{cmd_name_display}", style=STYLE_DEPRECATED_COMMAND + ) deprecated_rows.append(Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)) else: command_name_text = Text(cmd_name_display) From 963c14b51bece7e7856ddd478d5e16f10290086d Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Sun, 23 Nov 2025 22:36:19 +0330 Subject: [PATCH 03/17] fix some tests error --- typer/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typer/core.py b/typer/core.py index baf79a23bd..8f43d11880 100644 --- a/typer/core.py +++ b/typer/core.py @@ -850,7 +850,7 @@ def list_commands(self, ctx: click.Context) -> List[str]: In Typer, we wish to maintain the original order of creation (cf Issue #933)""" seen = set() result = [] - for name, cmd in self.commands.items(): + for _name, cmd in self.commands.items(): if cmd.name and cmd.name not in seen: seen.add(cmd.name) result.append(cmd.name) From d7bf3f80b5651080c8f07b271bff9da97cbb383e Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Mon, 24 Nov 2025 10:47:25 +0330 Subject: [PATCH 04/17] fix coverage test failed added document for new changes --- docs/tutorial/commands/name.md | 88 +++++++++++++++++++++++++++ docs_src/commands/name/tutorial002.py | 18 ++++++ docs_src/commands/name/tutorial003.py | 18 ++++++ tests/test_commands_aliases.py | 38 ++++++++++++ 4 files changed, 162 insertions(+) create mode 100644 docs_src/commands/name/tutorial002.py create mode 100644 docs_src/commands/name/tutorial003.py diff --git a/docs/tutorial/commands/name.md b/docs/tutorial/commands/name.md index 8fd36b3e95..b743082125 100644 --- a/docs/tutorial/commands/name.md +++ b/docs/tutorial/commands/name.md @@ -54,3 +54,91 @@ def create_user(username: str): ... ``` Then the command name will be `create-user`. + +## Command Aliases + +You can define aliases for commands so users can call them with different names. + +### Positional Aliases + +Pass additional positional arguments to `@app.command()`: + +{* docs_src/commands/name/tutorial002.py hl[6,9] *} + +The `list` command can be called with `list` or `ls`: + +
+ +```console +$ python main.py --help + +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: + list, ls + remove, rm, delete + +$ python main.py list + +Listing items + +$ python main.py ls + +Listing items + +$ python main.py remove + +Removing items + +$ python main.py rm + +Removing items + +$ python main.py delete + +Removing items +``` + +
+ +### Keyword Aliases + +Use the `aliases` parameter: + +{* docs_src/commands/name/tutorial002.py hl[9] *} + +Positional aliases and the `aliases` parameter can be combined. + +### Hidden Aliases + +Use `hidden_aliases` for aliases that work but don't appear in help: + +{* docs_src/commands/name/tutorial003.py hl[6] *} + +
+ +```console +$ python main.py --help + +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: + list, ls + remove + +$ python main.py secretlist + +Listing items +``` + +
diff --git a/docs_src/commands/name/tutorial002.py b/docs_src/commands/name/tutorial002.py new file mode 100644 index 0000000000..b9d41827b9 --- /dev/null +++ b/docs_src/commands/name/tutorial002.py @@ -0,0 +1,18 @@ +import typer + +app = typer.Typer() + + +@app.command("list", "ls") +def list_items(): + print("Listing items") + + +@app.command("remove", aliases=["rm", "delete"]) +def remove_items(): + print("Removing items") + + +if __name__ == "__main__": + app() + diff --git a/docs_src/commands/name/tutorial003.py b/docs_src/commands/name/tutorial003.py new file mode 100644 index 0000000000..a84db138f5 --- /dev/null +++ b/docs_src/commands/name/tutorial003.py @@ -0,0 +1,18 @@ +import typer + +app = typer.Typer() + + +@app.command("list", "ls", hidden_aliases=["secretlist"]) +def list_items(): + print("Listing items") + + +@app.command("remove") +def remove_items(): + print("Removing items") + + +if __name__ == "__main__": + app() + diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index b433ae1ed7..df1013f57f 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -141,3 +141,41 @@ def show_documents(): result = runner.invoke(app, ["docs"]) assert result.exit_code == 0 assert "documents" in result.stdout + + +def test_command_no_aliases_help_output(): + app = typer.Typer() + + @app.command("list") + def list_items(): + pass + + @app.command("remove") + def remove_items(): + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert " list" in result.stdout or "list " in result.stdout + assert " remove" in result.stdout or "remove " in result.stdout + + +def test_command_empty_aliases_list(): + app = typer.Typer() + + @app.command("list", aliases=[]) + def list_items(): + print("listed") + + @app.command("remove") + def remove_items(): + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "list" in result.stdout + assert "remove" in result.stdout + + result = runner.invoke(app, ["list"]) + assert result.exit_code == 0 + assert "listed" in result.stdout From 80e2c1d9c39f5f7022eac681b2fda3e6ea961181 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 07:18:00 +0000 Subject: [PATCH 05/17] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/commands/name/tutorial002.py | 1 - docs_src/commands/name/tutorial003.py | 1 - 2 files changed, 2 deletions(-) diff --git a/docs_src/commands/name/tutorial002.py b/docs_src/commands/name/tutorial002.py index b9d41827b9..e8f97439aa 100644 --- a/docs_src/commands/name/tutorial002.py +++ b/docs_src/commands/name/tutorial002.py @@ -15,4 +15,3 @@ def remove_items(): if __name__ == "__main__": app() - diff --git a/docs_src/commands/name/tutorial003.py b/docs_src/commands/name/tutorial003.py index a84db138f5..7f88527272 100644 --- a/docs_src/commands/name/tutorial003.py +++ b/docs_src/commands/name/tutorial003.py @@ -15,4 +15,3 @@ def remove_items(): if __name__ == "__main__": app() - From db5d273900aedf1fe054a50257bba08e25bfa984 Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Mon, 24 Nov 2025 11:00:21 +0330 Subject: [PATCH 06/17] Add test for multiple command aliases in This enhances the test coverage for command alias functionality. --- tests/test_commands_aliases.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index df1013f57f..1d5f419501 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -179,3 +179,25 @@ def remove_items(): result = runner.invoke(app, ["list"]) assert result.exit_code == 0 assert "listed" in result.stdout + + +def test_multiple_commands_with_aliases(): + app = typer.Typer() + + @app.command("cmd1", "c1") + def command1(): + pass + + @app.command("cmd2", aliases=["c2"]) + def command2(): + pass + + @app.command("cmd3") + def command3(): + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "cmd1, c1" in result.stdout or "c1, cmd1" in result.stdout + assert "cmd2, c2" in result.stdout or "c2, cmd2" in result.stdout + assert "cmd3" in result.stdout From 82eedc901af33b41708c4d2993fe9f5e6d03bde5 Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Mon, 24 Nov 2025 11:16:32 +0330 Subject: [PATCH 07/17] Add tests for command list deduplication and comprehensive coverage --- tests/test_commands_aliases.py | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index 1d5f419501..636bc1ee96 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -201,3 +201,44 @@ def command3(): assert "cmd1, c1" in result.stdout or "c1, cmd1" in result.stdout assert "cmd2, c2" in result.stdout or "c2, cmd2" in result.stdout assert "cmd3" in result.stdout + + +def test_commands_list_deduplication(): + app = typer.Typer() + + @app.command("same", "alias1") + def cmd1(): + pass + + @app.command("other", "alias2") + def cmd2(): + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + commands_output = result.stdout + assert "same" in commands_output + assert "other" in commands_output + assert commands_output.count("same") == 1 + + +def test_list_commands_covers_all_branches(): + app = typer.Typer() + + @app.command("cmd1") + def command1(): + pass + + @app.command("cmd2", "alias") + def command2(): + pass + + @app.command("cmd3", aliases=["a3"]) + def command3(): + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "cmd1" in result.stdout + assert "cmd2" in result.stdout or "alias" in result.stdout + assert "cmd3" in result.stdout or "a3" in result.stdout From ba0322c6fed209b6692fd99ee560411a5050a22f Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Mon, 24 Nov 2025 12:52:09 +0330 Subject: [PATCH 08/17] Add tests for command visibility and alias functionality Enhanced coverage for command help output to verify alias presence and hidden status. --- tests/test_commands_aliases.py | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index 636bc1ee96..4e7380f0f3 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -242,3 +242,60 @@ def command3(): assert "cmd1" in result.stdout assert "cmd2" in result.stdout or "alias" in result.stdout assert "cmd3" in result.stdout or "a3" in result.stdout + + +def test_commands_with_hidden_and_aliases(): + app = typer.Typer() + + @app.command("visible", "v", aliases=["vis"]) + def visible_cmd(): + pass + + @app.command("hidden", hidden=True) + def hidden_cmd(): + pass + + @app.command("another", aliases=["a"]) + def another_cmd(): + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "visible" in result.stdout or "v" in result.stdout or "vis" in result.stdout + assert "hidden" not in result.stdout + assert "another" in result.stdout or "a" in result.stdout + + +def test_comprehensive_alias_scenarios(): + app = typer.Typer() + + @app.command("a1", "a2", aliases=["a3", "a4"]) + def cmd_a(): + pass + + @app.command("b1", hidden_aliases=["b2"]) + def cmd_b(): + pass + + @app.command("c1") + def cmd_c(): + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "a1" in result.stdout or "a2" in result.stdout or "a3" in result.stdout or "a4" in result.stdout + assert "b1" in result.stdout + assert "b2" not in result.stdout + assert "c1" in result.stdout + + result = runner.invoke(app, ["a1"]) + assert result.exit_code == 0 + + result = runner.invoke(app, ["a2"]) + assert result.exit_code == 0 + + result = runner.invoke(app, ["a3"]) + assert result.exit_code == 0 + + result = runner.invoke(app, ["b2"]) + assert result.exit_code == 0 From d79df744f38706e66095780ce6ea86cacb96b570 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 09:22:31 +0000 Subject: [PATCH 09/17] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_commands_aliases.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index 4e7380f0f3..edb3cd5ef6 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -283,7 +283,12 @@ def cmd_c(): result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 - assert "a1" in result.stdout or "a2" in result.stdout or "a3" in result.stdout or "a4" in result.stdout + assert ( + "a1" in result.stdout + or "a2" in result.stdout + or "a3" in result.stdout + or "a4" in result.stdout + ) assert "b1" in result.stdout assert "b2" not in result.stdout assert "c1" in result.stdout From 8bce656e9e3c0de6eb96ce0195278e31347b4f9c Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Tue, 25 Nov 2025 21:46:15 +0330 Subject: [PATCH 10/17] =?UTF-8?q?=F0=9F=93=9D=20Add=20coverage=20pragma=20?= =?UTF-8?q?to=20command=20alias=20test=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_commands_aliases.py | 48 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index edb3cd5ef6..b0eb039a86 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -13,7 +13,7 @@ def list_items(): @app.command("remove") def remove_items(): - pass + pass # pragma: no cover result = runner.invoke(app, ["list"]) assert result.exit_code == 0 @@ -33,7 +33,7 @@ def list_items(): @app.command("remove") def remove_items(): - pass + pass # pragma: no cover result = runner.invoke(app, ["list"]) assert result.exit_code == 0 @@ -57,7 +57,7 @@ def list_items(): @app.command("remove") def remove_items(): - pass + pass # pragma: no cover result = runner.invoke(app, ["list"]) assert result.exit_code == 0 @@ -77,11 +77,11 @@ def test_command_aliases_help_output(): @app.command("list", "ls") def list_items(): - pass + pass # pragma: no cover @app.command("remove", aliases=["rm", "delete"]) def remove_items(): - pass + pass # pragma: no cover result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -96,11 +96,11 @@ def test_command_hidden_aliases(): @app.command("list", "ls", hidden_aliases=["secretlist"]) def list_items(): - pass + pass # pragma: no cover @app.command("remove") def remove_items(): - pass + pass # pragma: no cover result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -148,11 +148,11 @@ def test_command_no_aliases_help_output(): @app.command("list") def list_items(): - pass + pass # pragma: no cover @app.command("remove") def remove_items(): - pass + pass # pragma: no cover result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -169,7 +169,7 @@ def list_items(): @app.command("remove") def remove_items(): - pass + pass # pragma: no cover result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -186,15 +186,15 @@ def test_multiple_commands_with_aliases(): @app.command("cmd1", "c1") def command1(): - pass + pass # pragma: no cover @app.command("cmd2", aliases=["c2"]) def command2(): - pass + pass # pragma: no cover @app.command("cmd3") def command3(): - pass + pass # pragma: no cover result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -208,11 +208,11 @@ def test_commands_list_deduplication(): @app.command("same", "alias1") def cmd1(): - pass + pass # pragma: no cover @app.command("other", "alias2") def cmd2(): - pass + pass # pragma: no cover result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -227,15 +227,15 @@ def test_list_commands_covers_all_branches(): @app.command("cmd1") def command1(): - pass + pass # pragma: no cover @app.command("cmd2", "alias") def command2(): - pass + pass # pragma: no cover @app.command("cmd3", aliases=["a3"]) def command3(): - pass + pass # pragma: no cover result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -249,15 +249,15 @@ def test_commands_with_hidden_and_aliases(): @app.command("visible", "v", aliases=["vis"]) def visible_cmd(): - pass + pass # pragma: no cover @app.command("hidden", hidden=True) def hidden_cmd(): - pass + pass # pragma: no cover @app.command("another", aliases=["a"]) def another_cmd(): - pass + pass # pragma: no cover result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -271,15 +271,15 @@ def test_comprehensive_alias_scenarios(): @app.command("a1", "a2", aliases=["a3", "a4"]) def cmd_a(): - pass + pass # pragma: no cover @app.command("b1", hidden_aliases=["b2"]) def cmd_b(): - pass + pass # pragma: no cover @app.command("c1") def cmd_c(): - pass + pass # pragma: no cover result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 From e21408943bc5538f76f5c60fd7d5b4eeefba8785 Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Tue, 25 Nov 2025 21:59:47 +0330 Subject: [PATCH 11/17] =?UTF-8?q?=F0=9F=93=9D=20Add=20test=20for=20command?= =?UTF-8?q?=20list=20deduplication=20with=20aliases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_commands_aliases.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index b0eb039a86..21338f41ea 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -304,3 +304,31 @@ def cmd_c(): result = runner.invoke(app, ["b2"]) assert result.exit_code == 0 + + +def test_list_commands_deduplication_with_aliases(): + app = typer.Typer() + + @app.command("main1", "alias1", aliases=["a1"]) + def cmd1(): + pass # pragma: no cover + + @app.command("main2", "alias2") + def cmd2(): + pass # pragma: no cover + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "main1" in result.stdout or "alias1" in result.stdout or "a1" in result.stdout + assert "main2" in result.stdout or "alias2" in result.stdout + assert result.stdout.count("main1") <= 1 + assert result.stdout.count("main2") <= 1 + + result = runner.invoke(app, ["main1"]) + assert result.exit_code == 0 + + result = runner.invoke(app, ["alias1"]) + assert result.exit_code == 0 + + result = runner.invoke(app, ["a1"]) + assert result.exit_code == 0 From 09f893d02a84f2898803cdd18f73df604c6961a3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:30:21 +0000 Subject: [PATCH 12/17] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_commands_aliases.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index 21338f41ea..3c38a07f7f 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -319,7 +319,9 @@ def cmd2(): result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 - assert "main1" in result.stdout or "alias1" in result.stdout or "a1" in result.stdout + assert ( + "main1" in result.stdout or "alias1" in result.stdout or "a1" in result.stdout + ) assert "main2" in result.stdout or "alias2" in result.stdout assert result.stdout.count("main1") <= 1 assert result.stdout.count("main2") <= 1 From 47fc8e6f58e756039c3865b951f1190180eeac65 Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Wed, 26 Nov 2025 06:25:25 +0330 Subject: [PATCH 13/17] =?UTF-8?q?=F0=9F=93=9D=20Add=20tests=20for=20comman?= =?UTF-8?q?d=20formatting=20with=20and=20without=20aliases=20when=20rich?= =?UTF-8?q?=20output=20is=20disabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_commands_aliases.py | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index 21338f41ea..fd9985ad05 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -1,4 +1,6 @@ +import pytest import typer +import typer.core from typer.testing import CliRunner runner = CliRunner() @@ -332,3 +334,46 @@ def cmd2(): result = runner.invoke(app, ["a1"]) assert result.exit_code == 0 + + +def test_format_commands_with_aliases_no_rich(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(typer.core, "HAS_RICH", False) + + app = typer.Typer() + + @app.command("list", "ls", aliases=["l"]) + def list_items(): + pass # pragma: no cover + + @app.command("remove", aliases=["rm"]) + def remove_items(): + pass # pragma: no cover + + @app.command("create") + def create_items(): + pass # pragma: no cover + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "list" in result.stdout or "ls" in result.stdout or "l" in result.stdout + assert "remove" in result.stdout or "rm" in result.stdout + assert "create" in result.stdout + + +def test_format_commands_no_aliases_no_rich(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(typer.core, "HAS_RICH", False) + + app = typer.Typer() + + @app.command("list") + def list_items(): + pass # pragma: no cover + + @app.command("remove") + def remove_items(): + pass # pragma: no cover + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "list" in result.stdout + assert "remove" in result.stdout From b0db5dcd7bf066f47322ce090a9ff74a19ce9007 Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Wed, 26 Nov 2025 06:41:33 +0330 Subject: [PATCH 14/17] =?UTF-8?q?=F0=9F=93=9D=20Add=20additional=20tests?= =?UTF-8?q?=20for=20command=20formatting=20and=20help=20output=20when=20ri?= =?UTF-8?q?ch=20output=20is=20disabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_commands_aliases.py | 78 ++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/test_commands_aliases.py b/tests/test_commands_aliases.py index f1ae5988e4..fca2913f40 100644 --- a/tests/test_commands_aliases.py +++ b/tests/test_commands_aliases.py @@ -379,3 +379,81 @@ def remove_items(): assert result.exit_code == 0 assert "list" in result.stdout assert "remove" in result.stdout + + +def test_format_commands_with_hidden_no_rich(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(typer.core, "HAS_RICH", False) + + app = typer.Typer() + + @app.command("visible") + def visible_cmd(): + pass # pragma: no cover + + @app.command("hidden", hidden=True) + def hidden_cmd(): + pass # pragma: no cover + + @app.command("another") + def another_cmd(): + pass # pragma: no cover + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "visible" in result.stdout + assert "hidden" not in result.stdout + assert "another" in result.stdout + + +def test_format_commands_empty_no_rich(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(typer.core, "HAS_RICH", False) + + app = typer.Typer() + + @app.callback(invoke_without_command=True) + def main(): + pass # pragma: no cover + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Commands" not in result.stdout or "Commands:" not in result.stdout + + +def test_format_help_command_no_rich(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(typer.core, "HAS_RICH", False) + + app = typer.Typer() + + @app.command(help="Test command") + def test_cmd(): + pass # pragma: no cover + + result = runner.invoke(app, ["test-cmd", "--help"]) + assert result.exit_code == 0 + assert "Test command" in result.stdout + + +def test_format_help_group_no_rich(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(typer.core, "HAS_RICH", False) + + app = typer.Typer(help="Test group") + + @app.command() + def test_cmd(): + pass # pragma: no cover + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Test group" in result.stdout or "Usage:" in result.stdout + + +def test_format_help_rich_markup_mode_none(): + app = typer.Typer(rich_markup_mode=None) + + @app.command(help="Test command") + def test_cmd(): + pass # pragma: no cover + + result = runner.invoke(app, ["test-cmd", "--help"]) + assert result.exit_code == 0 + assert "Test command" in result.stdout From 751677e71fccccc394b0abe64dbc8013383884cb Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Thu, 25 Dec 2025 13:40:41 +0330 Subject: [PATCH 15/17] resolve the conflict with main branch --- typer/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/typer/main.py b/typer/main.py index 80c872bf41..fc4bf64e27 100644 --- a/typer/main.py +++ b/typer/main.py @@ -217,8 +217,8 @@ def command( self, name: Optional[str] = None, *positional_aliases: str, - cls: Optional[Type[TyperCommand]] = None, - context_settings: Optional[Dict[Any, Any]] = None, + cls: Optional[type[TyperCommand]] = None, + context_settings: Optional[dict[Any, Any]] = None, help: Optional[str] = None, epilog: Optional[str] = None, short_help: Optional[str] = None, @@ -235,7 +235,7 @@ def command( if cls is None: cls = TyperCommand - all_aliases_list: List[str] = [] + all_aliases_list: list[str] = [] if positional_aliases: all_aliases_list.extend(positional_aliases) if aliases: From 490d2abc06c3143ebe1e11d1f05a3f8759a04316 Mon Sep 17 00:00:00 2001 From: pipinstalled Date: Thu, 25 Dec 2025 15:52:07 +0330 Subject: [PATCH 16/17] =?UTF-8?q?=F0=9F=93=9D=20Update=20command=20alias?= =?UTF-8?q?=20tests=20to=20improve=20coverage=20and=20ensure=20consistency?= =?UTF-8?q?=20with=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_name/test_tutorial002.py | 46 +++++++++++++++++++ .../test_name/test_tutorial003.py | 41 +++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/test_tutorial/test_commands/test_name/test_tutorial002.py create mode 100644 tests/test_tutorial/test_commands/test_name/test_tutorial003.py diff --git a/tests/test_tutorial/test_commands/test_name/test_tutorial002.py b/tests/test_tutorial/test_commands/test_name/test_tutorial002.py new file mode 100644 index 0000000000..f32f95fa27 --- /dev/null +++ b/tests/test_tutorial/test_commands/test_name/test_tutorial002.py @@ -0,0 +1,46 @@ +from typer.testing import CliRunner + +from docs_src.commands.name import tutorial002 as mod + +app = mod.app + +runner = CliRunner() + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Commands" in result.output + assert "list" in result.output or "ls" in result.output + assert "remove" in result.output or "rm" in result.output or "delete" in result.output + + +def test_list(): + result = runner.invoke(app, ["list"]) + assert result.exit_code == 0 + assert "Listing items" in result.output + + +def test_ls(): + result = runner.invoke(app, ["ls"]) + assert result.exit_code == 0 + assert "Listing items" in result.output + + +def test_remove(): + result = runner.invoke(app, ["remove"]) + assert result.exit_code == 0 + assert "Removing items" in result.output + + +def test_rm(): + result = runner.invoke(app, ["rm"]) + assert result.exit_code == 0 + assert "Removing items" in result.output + + +def test_delete(): + result = runner.invoke(app, ["delete"]) + assert result.exit_code == 0 + assert "Removing items" in result.output + diff --git a/tests/test_tutorial/test_commands/test_name/test_tutorial003.py b/tests/test_tutorial/test_commands/test_name/test_tutorial003.py new file mode 100644 index 0000000000..a98c7a4f8f --- /dev/null +++ b/tests/test_tutorial/test_commands/test_name/test_tutorial003.py @@ -0,0 +1,41 @@ +from typer.testing import CliRunner + +from docs_src.commands.name import tutorial003 as mod + +app = mod.app + +runner = CliRunner() + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "Commands" in result.output + assert "list" in result.output or "ls" in result.output + assert "remove" in result.output + assert "secretlist" not in result.output + + +def test_list(): + result = runner.invoke(app, ["list"]) + assert result.exit_code == 0 + assert "Listing items" in result.output + + +def test_ls(): + result = runner.invoke(app, ["ls"]) + assert result.exit_code == 0 + assert "Listing items" in result.output + + +def test_secretlist(): + result = runner.invoke(app, ["secretlist"]) + assert result.exit_code == 0 + assert "Listing items" in result.output + + +def test_remove(): + result = runner.invoke(app, ["remove"]) + assert result.exit_code == 0 + assert "Removing items" in result.output + From baed5c38f777e060b7d56eee4283ea5b8cdd6d86 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 25 Dec 2025 12:23:08 +0000 Subject: [PATCH 17/17] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_commands/test_name/test_tutorial002.py | 5 +++-- .../test_commands/test_name/test_tutorial003.py | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_tutorial/test_commands/test_name/test_tutorial002.py b/tests/test_tutorial/test_commands/test_name/test_tutorial002.py index f32f95fa27..67531e84e3 100644 --- a/tests/test_tutorial/test_commands/test_name/test_tutorial002.py +++ b/tests/test_tutorial/test_commands/test_name/test_tutorial002.py @@ -12,7 +12,9 @@ def test_help(): assert result.exit_code == 0 assert "Commands" in result.output assert "list" in result.output or "ls" in result.output - assert "remove" in result.output or "rm" in result.output or "delete" in result.output + assert ( + "remove" in result.output or "rm" in result.output or "delete" in result.output + ) def test_list(): @@ -43,4 +45,3 @@ def test_delete(): result = runner.invoke(app, ["delete"]) assert result.exit_code == 0 assert "Removing items" in result.output - diff --git a/tests/test_tutorial/test_commands/test_name/test_tutorial003.py b/tests/test_tutorial/test_commands/test_name/test_tutorial003.py index a98c7a4f8f..43dc9060e8 100644 --- a/tests/test_tutorial/test_commands/test_name/test_tutorial003.py +++ b/tests/test_tutorial/test_commands/test_name/test_tutorial003.py @@ -38,4 +38,3 @@ def test_remove(): result = runner.invoke(app, ["remove"]) assert result.exit_code == 0 assert "Removing items" in result.output -