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

1import sys 1abcdefg

2 

3import pytest 1abcdefg

4import typer 1abcdefg

5import typer.completion 1abcdefg

6from typer.testing import CliRunner 1abcdefg

7 

8from tests.utils import needs_rich 1abcdefg

9 

10runner = CliRunner() 1abcdefg

11 

12 

13def test_rich_utils_click_rewrapp(): 1abcdefg

14 app = typer.Typer(rich_markup_mode="markdown") 1abcdefg

15 

16 @app.command() 1abcdefg

17 def main(): 1abcdefg

18 """ 

19 \b 

20 Some text 

21 

22 Some unwrapped text 

23 """ 

24 print("Hello World") 1abcdefg

25 

26 @app.command() 1abcdefg

27 def secondary(): 1abcdefg

28 """ 

29 \b 

30 Secondary text 

31 

32 Some unwrapped text 

33 """ 

34 print("Hello Secondary World") 1abcdefg

35 

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

44 

45 

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

49 

50 @app.callback(invoke_without_command=True, no_args_is_help=True) 1abcdefg

51 def main() -> None: 1abcdefg

52 return None # pragma: no cover 

53 

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

55 

56 assert result.exit_code == 0 1abcdefg

57 assert "Show this message" in result.stdout 1abcdefg

58 

59 

60def test_rich_doesnt_print_None_default(): 1abcdefg

61 app = typer.Typer(rich_markup_mode="rich") 1abcdefg

62 

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

76 

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

87 

88 

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

95 

96 app = typer.Typer(rich_markup_mode=None) 1abcdefg

97 

98 @app.command() 1abcdefg

99 def main(bar: str): 1abcdefg

100 pass # pragma: no cover 

101 

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

103 assert "Usage" in result.stdout 1abcdefg

104 assert "{bar}" in result.stdout 1abcdefg

105 

106 

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 ) 

120 

121 console = _get_rich_console() 1abcdefg

122 

123 text = Text(input_text) 1abcdefg

124 highlighted = types_highlighter(text) 1abcdefg

125 console.print(highlighted) 1abcdefg

126 

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

130 

131 # The opening bracket should have types_sep style 

132 assert str(opening_bracket_style) == STYLE_TYPES_SEPARATOR 1abcdefg

133 

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

136 

137 

138def test_make_rich_text_with_ansi_escape_sequences(): 1abcdefg

139 from typer.rich_utils import Text, _make_rich_text 1abcdefg

140 

141 ansi_text = "This is \x1b[4munderlined\x1b[0m text" 1abcdefg

142 result = _make_rich_text(text=ansi_text, markup_mode="rich") 1abcdefg

143 

144 assert isinstance(result, Text) 1abcdefg

145 assert "\x1b[" not in result.plain 1abcdefg

146 assert "underlined" in result.plain 1abcdefg

147 

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

154 

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

160 

161 

162def test_make_rich_text_with_typer_style_in_help(): 1abcdefg

163 app = typer.Typer() 1abcdefg

164 

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 

172 

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

174 

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

179 

180 

181def test_help_table_alignment_with_styled_text(): 1abcdefg

182 app = typer.Typer() 1abcdefg

183 

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 

192 

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

194 

195 assert result.exit_code == 0 1abcdefg

196 

197 lines = result.stdout.split("\n") 1abcdefg

198 

199 option_a_line = None 1abcdefg

200 option_b_line = None 1abcdefg

201 option_c_line = None 1abcdefg

202 

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

210 

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

214 

215 def find_right_boundary_pos(line): 1abcdefg

216 return line.rfind("|") 1abcdefg

217 

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

221 

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 ) 

225 

226 

227def test_rich_help_no_boolean_type() -> None: 1abcdefg

228 app = typer.Typer(rich_markup_mode="rich") 1abcdefg

229 

230 @app.command() 1abcdefg

231 def main(name: str) -> None: 1abcdefg

232 pass # pragma: no cover 

233 

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

235 assert result.exit_code == 0 1abcdefg

236 assert "<boolean>" not in result.output 1abcdefg

237 

238 

239def test_rich_help_metavar(): 1abcdefg

240 app = typer.Typer(rich_markup_mode="rich") 1abcdefg

241 

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 

256 

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 ) 

262 

263 out_nospace = result.stdout.replace(" ", "") 1abcdefg

264 

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

272 

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

276 

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

281 

282 

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

286 

287 @app.callback() 1abcdefg

288 def main(path_or_module: str = typer.Argument(None)) -> None: 1abcdefg

289 pass # pragma: no cover 1abcdefg

290 

291 @app.command() 1abcdefg

292 def run(name: str) -> None: 1abcdefg

293 pass # pragma: no cover 

294 

295 result = runner.invoke(app, ["my-module.py", "run"], prog_name="typer") 1abcdefg

296 

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