Coverage for tests / test_rich_utils.py: 100%
160 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +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 TypesHighlighter (used to be the MetavarHighlighter) works correctly.
112 cf PR 1508
113 """
114 from typer.rich_utils import ( 1abcdefg
115 STYLE_TYPES_SEPARATOR,
116 Text,
117 _get_rich_console,
118 types_highlighter,
119 )
121 console = _get_rich_console() 1abcdefg
123 text = Text(input_text) 1abcdefg
124 highlighted = types_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 types_sep style
132 assert str(opening_bracket_style) == STYLE_TYPES_SEPARATOR 1abcdefg
134 # The closing bracket should have types_sep style (fails before PR 1508 when there are 3 dots)
135 assert str(closing_bracket_style) == STYLE_TYPES_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 )
227def test_rich_help_no_boolean_type() -> None: 1abcdefg
228 app = typer.Typer(rich_markup_mode="rich") 1abcdefg
230 @app.command() 1abcdefg
231 def main(name: str) -> None: 1abcdefg
232 pass # pragma: no cover
234 result = runner.invoke(app, ["--help"]) 1abcdefg
235 assert result.exit_code == 0 1abcdefg
236 assert "<boolean>" not in result.output 1abcdefg
239def test_rich_help_metavar(): 1abcdefg
240 app = typer.Typer(rich_markup_mode="rich") 1abcdefg
242 @app.command() 1abcdefg
243 def main( 1abcdefg
244 *,
245 arg1: int,
246 arg2: int = 42,
247 arg3: int = typer.Argument(...),
248 ARG4: int = typer.Argument(42),
249 ARG5: int = typer.Option(...),
250 arg6: int = typer.Option(42),
251 arg7: int = typer.Argument(42, metavar="meta7"),
252 arg8: int = typer.Argument(metavar="ARG8"),
253 arg9: int = typer.Argument(metavar="arg9"),
254 ):
255 pass # pragma: no cover
257 result = runner.invoke(app, ["--help"]) 1abcdefg
258 assert ( 1abcdefg
259 "Usage: main [OPTIONS] {arg1} {arg3} [ARG4] [meta7] {ARG8} {arg9}"
260 in result.stdout
261 )
263 out_nospace = result.stdout.replace(" ", "") 1abcdefg
265 # arguments
266 assert "arg1<int>" in out_nospace 1abcdefg
267 assert "arg3<int>" in out_nospace 1abcdefg
268 assert "ARG4<int>" in out_nospace 1abcdefg
269 assert "meta7<int>" in out_nospace 1abcdefg
270 assert "ARG8<int>" in out_nospace 1abcdefg
271 assert "arg9<int>" in out_nospace 1abcdefg
273 assert "arg7" not in result.stdout.lower() 1abcdefg
274 assert "arg8" not in result.stdout 1abcdefg
275 assert "ARG9" not in result.stdout 1abcdefg
277 # options
278 assert "--arg2<int>" in out_nospace 1abcdefg
279 assert "--ARG5<int>" in out_nospace 1abcdefg
280 assert "--arg6<int>" in out_nospace 1abcdefg
283def test_rich_lowercase_bracketed_metavar() -> None: 1abcdefg
284 # Make sure Rich doesn't "swallow" a lowercased [path] (thinking it's a Rich annotation)
285 app = typer.Typer(rich_markup_mode="rich") 1abcdefg
287 @app.callback() 1abcdefg
288 def main(path_or_module: str = typer.Argument(None)) -> None: 1abcdefg
289 pass # pragma: no cover 1abcdefg
291 @app.command() 1abcdefg
292 def run(name: str) -> None: 1abcdefg
293 pass # pragma: no cover
295 result = runner.invoke(app, ["my-module.py", "run"], prog_name="typer") 1abcdefg
297 assert result.exit_code == 2 1abcdefg
298 usage_line = result.output.splitlines()[0] 1abcdefg
299 assert usage_line.startswith("Usage: typer [path_or_module] run [OPTIONS] {name}") 1abcdefg
300 assert "Try 'typer [path_or_module] run --help' for help." in result.output 1abcdefg