Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
adc39e2
add two more unit tests for rich formatting
svlandeg Nov 17, 2025
fa7d8d5
add test from #438
svlandeg Nov 17, 2025
6bb0977
test correct usage string
svlandeg Nov 17, 2025
13bd5e4
fix usage string
svlandeg Nov 17, 2025
a6282f7
expand new unit test
svlandeg Nov 17, 2025
5b105f6
fix newline
svlandeg Nov 17, 2025
8f8f080
change metavar column into type column (WIP)
svlandeg Nov 17, 2025
98b8469
fix other tests that didn't check for the right capitalization
svlandeg Nov 17, 2025
a6314ca
depending on argument/option, parse the metavar string differently
svlandeg Nov 17, 2025
b7d902b
fix one more old test
svlandeg Nov 17, 2025
da5e707
fix comments related to PR 1409
svlandeg Nov 17, 2025
0208421
pragma no cover for example app output
svlandeg Nov 17, 2025
987eaaf
add no cover to L390
svlandeg Nov 17, 2025
62b6b59
Merge branch 'master' into fix/metavar
svlandeg Nov 25, 2025
c601832
revert back to old format to explicitely set Rich formatting in app
svlandeg Nov 25, 2025
a256d02
rename tutorial files to follow new py39 format
svlandeg Jan 7, 2026
ceb8e67
Merge branch 'master' into fix/metavar
svlandeg Jan 7, 2026
d6859fe
🎨 Auto format
pre-commit-ci-lite[bot] Jan 7, 2026
d495801
test for different rich_markup_mode's within the same test file
svlandeg Jan 7, 2026
bb27c0b
fix assert now that #1409 is merged
svlandeg Jan 7, 2026
412d7ed
fix assert now that #1409 is merged (part 2)
svlandeg Jan 7, 2026
078e22f
Merge branch 'master' into fix/metavar
svlandeg Feb 14, 2026
f2623ce
fix after merge conflict with PR 1508
svlandeg Feb 14, 2026
dcf13a1
Merge branch 'master' into fix/metavar
svlandeg Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs_src/arguments/help/tutorial006_an_py310.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


@app.command()
def main(name: Annotated[str, typer.Argument(metavar="✨username✨")] = "World"):
def main(name: Annotated[str, typer.Argument(metavar="✨user✨")] = "World"):
print(f"Hello {name}")


Expand Down
2 changes: 1 addition & 1 deletion docs_src/arguments/help/tutorial006_py310.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


@app.command()
def main(name: str = typer.Argument("World", metavar="✨username✨")):
def main(name: str = typer.Argument("World", metavar="✨user✨")):
print(f"Hello {name}")


Expand Down
58 changes: 49 additions & 9 deletions tests/test_rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def main(

result = runner.invoke(app, ["--help"])
assert "Usage" in result.stdout
assert "name" in result.stdout
assert "NAME" in result.stdout
assert "option-1" in result.stdout
assert "option-2" in result.stdout
assert result.stdout.count("[default: None]") == 0
Expand Down Expand Up @@ -108,31 +108,31 @@ def main(bar: str):
@pytest.mark.parametrize("input_text", ["[ARGS]", "[ARGS]..."])
def test_metavar_highlighter(input_text: str):
"""
Test that the MetavarHighlighter works correctly.
Test that the TypesHighlighter (used to be the MetavarHighlighter) works correctly.
cf PR 1508
"""
from typer.rich_utils import (
STYLE_METAVAR_SEPARATOR,
STYLE_TYPES_SEPARATOR,
Text,
_get_rich_console,
metavar_highlighter,
types_highlighter,
)

console = _get_rich_console()

text = Text(input_text)
highlighted = metavar_highlighter(text)
highlighted = types_highlighter(text)
console.print(highlighted)

# Get the style for each bracket
opening_bracket_style = highlighted.get_style_at_offset(console, 0)
closing_bracket_style = highlighted.get_style_at_offset(console, 5)

# The opening bracket should have metavar_sep style
assert str(opening_bracket_style) == STYLE_METAVAR_SEPARATOR
# The opening bracket should have types_sep style
assert str(opening_bracket_style) == STYLE_TYPES_SEPARATOR

# The closing bracket should have metavar_sep style (fails before PR 1508 when there are 3 dots)
assert str(closing_bracket_style) == STYLE_METAVAR_SEPARATOR
# The closing bracket should have types_sep style (fails before PR 1508 when there are 3 dots)
assert str(closing_bracket_style) == STYLE_TYPES_SEPARATOR


def test_make_rich_text_with_ansi_escape_sequences():
Expand Down Expand Up @@ -222,3 +222,43 @@ def find_right_boundary_pos(line):
assert pos_a == pos_b == pos_c, (
f"Right boundaries not aligned: A={pos_a}, B={pos_b}, C={pos_c}"
)


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(metavar="ARG8"),
arg9: int = typer.Argument(metavar="arg9"),
):
pass # pragma: no cover

result = runner.invoke(app, ["--help"])
assert "Usage: main [OPTIONS] ARG1 ARG3 [ARG4] [meta7] ARG8 arg9" in result.stdout

out_nospace = result.stdout.replace(" ", "")

# arguments
assert "ARG1INTEGER" in out_nospace
assert "ARG3INTEGER" in out_nospace
assert "[ARG4]INTEGER" in out_nospace
assert "[meta7]INTEGER" in out_nospace
assert "ARG8INTEGER" in out_nospace
assert "arg9INTEGER" in out_nospace

assert "arg7" not in result.stdout.lower()
assert "ARG9" not in result.stdout

# options
assert "arg2INTEGER" in out_nospace
assert "arg5INTEGER" in out_nospace
assert "arg6INTEGER" in out_nospace
26 changes: 21 additions & 5 deletions tests/test_tutorial/test_arguments/test_help/test_tutorial006.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from types import ModuleType

import pytest
import typer
from typer.testing import CliRunner

runner = CliRunner()
Expand All @@ -22,16 +23,31 @@ def get_mod(request: pytest.FixtureRequest) -> ModuleType:
return mod


def test_help(mod: ModuleType):
result = runner.invoke(mod.app, ["--help"])
@pytest.fixture(
name="app",
params=[
pytest.param(None),
pytest.param("rich"),
],
)
def get_app(mod: ModuleType, request: pytest.FixtureRequest) -> typer.Typer:
rich_markup_mode = request.param
app = typer.Typer(rich_markup_mode=rich_markup_mode)
app.command()(mod.main)
return app


def test_help(app):
result = runner.invoke(app, ["--help"])
assert result.exit_code == 0
assert "Usage: main [OPTIONS] [✨username✨]" in result.output
assert "Usage: main [OPTIONS] [✨user✨]" in result.output
assert "Arguments" in result.output
assert "name" not in result.output
assert "[default: World]" in result.output


def test_call_arg(mod: ModuleType):
result = runner.invoke(mod.app, ["Camila"])
def test_call_arg(app):
result = runner.invoke(app, ["Camila"])
assert result.exit_code == 0
assert "Hello Camila" in result.output

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ def test_main_help(mod: ModuleType):
def test_create_help(mod: ModuleType):
result = runner.invoke(mod.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
Expand Down
60 changes: 35 additions & 25 deletions typer/rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,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"
Expand Down Expand Up @@ -109,7 +109,7 @@ class OptionHighlighter(RegexHighlighter):
highlights = [
r"(^|\W)(?P<switch>\-\w+)(?![a-zA-Z0-9])",
r"(^|\W)(?P<option>\-\-[\w\-]+)(?![a-zA-Z0-9])",
r"(?P<metavar>\<[^\>]+\>)",
r"(?P<types>\<[^\>]+\>)",
r"(?P<usage>Usage: )",
]

Expand All @@ -122,17 +122,17 @@ class NegativeOptionHighlighter(RegexHighlighter):


# Highlighter to make [ | ] and <> dim
class MetavarHighlighter(RegexHighlighter):
class TypesHighlighter(RegexHighlighter):
highlights = [
r"^(?P<metavar_sep>(\[|<))",
r"(?P<metavar_sep>\|)",
r"(?P<metavar_sep>(\]|>))(\.\.\.)?$",
r"^(?P<types_sep>(\[|<))",
r"(?P<types_sep>\|)",
r"(?P<types_sep>(\]|>))(\.\.\.)?$",
]


highlighter = OptionHighlighter()
negative_highlighter = NegativeOptionHighlighter()
metavar_highlighter = MetavarHighlighter()
types_highlighter = TypesHighlighter()


def _has_ansi_character(text: str) -> bool:
Expand All @@ -147,8 +147,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,
},
),
Expand Down Expand Up @@ -361,31 +361,41 @@ def _print_options_panel(
opt_short_strs = []
secondary_opt_long_strs = []
secondary_opt_short_strs = []

# check whether argument has a metavar name or type set
metavar_name = None
metavar_type = None
metavar_str = param.make_metavar(ctx=ctx)
if isinstance(param, click.Argument):
metavar_name = metavar_str
if isinstance(param, click.Option):
metavar_type = metavar_str
Comment on lines +371 to +372
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this part is crucial to ensure the current Enum tests won't fail. This is the part that will replace Choice with something like [simple|conv|lstm]


for opt_str in param.opts:
if "--" in opt_str:
opt_long_strs.append(opt_str)
elif metavar_name:
opt_short_strs.append(metavar_name)
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_name: # pragma: no cover
secondary_opt_short_strs.append(metavar_name)
Comment on lines +384 to +385
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too sure yet about these secondary_opts, probably requires another test (instead of having pragma cover).

else:
secondary_opt_short_strs.append(opt_str)

# Column for a metavar, if we have one
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()
# 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
if metavar_type and metavar_type != "BOOLEAN":
types.append(metavar_type)
else:
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
Expand All @@ -397,7 +407,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: str | Text = ""
Expand All @@ -411,7 +421,7 @@ def _print_options_panel(
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,
Expand Down