Coverage for tests / test_termui.py: 100%

272 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-09 00:15 +0000

1""" 

2Tests for the termui, echo, and CliRunner isolation functionality. 

3Created after vendoring Click to ensure test coverage is back up to 100%. 

4""" 

5 

6import io 1abfgcde

7import os 1abfgcde

8from contextlib import contextmanager 1abfgcde

9from typing import Literal 1abfgcde

10 

11import pytest 1abfgcde

12import typer 1abfgcde

13from typer._click import _termui_impl, termui 1abfgcde

14from typer.testing import CliRunner 1abfgcde

15 

16from tests.utils import needs_windows, skip_if_windows 1abfgcde

17 

18 

19def test_raw_terminal(monkeypatch): 1abfgcde

20 runner = CliRunner() 1abfgcde

21 app = typer.Typer() 1abfgcde

22 state = {"entered": 0, "exited": 0} 1abfgcde

23 

24 @contextmanager 1abfgcde

25 def fake_raw_terminal(): 1abfgcde

26 state["entered"] += 1 1abfgcde

27 try: 1abfgcde

28 yield 42 1abfgcde

29 finally: 

30 state["exited"] += 1 1abfgcde

31 

32 monkeypatch.setattr(_termui_impl, "raw_terminal", fake_raw_terminal) 1abfgcde

33 

34 @app.command() 1abfgcde

35 def main(): 1abfgcde

36 with termui.raw_terminal() as fd: 1abfgcde

37 typer.echo(f"fd={fd}") 1abfgcde

38 

39 result = runner.invoke(app, []) 1abfgcde

40 assert result.exit_code == 0, result.output 1abfgcde

41 assert "fd=42" in result.stdout 1abfgcde

42 assert state["entered"] == 1 1abfgcde

43 assert state["exited"] == 1 1abfgcde

44 

45 

46def test_getchar(monkeypatch): 1abfgcde

47 # Cached path: call the existing _getchar directly. 

48 cached_state = {"echo": None} 1abfgcde

49 

50 def cached_getchar(echo: bool) -> str: 1abfgcde

51 cached_state["echo"] = echo 1abfgcde

52 return "x" 1abfgcde

53 

54 monkeypatch.setattr(termui, "_getchar", cached_getchar) 1abfgcde

55 assert termui.getchar(echo=True) == "x" 1abfgcde

56 assert cached_state["echo"] is True 1abfgcde

57 

58 # Lazy-load path: _getchar is None, so import/cache _termui_impl.getchar. 

59 lazy_state = {"calls": 0} 1abfgcde

60 

61 def lazy_getchar(echo: bool) -> str: 1abfgcde

62 lazy_state["calls"] += 1 1abfgcde

63 return "y" if not echo else "z" 1abfgcde

64 

65 monkeypatch.setattr(termui, "_getchar", None) 1abfgcde

66 monkeypatch.setattr(_termui_impl, "getchar", lazy_getchar) 1abfgcde

67 

68 assert termui.getchar(echo=False) == "y" 1abfgcde

69 assert termui._getchar is lazy_getchar 1abfgcde

70 assert termui.getchar(echo=True) == "z" 1abfgcde

71 assert lazy_state["calls"] == 2 1abfgcde

72 

73 

74def test_clirunner_getchar(monkeypatch) -> None: 1abfgcde

75 runner = CliRunner() 1abfgcde

76 app = typer.Typer() 1abfgcde

77 

78 @app.command() 1abfgcde

79 def main() -> None: 1abfgcde

80 first = termui.getchar(echo=False) 1abfgcde

81 second = termui.getchar(echo=True) 1abfgcde

82 typer.echo(f"\nfirst={first};second={second}") 1abfgcde

83 

84 monkeypatch.setattr(termui, "_getchar", None) 1abfgcde

85 result = runner.invoke(app, [], input="ab") 1abfgcde

86 assert result.exit_code == 0, result.output 1abfgcde

87 assert result.stdout.splitlines() == ["b", "first=a;second=b"] 1abfgcde

88 

89 

90def test_clirunner_env_none(monkeypatch) -> None: 1abfgcde

91 runner = CliRunner() 1abfgcde

92 app = typer.Typer() 1abfgcde

93 env_key = "TYPER_TEST_ENV_REMOVE" 1abfgcde

94 monkeypatch.setenv(env_key, "present") 1abfgcde

95 

96 @app.command() 1abfgcde

97 def main() -> None: 1abfgcde

98 typer.echo(f"inside={os.environ.get(env_key)}") 1abfgcde

99 

100 result = runner.invoke(app, [], env={env_key: None}) 1abfgcde

101 assert result.exit_code == 0, result.output 1abfgcde

102 assert "inside=None" in result.stdout 1abfgcde

103 assert os.environ.get(env_key) == "present" 1abfgcde

104 

105 

106@pytest.mark.parametrize( 1abfgcde

107 ("exit_value", "expected_exit_code", "expected_stdout"), 

108 [ 

109 (None, 0, ""), 

110 ("bad-exit", 1, "bad-exit\n"), 

111 ], 

112) 

113def test_clirunner_invoke_system_exit_branches( 1abfgcde

114 exit_value: object, 

115 expected_exit_code: int, 

116 expected_stdout: str, 

117) -> None: 

118 runner = CliRunner() 1abfgcde

119 app = typer.Typer() 1abfgcde

120 

121 @app.command() 1abfgcde

122 def main() -> None: 1abfgcde

123 raise SystemExit(exit_value) 1abfgcde

124 

125 result = runner.invoke(app, []) 1abfgcde

126 assert result.exit_code == expected_exit_code 1abfgcde

127 assert result.stdout == expected_stdout 1abfgcde

128 if expected_exit_code: 1abfgcde

129 assert isinstance(result.exception, SystemExit) 1abfgcde

130 else: 

131 assert result.exception is None 1abfgcde

132 

133 

134@needs_windows 1abfgcde

135def test_termui_impl_windows_raw_terminal(): 1abfgcde

136 with _termui_impl.raw_terminal() as fd: 1fg

137 assert fd == -1 1fg

138 with termui.raw_terminal() as fd: 1fg

139 assert fd == -1 1fg

140 

141 

142@needs_windows 1abfgcde

143def test_termui_impl_windows_getchar(monkeypatch): 1abfgcde

144 monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: "a") 1fg

145 monkeypatch.setattr(_termui_impl.msvcrt, "getwche", lambda: "b") 1fg

146 assert _termui_impl.getchar(echo=False) == "a" 1fg

147 assert _termui_impl.getchar(echo=True) == "b" 1fg

148 

149 seq_null = iter(["\x00", "K"]) 1fg

150 monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: next(seq_null)) 1fg

151 assert _termui_impl.getchar(echo=False) == "\x00K" 1fg

152 

153 seq_e0 = iter(["\xe0", "H"]) 1fg

154 monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: next(seq_e0)) 1fg

155 assert _termui_impl.getchar(echo=False) == "\xe0H" 1fg

156 

157 seq_echo = iter(["\x00", "M"]) 1fg

158 monkeypatch.setattr(_termui_impl.msvcrt, "getwche", lambda: next(seq_echo)) 1fg

159 assert _termui_impl.getchar(echo=True) == "\x00M" 1fg

160 

161 seq_e0_echo = iter(["\xe0", "Z"]) 1fg

162 monkeypatch.setattr(_termui_impl.msvcrt, "getwche", lambda: next(seq_e0_echo)) 1fg

163 assert _termui_impl.getchar(echo=True) == "\xe0Z" 1fg

164 

165 monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: "\x03") 1fg

166 with pytest.raises(KeyboardInterrupt): 1fg

167 _termui_impl.getchar(echo=False) 1fg

168 

169 monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: "\x1a") 1fg

170 with pytest.raises(EOFError): 1fg

171 _termui_impl.getchar(echo=False) 1fg

172 

173 

174@skip_if_windows 1abfgcde

175@pytest.mark.parametrize("use_stdin_tty", [True, False]) 1abfgcde

176def test_termui_impl_posix_raw_terminal(monkeypatch, use_stdin_tty: bool): 1abfgcde

177 state: dict[str, object] = {} 1abcde

178 flushed: list[None] = [] 1abcde

179 fake_tty = None 1abcde

180 

181 if use_stdin_tty: 1abcde

182 expected_fd = 14 1abcde

183 old_termios = "old_settings" 1abcde

184 monkeypatch.setattr( 1abcde

185 _termui_impl, "isatty", lambda s: s is _termui_impl.sys.stdin 

186 ) 

187 monkeypatch.setattr(_termui_impl.sys.stdin, "fileno", lambda: expected_fd) 1abcde

188 else: 

189 expected_fd = 27 1abcde

190 old_termios = "old" 1abcde

191 monkeypatch.setattr( 1abcde

192 _termui_impl, 

193 "isatty", 

194 lambda s: s is not _termui_impl.sys.stdin, 

195 ) 

196 

197 class FakeTTY: 1abcde

198 def __init__(self) -> None: 1abcde

199 self.closed = False 1abcde

200 

201 def fileno(self) -> int: 1abcde

202 return expected_fd 1abcde

203 

204 def close(self) -> None: 1abcde

205 self.closed = True 1abcde

206 

207 fake_tty = FakeTTY() 1abcde

208 real_open = open 1abcde

209 

210 def fake_open(path, *args, **kwargs): 1abcde

211 if path == "/dev/tty": 1abcde

212 return fake_tty 1abcde

213 return real_open(path, *args, **kwargs) # pragma: no cover 

214 

215 monkeypatch.setattr("builtins.open", fake_open) 1abcde

216 

217 def tcgetattr(fd: int) -> str: 1abcde

218 state["tcgetattr_fd"] = fd 1abcde

219 return old_termios 1abcde

220 

221 def setraw(fd: int) -> None: 1abcde

222 state["setraw_fd"] = fd 1abcde

223 

224 def tcsetattr(fd: int, when: int, old: str) -> None: 1abcde

225 state["tcsetattr"] = (fd, when, old) 1abcde

226 

227 monkeypatch.setattr(_termui_impl.termios, "tcgetattr", tcgetattr) 1abcde

228 monkeypatch.setattr(_termui_impl.tty, "setraw", setraw) 1abcde

229 monkeypatch.setattr(_termui_impl.termios, "tcsetattr", tcsetattr) 1abcde

230 monkeypatch.setattr( 1abcde

231 _termui_impl.sys.stdout, "flush", lambda *a, **k: flushed.append(None) 

232 ) 

233 

234 with _termui_impl.raw_terminal() as fd: 1abcde

235 assert fd == expected_fd 1abcde

236 

237 assert state["tcgetattr_fd"] == expected_fd 1abcde

238 assert state["setraw_fd"] == expected_fd 1abcde

239 assert state["tcsetattr"] == ( 1abcde

240 expected_fd, 

241 _termui_impl.termios.TCSADRAIN, 

242 old_termios, 

243 ) 

244 assert flushed == [None] 1abcde

245 if fake_tty is not None: 1abcde

246 assert fake_tty.closed is True 1abcde

247 

248 

249@skip_if_windows 1abfgcde

250def test_termui_impl_posix_getchar(monkeypatch): 1abfgcde

251 @contextmanager 1abcde

252 def fake_raw(): 1abcde

253 yield 7 1abcde

254 

255 monkeypatch.setattr(_termui_impl, "raw_terminal", fake_raw) 1abcde

256 monkeypatch.setattr(_termui_impl.os, "read", lambda fd, n: b"q") 1abcde

257 monkeypatch.setattr(_termui_impl, "get_best_encoding", lambda stdin: "utf-8") 1abcde

258 monkeypatch.setattr(_termui_impl, "isatty", lambda f: f is _termui_impl.sys.stdout) 1abcde

259 written: list[str] = [] 1abcde

260 monkeypatch.setattr(_termui_impl.sys.stdout, "write", lambda s: written.append(s)) 1abcde

261 

262 assert _termui_impl.getchar(echo=True) == "q" 1abcde

263 assert written == ["q"] 1abcde

264 

265 

266@skip_if_windows 1abfgcde

267def test_termui_impl_posix_getchar_eof(monkeypatch): 1abfgcde

268 @contextmanager 1abcde

269 def fake_raw(): 1abcde

270 yield 5 1abcde

271 

272 monkeypatch.setattr(_termui_impl, "raw_terminal", fake_raw) 1abcde

273 monkeypatch.setattr(_termui_impl.os, "read", lambda fd, n: b"\x04") 1abcde

274 monkeypatch.setattr(_termui_impl, "get_best_encoding", lambda stdin: "utf-8") 1abcde

275 monkeypatch.setattr(_termui_impl, "isatty", lambda f: False) 1abcde

276 

277 with pytest.raises(EOFError): 1abcde

278 _termui_impl.getchar(echo=False) 1abcde

279 

280 

281def test_prompt(): 1abfgcde

282 runner = CliRunner() 1abfgcde

283 app = typer.Typer() 1abfgcde

284 fake_file = io.StringIO("data") 1abfgcde

285 fake_file.name = "demo.txt" 1abfgcde

286 

287 @app.command() 1abfgcde

288 def main( 1abfgcde

289 accept: bool = typer.Option(True, prompt=True), 

290 name: str = typer.Option(..., prompt=True), 

291 flavor: Literal["a", "b"] = typer.Option(..., prompt=True), 

292 city: str = typer.Option("London", prompt=True), 

293 config: str = typer.Option(fake_file, prompt=True), 

294 password: str = typer.Option( 

295 ..., 

296 prompt=True, 

297 hide_input=True, 

298 confirmation_prompt=True, 

299 ), 

300 ): 

301 typer.echo( 1abfgcde

302 f"accept={accept};name={name};flavor={flavor};city={city};config={config};pass_len={len(password)}" 

303 ) 

304 

305 result = runner.invoke(app, [], input="\nAda\na\n\ncustom.ini\nsecret\nsecret\n") 1abfgcde

306 assert result.exit_code == 0, result.output 1abfgcde

307 assert ( 1abfgcde

308 "accept=True;name=Ada;flavor=a;city=London;config=custom.ini;pass_len=6" 

309 in result.stdout 

310 ) 

311 assert "(a, b): " in result.stdout 1abfgcde

312 assert "[demo.txt]: " in result.stdout 1abfgcde

313 

314 

315def test_hidden_prompt_func(monkeypatch): 1abfgcde

316 monkeypatch.setattr("getpass.getpass", lambda prompt: "secret") 1abfgcde

317 assert termui.hidden_prompt_func("Password: ") == "secret" 1abfgcde

318 

319 

320def test_echo_stdout_missing(monkeypatch: pytest.MonkeyPatch) -> None: 1abfgcde

321 monkeypatch.setattr("sys.stdout", None) 1abfgcde

322 typer.echo("ignored") 1abfgcde

323 

324 

325def test_echo_stringifies() -> None: 1abfgcde

326 stream = io.StringIO() 1abfgcde

327 typer.echo(123, file=stream, nl=False) 1abfgcde

328 assert stream.getvalue() == "123" 1abfgcde

329 

330 

331def test_echo_bytes() -> None: 1abfgcde

332 buffer = io.BytesIO() 1abfgcde

333 stream = io.TextIOWrapper(buffer, encoding="utf-8") 1abfgcde

334 typer.echo(b"abc", file=stream, nl=True) 1abfgcde

335 assert buffer.getvalue() == b"abc\n" 1abfgcde

336 

337 

338def test_echo_empty_output() -> None: 1abfgcde

339 class FlushTrackingTextStream(io.StringIO): 1abfgcde

340 def __init__(self) -> None: 1abfgcde

341 super().__init__() 1abfgcde

342 self.flush_count = 0 1abfgcde

343 

344 def flush(self) -> None: 1abfgcde

345 self.flush_count += 1 1abfgcde

346 super().flush() 1abfgcde

347 

348 def write(self, s: str) -> int: 1abfgcde

349 raise AssertionError("Empty output") # pragma: no cover 

350 

351 stream = FlushTrackingTextStream() 1abfgcde

352 typer.echo("", file=stream, nl=False) 1abfgcde

353 assert stream.flush_count == 1 1abfgcde

354 

355 

356@needs_windows 1abfgcde

357def test_echo_windows_color_none( 1abfgcde

358 monkeypatch: pytest.MonkeyPatch, 

359) -> None: 

360 class TtyStream(io.StringIO): 1fg

361 def isatty(self) -> bool: 1fg

362 return True 1fg

363 

364 stream = TtyStream() 1fg

365 monkeypatch.setattr("typer._click.utils.auto_wrap_for_ansi", None) 1fg

366 typer.echo("\x1b[31mred\x1b[0m", file=stream, nl=False, color=None) 1fg

367 assert stream.getvalue() == "red" 1fg

368 

369 

370@pytest.mark.parametrize( 1abfgcde

371 ("flag", "true_code", "false_code"), 

372 [ 

373 ("bold", "\x1b[1m", "\x1b[22m"), 

374 ("dim", "\x1b[2m", "\x1b[22m"), 

375 ("underline", "\x1b[4m", "\x1b[24m"), 

376 ("overline", "\x1b[53m", "\x1b[55m"), 

377 ("italic", "\x1b[3m", "\x1b[23m"), 

378 ("blink", "\x1b[5m", "\x1b[25m"), 

379 ("reverse", "\x1b[7m", "\x1b[27m"), 

380 ("strikethrough", "\x1b[9m", "\x1b[29m"), 

381 ], 

382) 

383def test_style(flag, true_code, false_code): 1abfgcde

384 runner = CliRunner() 1abfgcde

385 app = typer.Typer() 1abfgcde

386 

387 @app.command() 1abfgcde

388 def main(): 1abfgcde

389 # testing an int and a str on purpose 

390 typer.echo("TRUE=" + typer.style("42", **{flag: True}), color=True) 1abfgcde

391 typer.echo("FALSE=" + typer.style(666, **{flag: False}), color=True) 1abfgcde

392 

393 result = runner.invoke(app, []) 1abfgcde

394 assert result.exit_code == 0, result.output 1abfgcde

395 lines = [line for line in result.stdout.splitlines() if line] 1abfgcde

396 true_line = next(line for line in lines if line.startswith("TRUE=")) 1abfgcde

397 false_line = next(line for line in lines if line.startswith("FALSE=")) 1abfgcde

398 assert "42" in true_line 1abfgcde

399 assert "666" in false_line 1abfgcde

400 

401 assert true_code in true_line 1abfgcde

402 assert true_code not in false_line 1abfgcde

403 assert false_code in false_line 1abfgcde

404 assert false_code not in true_line 1abfgcde

405 

406 

407def test_style_color(): 1abfgcde

408 fg_int = typer.style("x", fg=123) 1abfgcde

409 assert "\x1b[38;5;123m" in fg_int 1abfgcde

410 

411 bg_list = typer.style("x", bg=[1, 2, 3]) 1abfgcde

412 assert "\x1b[48;2;1;2;3m" in bg_list 1abfgcde

413 

414 with pytest.raises(TypeError, match="Unknown color"): 1abfgcde

415 typer.style("x", fg="not-a-color") 1abfgcde

416 

417 with pytest.raises(TypeError, match="Unknown color"): 1abfgcde

418 typer.style("x", bg="not-a-color") 1abfgcde

419 

420 

421def test_termui_launch(monkeypatch): 1abfgcde

422 captured = {} 1abfgcde

423 

424 def fake_open_url(url, wait=False, locate=False): 1abfgcde

425 captured["url"] = url 1abfgcde

426 captured["wait"] = wait 1abfgcde

427 captured["locate"] = locate 1abfgcde

428 return 7 1abfgcde

429 

430 monkeypatch.setattr(_termui_impl, "open_url", fake_open_url) 1abfgcde

431 rv = termui.launch("https://example.com", wait=True, locate=True) 1abfgcde

432 assert rv == 7 1abfgcde

433 assert captured == { 1abfgcde

434 "url": "https://example.com", 

435 "wait": True, 

436 "locate": True, 

437 }