Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
89 changes: 89 additions & 0 deletions tests/test_rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,92 @@ def main(bar: str):
result = runner.invoke(app, ["--help"])
assert "Usage" in result.stdout
assert "BAR" in result.stdout


def test_make_rich_text_with_ansi_escape_sequences():
from typer.rich_utils import Text, _make_rich_text

ansi_text = "This is \x1b[4munderlined\x1b[0m text"
result = _make_rich_text(text=ansi_text, markup_mode="rich")

assert isinstance(result, Text)
assert "\x1b[" not in result.plain
assert "underlined" in result.plain

mixed_text = "Start \x1b[31mred\x1b[0m middle \x1b[32mgreen\x1b[0m end"
result = _make_rich_text(text=mixed_text, markup_mode="rich")
assert isinstance(result, Text)
assert "\x1b[" not in result.plain
assert "red" in result.plain
assert "green" in result.plain

fake_ansi = "This contains \x1b[ but not a complete sequence"
result = _make_rich_text(text=fake_ansi, markup_mode="rich")
assert isinstance(result, Text)
assert "\x1b[" not in result.plain
assert "This contains " in result.plain


def test_make_rich_text_with_typer_style_in_help():
app = typer.Typer()

@app.command()
def example(
a: str = typer.Option(help="This is A"),
b: str = typer.Option(help=f"This is {typer.style('B', underline=True)}"),
):
"""Example command with styled help text."""
pass # pragma: no cover

result = runner.invoke(app, ["--help"])

assert result.exit_code == 0
assert "This is A" in result.stdout
assert "This is B" in result.stdout
assert "\x1b[" not in result.stdout


def test_help_table_alignment_with_styled_text():
app = typer.Typer()

@app.command()
def example(
a: str = typer.Option(help="This is A"),
b: str = typer.Option(help=f"This is {typer.style('B', underline=True)}"),
c: str = typer.Option(help="This is C"),
):
"""Example command with styled help text."""
pass # pragma: no cover

result = runner.invoke(app, ["--help"])

assert result.exit_code == 0

lines = result.stdout.split("\n")

option_a_line = None
option_b_line = None
option_c_line = None

for line in lines:
if "--a" in line and "This is A" in line:
option_a_line = line
elif "--b" in line and "This is B" in line:
option_b_line = line
elif "--c" in line and "This is C" in line:
option_c_line = line

assert option_a_line is not None, "Option A line not found"
assert option_b_line is not None, "Option B line not found"
assert option_c_line is not None, "Option C line not found"

def find_right_boundary_pos(line):
return line.rfind("|")

pos_a = find_right_boundary_pos(option_a_line)
pos_b = find_right_boundary_pos(option_b_line)
pos_c = find_right_boundary_pos(option_c_line)

assert pos_a == pos_b == pos_c, (
f"Right boundaries not aligned: A={pos_a}, B={pos_b}, C={pos_c}"
)
10 changes: 9 additions & 1 deletion typer/rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
MARKUP_MODE_MARKDOWN = "markdown"
MARKUP_MODE_RICH = "rich"
_RICH_HELP_PANEL_NAME = "rich_help_panel"
ANSI_PREFIX = "\033["

MarkupModeStrict = Literal["markdown", "rich"]

Expand Down Expand Up @@ -124,6 +125,10 @@ class NegativeOptionHighlighter(RegexHighlighter):
negative_highlighter = NegativeOptionHighlighter()


def _has_ansi_character(text: str) -> bool:
return ANSI_PREFIX in text


def _get_rich_console(stderr: bool = False) -> Console:
return Console(
theme=Theme(
Expand Down Expand Up @@ -160,7 +165,10 @@ def _make_rich_text(
return Markdown(text, style=style)
else:
assert markup_mode == MARKUP_MODE_RICH
return highlighter(Text.from_markup(text, style=style))
if _has_ansi_character(text):
return highlighter(Text.from_ansi(text, style=style))
else:
return highlighter(Text.from_markup(text, style=style))


@group()
Expand Down