Coverage for tests / test_rich_utils.py: 100%
123 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-26 21:46 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-26 21:46 +0000
1import sys 1abcdefg
3import pytest 1abcdefg
4import typer 1abcdefg
5import typer.completion 1abcdefg
6from typer.testing import CliRunner 1abcdefg
8from tests.utils import needs_rich 1abcdefg
10runner = CliRunner() 1abcdefg
13def test_rich_utils_click_rewrapp(): 1abcdefg
14 app = typer.Typer(rich_markup_mode="markdown") 1abcdefg
16 @app.command() 1abcdefg
17 def main(): 1abcdefg
18 """
19 \b
20 Some text
22 Some unwrapped text
23 """
24 print("Hello World") 1abcdefg
26 @app.command() 1abcdefg
27 def secondary(): 1abcdefg
28 """
29 \b
30 Secondary text
32 Some unwrapped text
33 """
34 print("Hello Secondary World") 1abcdefg
36 result = runner.invoke(app, ["--help"]) 1abcdefg
37 assert "Some text" in result.stdout 1abcdefg
38 assert "Secondary text" in result.stdout 1abcdefg
39 assert "\b" not in result.stdout 1abcdefg
40 result = runner.invoke(app, ["main"]) 1abcdefg
41 assert "Hello World" in result.stdout 1abcdefg
42 result = runner.invoke(app, ["secondary"]) 1abcdefg
43 assert "Hello Secondary World" in result.stdout 1abcdefg
46def test_rich_help_no_commands(): 1abcdefg
47 """Ensure that the help still works for a Typer instance with no commands, but with a callback."""
48 app = typer.Typer(help="My cool Typer app") 1abcdefg
50 @app.callback(invoke_without_command=True, no_args_is_help=True) 1abcdefg
51 def main() -> None: 1abcdefg
52 return None # pragma: no cover
54 result = runner.invoke(app, ["--help"]) 1abcdefg
56 assert result.exit_code == 0 1abcdefg
57 assert "Show this message" in result.stdout 1abcdefg
60def test_rich_doesnt_print_None_default(): 1abcdefg
61 app = typer.Typer(rich_markup_mode="rich") 1abcdefg
63 @app.command() 1abcdefg
64 def main( 1abcdefg
65 name: str,
66 option_1: str = typer.Option(
67 "option_1_default",
68 ),
69 option_2: str = typer.Option(
70 ...,
71 ),
72 ):
73 print(f"Hello {name}") 1abcdefg
74 print(f"First: {option_1}") 1abcdefg
75 print(f"Second: {option_2}") 1abcdefg
77 result = runner.invoke(app, ["--help"]) 1abcdefg
78 assert "Usage" in result.stdout 1abcdefg
79 assert "name" in result.stdout 1abcdefg
80 assert "option-1" in result.stdout 1abcdefg
81 assert "option-2" in result.stdout 1abcdefg
82 assert result.stdout.count("[default: None]") == 0 1abcdefg
83 result = runner.invoke(app, ["Rick", "--option-2=Morty"]) 1abcdefg
84 assert "Hello Rick" in result.stdout 1abcdefg
85 assert "First: option_1_default" in result.stdout 1abcdefg
86 assert "Second: Morty" in result.stdout 1abcdefg
89def test_rich_markup_import_regression(): 1abcdefg
90 # Remove rich.markup if it was imported by other tests
91 if "rich" in sys.modules: 1abcdefg
92 rich_module = sys.modules["rich"] 1abcdefg
93 if hasattr(rich_module, "markup"): 1abcdefg
94 delattr(rich_module, "markup") 1abcdefg
96 app = typer.Typer(rich_markup_mode=None) 1abcdefg
98 @app.command() 1abcdefg
99 def main(bar: str): 1abcdefg
100 pass # pragma: no cover
102 result = runner.invoke(app, ["--help"]) 1abcdefg
103 assert "Usage" in result.stdout 1abcdefg
104 assert "BAR" in result.stdout 1abcdefg
107@needs_rich 1abcdefg
108@pytest.mark.parametrize("input_text", ["[ARGS]", "[ARGS]..."]) 1abcdefg
109def test_metavar_highlighter(input_text: str): 1abcdefg
110 """
111 Test that the MetavarHighlighter works correctly.
112 cf PR 1508
113 """
114 from typer.rich_utils import ( 1abcdefg
115 STYLE_METAVAR_SEPARATOR,
116 Text,
117 _get_rich_console,
118 metavar_highlighter,
119 )
121 console = _get_rich_console() 1abcdefg
123 text = Text(input_text) 1abcdefg
124 highlighted = metavar_highlighter(text) 1abcdefg
125 console.print(highlighted) 1abcdefg
127 # Get the style for each bracket
128 opening_bracket_style = highlighted.get_style_at_offset(console, 0) 1abcdefg
129 closing_bracket_style = highlighted.get_style_at_offset(console, 5) 1abcdefg
131 # The opening bracket should have metavar_sep style
132 assert str(opening_bracket_style) == STYLE_METAVAR_SEPARATOR 1abcdefg
134 # The closing bracket should have metavar_sep style (fails before PR 1508 when there are 3 dots)
135 assert str(closing_bracket_style) == STYLE_METAVAR_SEPARATOR 1abcdefg
138def test_make_rich_text_with_ansi_escape_sequences(): 1abcdefg
139 from typer.rich_utils import Text, _make_rich_text 1abcdefg
141 ansi_text = "This is \x1b[4munderlined\x1b[0m text" 1abcdefg
142 result = _make_rich_text(text=ansi_text, markup_mode="rich") 1abcdefg
144 assert isinstance(result, Text) 1abcdefg
145 assert "\x1b[" not in result.plain 1abcdefg
146 assert "underlined" in result.plain 1abcdefg
148 mixed_text = "Start \x1b[31mred\x1b[0m middle \x1b[32mgreen\x1b[0m end" 1abcdefg
149 result = _make_rich_text(text=mixed_text, markup_mode="rich") 1abcdefg
150 assert isinstance(result, Text) 1abcdefg
151 assert "\x1b[" not in result.plain 1abcdefg
152 assert "red" in result.plain 1abcdefg
153 assert "green" in result.plain 1abcdefg
155 fake_ansi = "This contains \x1b[ but not a complete sequence" 1abcdefg
156 result = _make_rich_text(text=fake_ansi, markup_mode="rich") 1abcdefg
157 assert isinstance(result, Text) 1abcdefg
158 assert "\x1b[" not in result.plain 1abcdefg
159 assert "This contains " in result.plain 1abcdefg
162def test_make_rich_text_with_typer_style_in_help(): 1abcdefg
163 app = typer.Typer() 1abcdefg
165 @app.command() 1abcdefg
166 def example( 1abcdefg
167 a: str = typer.Option(help="This is A"),
168 b: str = typer.Option(help=f"This is {typer.style('B', underline=True)}"),
169 ):
170 """Example command with styled help text."""
171 pass # pragma: no cover
173 result = runner.invoke(app, ["--help"]) 1abcdefg
175 assert result.exit_code == 0 1abcdefg
176 assert "This is A" in result.stdout 1abcdefg
177 assert "This is B" in result.stdout 1abcdefg
178 assert "\x1b[" not in result.stdout 1abcdefg
181def test_help_table_alignment_with_styled_text(): 1abcdefg
182 app = typer.Typer() 1abcdefg
184 @app.command() 1abcdefg
185 def example( 1abcdefg
186 a: str = typer.Option(help="This is A"),
187 b: str = typer.Option(help=f"This is {typer.style('B', underline=True)}"),
188 c: str = typer.Option(help="This is C"),
189 ):
190 """Example command with styled help text."""
191 pass # pragma: no cover
193 result = runner.invoke(app, ["--help"]) 1abcdefg
195 assert result.exit_code == 0 1abcdefg
197 lines = result.stdout.split("\n") 1abcdefg
199 option_a_line = None 1abcdefg
200 option_b_line = None 1abcdefg
201 option_c_line = None 1abcdefg
203 for line in lines: 1abcdefg
204 if "--a" in line and "This is A" in line: 1abcdefg
205 option_a_line = line 1abcdefg
206 elif "--b" in line and "This is B" in line: 1abcdefg
207 option_b_line = line 1abcdefg
208 elif "--c" in line and "This is C" in line: 1abcdefg
209 option_c_line = line 1abcdefg
211 assert option_a_line is not None, "Option A line not found" 1abcdefg
212 assert option_b_line is not None, "Option B line not found" 1abcdefg
213 assert option_c_line is not None, "Option C line not found" 1abcdefg
215 def find_right_boundary_pos(line): 1abcdefg
216 return line.rfind("|") 1abcdefg
218 pos_a = find_right_boundary_pos(option_a_line) 1abcdefg
219 pos_b = find_right_boundary_pos(option_b_line) 1abcdefg
220 pos_c = find_right_boundary_pos(option_c_line) 1abcdefg
222 assert pos_a == pos_b == pos_c, ( 1abcdefg
223 f"Right boundaries not aligned: A={pos_a}, B={pos_b}, C={pos_c}"
224 )