diff --git a/tests/test_argument_metavar_rich.py b/tests/test_argument_metavar_rich.py new file mode 100644 index 0000000000..5b4d396ac7 --- /dev/null +++ b/tests/test_argument_metavar_rich.py @@ -0,0 +1,103 @@ +"""Test that Argument(metavar=...) displays correctly in rich help output. + +Regression test for https://github.com/fastapi/typer/issues/1156 + +When a custom metavar is set on an Argument, the rich help panel should: +- Show the metavar as the argument name (replacing the parameter name) +- Show the type (e.g. TEXT) in the type column +- NOT show the parameter name in the name column with the metavar in the type column +""" + +from typing import Annotated + +import typer +from typer.testing import CliRunner + +runner = CliRunner() + + +def test_argument_custom_metavar_shows_as_name_in_rich_help(): + """Custom metavar should replace the argument name, not the type.""" + app = typer.Typer() + + @app.command() + def show(user: Annotated[str, typer.Argument(metavar="MY_ARG")]): + """Show user.""" + typer.echo(f"show user: {user}") + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + output = result.output + + # The usage line should show the metavar + assert "MY_ARG" in output + + # In the Arguments panel, MY_ARG should appear as the name + # and TEXT should appear as the type + # Before the fix: "user MY_ARG" (name=user, type=MY_ARG) + # After the fix: "MY_ARG TEXT" (name=MY_ARG, type=TEXT) + assert "MY_ARG" in output + assert "TEXT" in output + + # The parameter name 'user' should NOT appear in the help output + # (it should be replaced by the metavar) + lines = output.split("\n") + argument_section = False + for line in lines: + if "Arguments" in line: + argument_section = True + elif argument_section and "─" not in line and line.strip(): + # This is an argument line in the panel + # It should NOT contain the raw parameter name 'user' + assert "user" not in line.lower().split(), ( + f"Parameter name 'user' should not appear in argument panel " + f"when metavar is set. Got: {line!r}" + ) + break + + +def test_argument_without_metavar_shows_default_name(): + """Without a custom metavar, argument should show name and type normally.""" + app = typer.Typer() + + @app.command() + def show(user: Annotated[str, typer.Argument()]): + """Show user.""" + typer.echo(f"show user: {user}") + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + output = result.output + + # Should show 'user' as name and 'TEXT' as type + assert "user" in output + assert "TEXT" in output + + +def test_argument_metavar_with_int_type(): + """Custom metavar with non-string type should show correct type.""" + app = typer.Typer() + + @app.command() + def process(count: Annotated[int, typer.Argument(metavar="NUM")]): + """Process items.""" + pass + + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + output = result.output + + assert "NUM" in output + assert "INTEGER" in output + + # 'count' should not appear in the arguments panel + lines = output.split("\n") + argument_section = False + for line in lines: + if "Arguments" in line: + argument_section = True + elif argument_section and "─" not in line and line.strip(): + assert "count" not in line.lower().split(), ( + f"Parameter name 'count' should not appear when metavar is set. Got: {line!r}" + ) + break diff --git a/typer/rich_utils.py b/typer/rich_utils.py index d85043238c..6f3e3cfe94 100644 --- a/typer/rich_utils.py +++ b/typer/rich_utils.py @@ -376,12 +376,15 @@ def _print_options_panel( metavar = Text(style=STYLE_METAVAR, overflow="fold") metavar_str = param.make_metavar(ctx=ctx) # 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() + if isinstance(param, click.Argument) and param.name: + if param.metavar and metavar_str != param.name.upper(): + # Custom metavar set: use it as the display name and show + # the type in the metavar column instead + opt_long_strs = [metavar_str] + opt_short_strs = [] + metavar_str = param.type.name.upper() + elif metavar_str == param.name.upper(): + metavar_str = param.type.name.upper() # Skip booleans and choices (handled above) if metavar_str != "BOOLEAN":