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

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 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 ) 

120 

121 console = _get_rich_console() 1abcdefg

122 

123 text = Text(input_text) 1abcdefg

124 highlighted = metavar_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 metavar_sep style 

132 assert str(opening_bracket_style) == STYLE_METAVAR_SEPARATOR 1abcdefg

133 

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

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 )