From adc39e2502fa1e42fbdaa3cbeb25ce7b0e42c8fb Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 17 Nov 2025 11:19:59 +0100 Subject: [PATCH 01/13] add two more unit tests for rich formatting --- docs_src/arguments/help/tutorial006.py | 2 +- docs_src/arguments/help/tutorial006_an.py | 2 +- .../test_help/test_tutorial006.py | 7 ++-- .../test_help/test_tutorial006_an.py | 7 ++-- .../test_help/test_tutorial006_an_rich.py | 37 +++++++++++++++++++ .../test_help/test_tutorial006_rich.py | 37 +++++++++++++++++++ 6 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 tests/test_tutorial/test_arguments/test_help/test_tutorial006_an_rich.py create mode 100644 tests/test_tutorial/test_arguments/test_help/test_tutorial006_rich.py diff --git a/docs_src/arguments/help/tutorial006.py b/docs_src/arguments/help/tutorial006.py index e502a506f4..262d475c77 100644 --- a/docs_src/arguments/help/tutorial006.py +++ b/docs_src/arguments/help/tutorial006.py @@ -1,7 +1,7 @@ import typer -def main(name: str = typer.Argument("World", metavar="✨username✨")): +def main(name: str = typer.Argument("World", metavar="✨user✨")): print(f"Hello {name}") diff --git a/docs_src/arguments/help/tutorial006_an.py b/docs_src/arguments/help/tutorial006_an.py index 4d745278b4..ae44b8d48a 100644 --- a/docs_src/arguments/help/tutorial006_an.py +++ b/docs_src/arguments/help/tutorial006_an.py @@ -2,7 +2,7 @@ from typing_extensions import Annotated -def main(name: Annotated[str, typer.Argument(metavar="✨username✨")] = "World"): +def main(name: Annotated[str, typer.Argument(metavar="✨user✨")] = "World"): print(f"Hello {name}") diff --git a/tests/test_tutorial/test_arguments/test_help/test_tutorial006.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial006.py index 4c8abb8a86..9c7fe4679d 100644 --- a/tests/test_tutorial/test_arguments/test_help/test_tutorial006.py +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial006.py @@ -8,16 +8,17 @@ runner = CliRunner() -app = typer.Typer() +app = typer.Typer(rich_markup_mode=None) app.command()(mod.main) def test_help(): result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 - assert "[OPTIONS] ✨username✨" in result.output + assert "[OPTIONS] ✨user✨" in result.output assert "Arguments" in result.output - assert "✨username✨" in result.output + assert "✨user✨" in result.output + assert "name" not in result.output assert "[default: World]" in result.output diff --git a/tests/test_tutorial/test_arguments/test_help/test_tutorial006_an.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial006_an.py index e60ec51666..fd8c9fe0c1 100644 --- a/tests/test_tutorial/test_arguments/test_help/test_tutorial006_an.py +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial006_an.py @@ -8,16 +8,17 @@ runner = CliRunner() -app = typer.Typer() +app = typer.Typer(rich_markup_mode=None) app.command()(mod.main) def test_help(): result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 - assert "[OPTIONS] ✨username✨" in result.output + assert "[OPTIONS] ✨user✨" in result.output assert "Arguments" in result.output - assert "✨username✨" in result.output + assert "✨user✨" in result.output + assert "name" not in result.output assert "[default: World]" in result.output diff --git a/tests/test_tutorial/test_arguments/test_help/test_tutorial006_an_rich.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial006_an_rich.py new file mode 100644 index 0000000000..fbfd99a627 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial006_an_rich.py @@ -0,0 +1,37 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.help import tutorial006_an as mod + +runner = CliRunner() + +app = typer.Typer(rich_markup_mode="rich") +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "[OPTIONS] ✨user✨" in result.output + assert "Arguments" in result.output + assert "✨user✨" in result.output + assert "name" not in result.output + assert "[default: World]" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_arguments/test_help/test_tutorial006_rich.py b/tests/test_tutorial/test_arguments/test_help/test_tutorial006_rich.py new file mode 100644 index 0000000000..fe1d29b281 --- /dev/null +++ b/tests/test_tutorial/test_arguments/test_help/test_tutorial006_rich.py @@ -0,0 +1,37 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.arguments.help import tutorial006 as mod + +runner = CliRunner() + +app = typer.Typer(rich_markup_mode="rich") +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "[OPTIONS] ✨user✨" in result.output + assert "Arguments" in result.output + assert "✨user✨" in result.output + assert "name" not in result.output + assert "[default: World]" in result.output + + +def test_call_arg(): + result = runner.invoke(app, ["Camila"]) + assert result.exit_code == 0 + assert "Hello Camila" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout From fa7d8d52e19bb78fb6c15af9f24e635182ffbe0f Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 17 Nov 2025 11:58:26 +0100 Subject: [PATCH 02/13] add test from #438 --- tests/test_rich_utils.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_rich_utils.py b/tests/test_rich_utils.py index d31dbafb5c..a1f001daf5 100644 --- a/tests/test_rich_utils.py +++ b/tests/test_rich_utils.py @@ -99,3 +99,39 @@ def main(bar: str): result = runner.invoke(app, ["--help"]) assert "Usage" in result.stdout assert "BAR" in result.stdout + + +def test_rich_help_metavar(): + app = typer.Typer(rich_markup_mode="rich") + + @app.command() + def main( + *, + arg1: int, + arg2: int = 42, + arg3: int = typer.Argument(...), + arg4: int = typer.Argument(42), + arg5: int = typer.Option(...), + arg6: int = typer.Option(42), + arg7: int = typer.Argument(42, metavar="meta7"), + arg8: int = typer.Argument(42, metavar="ARG8"), + ): + pass + + result = runner.invoke(app, ["--help"]) + assert "Usage" in result.stdout + + out_nospace = result.stdout.replace(" ", "") + + assert "arg1INTEGER" in out_nospace + assert "arg2INTEGER" in out_nospace + assert "arg3INTEGER" in out_nospace + assert "arg4INTEGER" in out_nospace + assert "arg5INTEGER" in out_nospace + assert "arg6INTEGER" in out_nospace + assert "meta7INTEGER" in out_nospace + assert "ARG8INTEGER" in out_nospace + + assert "[ARG4]" not in result.stdout + assert "arg7" not in result.stdout + assert "arg8" not in result.stdout From 6bb0977bbdaf1537e3a720142ed7de1b8f7d69c7 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 17 Nov 2025 13:13:16 +0100 Subject: [PATCH 03/13] test correct usage string --- tests/test_rich_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_rich_utils.py b/tests/test_rich_utils.py index a1f001daf5..9b381c2208 100644 --- a/tests/test_rich_utils.py +++ b/tests/test_rich_utils.py @@ -114,12 +114,12 @@ def main( arg5: int = typer.Option(...), arg6: int = typer.Option(42), arg7: int = typer.Argument(42, metavar="meta7"), - arg8: int = typer.Argument(42, metavar="ARG8"), + arg8: int = typer.Argument(metavar="ARG8"), ): pass result = runner.invoke(app, ["--help"]) - assert "Usage" in result.stdout + assert "Usage main [OPTIONS] ARG1 ARG3 [ARG4] [meta7] [ARG8]" in result.stdout out_nospace = result.stdout.replace(" ", "") From 13bd5e4685ed88d102c9db87ee9b4707fc2d3fc3 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 17 Nov 2025 13:15:26 +0100 Subject: [PATCH 04/13] fix usage string --- tests/test_rich_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_rich_utils.py b/tests/test_rich_utils.py index 9b381c2208..3bba4eaf99 100644 --- a/tests/test_rich_utils.py +++ b/tests/test_rich_utils.py @@ -119,7 +119,7 @@ def main( pass result = runner.invoke(app, ["--help"]) - assert "Usage main [OPTIONS] ARG1 ARG3 [ARG4] [meta7] [ARG8]" in result.stdout + assert "Usage main [OPTIONS] ARG1 ARG3 [ARG4] meta7 [ARG8]" in result.stdout out_nospace = result.stdout.replace(" ", "") From a6282f7405bbf07ea1cb4b5e4b2a37c565e24a69 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 17 Nov 2025 15:57:07 +0100 Subject: [PATCH 05/13] expand new unit test --- tests/test_rich_utils.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/test_rich_utils.py b/tests/test_rich_utils.py index 3bba4eaf99..dd332685e4 100644 --- a/tests/test_rich_utils.py +++ b/tests/test_rich_utils.py @@ -115,23 +115,31 @@ def main( arg6: int = typer.Option(42), arg7: int = typer.Argument(42, metavar="meta7"), arg8: int = typer.Argument(metavar="ARG8"), + arg9: int = typer.Argument(metavar="arg9"), ): pass result = runner.invoke(app, ["--help"]) - assert "Usage main [OPTIONS] ARG1 ARG3 [ARG4] meta7 [ARG8]" in result.stdout + # assert "Usage: main [OPTIONS] ARG1 ARG3 [ARG4] meta7 [ARG8] [arg9]" in result.stdout + assert "Usage: main [OPTIONS] ARG1 ARG3 [ARG4] meta7 ARG8 arg9" in result.stdout out_nospace = result.stdout.replace(" ", "") - assert "arg1INTEGER" in out_nospace + # arguments + assert "ARG1INTEGER" in out_nospace + assert "ARG3INTEGER" in out_nospace + assert "[ARG4]INTEGER" in out_nospace + assert "meta7INTEGER" in out_nospace + assert "ARG8INTEGER" in out_nospace + # assert "[ARG8]INTEGER" in out_nospace + assert "arg9INTEGER" in out_nospace + # assert "[arg9]INTEGER" in out_nospace + + assert "arg7" not in result.stdout.lower() + assert "ARG9" not in result.stdout + + # options assert "arg2INTEGER" in out_nospace - assert "arg3INTEGER" in out_nospace - assert "arg4INTEGER" in out_nospace assert "arg5INTEGER" in out_nospace assert "arg6INTEGER" in out_nospace - assert "meta7INTEGER" in out_nospace - assert "ARG8INTEGER" in out_nospace - assert "[ARG4]" not in result.stdout - assert "arg7" not in result.stdout - assert "arg8" not in result.stdout From 5b105f6e2956edfc1d072b48cbd4b147fac350a3 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 17 Nov 2025 15:59:58 +0100 Subject: [PATCH 06/13] fix newline --- tests/test_rich_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_rich_utils.py b/tests/test_rich_utils.py index dd332685e4..d3b68cb6b4 100644 --- a/tests/test_rich_utils.py +++ b/tests/test_rich_utils.py @@ -142,4 +142,3 @@ def main( assert "arg2INTEGER" in out_nospace assert "arg5INTEGER" in out_nospace assert "arg6INTEGER" in out_nospace - From 8f8f08055c083f1d6c294dfba8bebb1a624c72da Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 17 Nov 2025 16:05:01 +0100 Subject: [PATCH 07/13] change metavar column into type column (WIP) --- typer/rich_utils.py | 64 +++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/typer/rich_utils.py b/typer/rich_utils.py index d4c3676aea..80467835ef 100644 --- a/typer/rich_utils.py +++ b/typer/rich_utils.py @@ -35,8 +35,8 @@ STYLE_SWITCH = "bold green" STYLE_NEGATIVE_OPTION = "bold magenta" STYLE_NEGATIVE_SWITCH = "bold red" -STYLE_METAVAR = "bold yellow" -STYLE_METAVAR_SEPARATOR = "dim" +STYLE_TYPES = "bold yellow" +STYLE_TYPES_SEPARATOR = "dim" STYLE_USAGE = "yellow" STYLE_USAGE_COMMAND = "bold" STYLE_DEPRECATED = "red" @@ -137,8 +137,8 @@ def _get_rich_console(stderr: bool = False) -> Console: "switch": STYLE_SWITCH, "negative_option": STYLE_NEGATIVE_OPTION, "negative_switch": STYLE_NEGATIVE_SWITCH, - "metavar": STYLE_METAVAR, - "metavar_sep": STYLE_METAVAR_SEPARATOR, + "types": STYLE_TYPES, + "types_sep": STYLE_TYPES_SEPARATOR, "usage": STYLE_USAGE, }, ), @@ -361,38 +361,40 @@ def _print_options_panel( opt_short_strs = [] secondary_opt_long_strs = [] secondary_opt_short_strs = [] + + # check whether argument has a metavar set + metavar_str = None + # TODO: when deprecating Click < 8.2, make ctx required + if isinstance(param, click.Argument): + signature = inspect.signature(param.make_metavar) + if "ctx" in signature.parameters: + metavar_str = param.make_metavar(ctx=ctx) + else: + # Click < 8.2 + metavar_str = param.make_metavar() # type: ignore[call-arg] + for opt_str in param.opts: if "--" in opt_str: opt_long_strs.append(opt_str) + elif metavar_str: + opt_short_strs.append(metavar_str) else: opt_short_strs.append(opt_str) for opt_str in param.secondary_opts: if "--" in opt_str: secondary_opt_long_strs.append(opt_str) + elif metavar_str: + secondary_opt_short_strs.append(metavar_str) else: secondary_opt_short_strs.append(opt_str) - # Column for a metavar, if we have one - metavar = Text(style=STYLE_METAVAR, overflow="fold") - # TODO: when deprecating Click < 8.2, make ctx required - signature = inspect.signature(param.make_metavar) - if "ctx" in signature.parameters: - metavar_str = param.make_metavar(ctx=ctx) - else: - # Click < 8.2 - metavar_str = param.make_metavar() # type: ignore[call-arg] - - # Do it ourselves if this is a positional argument - if ( - isinstance(param, click.Argument) - and param.name - and metavar_str == param.name.upper() - ): - metavar_str = param.type.name.upper() + # Column for recording the type + types = Text(style=STYLE_TYPES, overflow="fold") - # Skip booleans and choices (handled above) - if metavar_str != "BOOLEAN": - metavar.append(metavar_str) + # Fetch type + type_str = param.type.name.upper() + if type_str != "BOOLEAN": + types.append(type_str) # Range - from # https://github.com/pallets/click/blob/c63c70dabd3f86ca68678b4f00951f78f52d0270/src/click/core.py#L2698-L2706 # noqa: E501 @@ -404,7 +406,7 @@ def _print_options_panel( ): range_str = param.type._describe_range() if range_str: - metavar.append(RANGE_STRING.format(range_str)) + types.append(RANGE_STRING.format(range_str)) # Required asterisk required: Union[str, Text] = "" @@ -412,14 +414,14 @@ def _print_options_panel( required = Text(REQUIRED_SHORT_STRING, style=STYLE_REQUIRED_SHORT) # Highlighter to make [ | ] and <> dim - class MetavarHighlighter(RegexHighlighter): + class TypesHighlighter(RegexHighlighter): highlights = [ - r"^(?P(\[|<))", - r"(?P\|)", - r"(?P(\]|>)$)", + r"^(?P(\[|<))", + r"(?P\|)", + r"(?P(\]|>)$)", ] - metavar_highlighter = MetavarHighlighter() + types_highlighter = TypesHighlighter() required_rows.append(required) options_rows.append( @@ -428,7 +430,7 @@ class MetavarHighlighter(RegexHighlighter): highlighter(",".join(opt_short_strs)), negative_highlighter(",".join(secondary_opt_long_strs)), negative_highlighter(",".join(secondary_opt_short_strs)), - metavar_highlighter(metavar), + types_highlighter(types), _get_parameter_help( param=param, ctx=ctx, From 98b8469675754e20e414243f79050ba23f9238cf Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 17 Nov 2025 16:32:32 +0100 Subject: [PATCH 08/13] fix other tests that didn't check for the right capitalization --- .../test_tutorial/test_commands/test_help/test_tutorial007.py | 4 ++-- .../test_commands/test_help/test_tutorial007_an.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_tutorial/test_commands/test_help/test_tutorial007.py b/tests/test_tutorial/test_commands/test_help/test_tutorial007.py index f262c251f5..d8c03bb7ea 100644 --- a/tests/test_tutorial/test_commands/test_help/test_tutorial007.py +++ b/tests/test_tutorial/test_commands/test_help/test_tutorial007.py @@ -24,10 +24,10 @@ def test_main_help(): def test_create_help(): result = runner.invoke(app, ["create", "--help"]) assert result.exit_code == 0 - assert "username" in result.output + assert "USERNAME" in result.output assert "The username to create" in result.output assert "Secondary Arguments" in result.output - assert "lastname" in result.output + assert "LASTNAME" in result.output assert "The last name of the new user" in result.output assert "--force" in result.output assert "--no-force" in result.output diff --git a/tests/test_tutorial/test_commands/test_help/test_tutorial007_an.py b/tests/test_tutorial/test_commands/test_help/test_tutorial007_an.py index 1a8c3d60a7..4c634c9856 100644 --- a/tests/test_tutorial/test_commands/test_help/test_tutorial007_an.py +++ b/tests/test_tutorial/test_commands/test_help/test_tutorial007_an.py @@ -24,10 +24,10 @@ def test_main_help(): def test_create_help(): result = runner.invoke(app, ["create", "--help"]) assert result.exit_code == 0 - assert "username" in result.output + assert "USERNAME" in result.output assert "The username to create" in result.output assert "Secondary Arguments" in result.output - assert "lastname" in result.output + assert "LASTNAME" in result.output assert "The last name of the new user" in result.output assert "--force" in result.output assert "--no-force" in result.output From a6314caf62b8276ceee9fb86b287915ea107b8cc Mon Sep 17 00:00:00 2001 From: svlandeg Date: Mon, 17 Nov 2025 16:33:06 +0100 Subject: [PATCH 09/13] depending on argument/option, parse the metavar string differently --- typer/rich_utils.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/typer/rich_utils.py b/typer/rich_utils.py index 80467835ef..8f50eb0b28 100644 --- a/typer/rich_utils.py +++ b/typer/rich_utils.py @@ -113,7 +113,7 @@ class OptionHighlighter(RegexHighlighter): highlights = [ r"(^|\W)(?P\-\w+)(?![a-zA-Z0-9])", r"(^|\W)(?P