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
« 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"""
6import io 1abfgcde
7import os 1abfgcde
8from contextlib import contextmanager 1abfgcde
9from typing import Literal 1abfgcde
11import pytest 1abfgcde
12import typer 1abfgcde
13from typer._click import _termui_impl, termui 1abfgcde
14from typer.testing import CliRunner 1abfgcde
16from tests.utils import needs_windows, skip_if_windows 1abfgcde
19def test_raw_terminal(monkeypatch): 1abfgcde
20 runner = CliRunner() 1abfgcde
21 app = typer.Typer() 1abfgcde
22 state = {"entered": 0, "exited": 0} 1abfgcde
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
32 monkeypatch.setattr(_termui_impl, "raw_terminal", fake_raw_terminal) 1abfgcde
34 @app.command() 1abfgcde
35 def main(): 1abfgcde
36 with termui.raw_terminal() as fd: 1abfgcde
37 typer.echo(f"fd={fd}") 1abfgcde
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
46def test_getchar(monkeypatch): 1abfgcde
47 # Cached path: call the existing _getchar directly.
48 cached_state = {"echo": None} 1abfgcde
50 def cached_getchar(echo: bool) -> str: 1abfgcde
51 cached_state["echo"] = echo 1abfgcde
52 return "x" 1abfgcde
54 monkeypatch.setattr(termui, "_getchar", cached_getchar) 1abfgcde
55 assert termui.getchar(echo=True) == "x" 1abfgcde
56 assert cached_state["echo"] is True 1abfgcde
58 # Lazy-load path: _getchar is None, so import/cache _termui_impl.getchar.
59 lazy_state = {"calls": 0} 1abfgcde
61 def lazy_getchar(echo: bool) -> str: 1abfgcde
62 lazy_state["calls"] += 1 1abfgcde
63 return "y" if not echo else "z" 1abfgcde
65 monkeypatch.setattr(termui, "_getchar", None) 1abfgcde
66 monkeypatch.setattr(_termui_impl, "getchar", lazy_getchar) 1abfgcde
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
74def test_clirunner_getchar(monkeypatch) -> None: 1abfgcde
75 runner = CliRunner() 1abfgcde
76 app = typer.Typer() 1abfgcde
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
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
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
96 @app.command() 1abfgcde
97 def main() -> None: 1abfgcde
98 typer.echo(f"inside={os.environ.get(env_key)}") 1abfgcde
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
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
121 @app.command() 1abfgcde
122 def main() -> None: 1abfgcde
123 raise SystemExit(exit_value) 1abfgcde
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
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
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
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
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
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
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
165 monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: "\x03") 1fg
166 with pytest.raises(KeyboardInterrupt): 1fg
167 _termui_impl.getchar(echo=False) 1fg
169 monkeypatch.setattr(_termui_impl.msvcrt, "getwch", lambda: "\x1a") 1fg
170 with pytest.raises(EOFError): 1fg
171 _termui_impl.getchar(echo=False) 1fg
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
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 )
197 class FakeTTY: 1abcde
198 def __init__(self) -> None: 1abcde
199 self.closed = False 1abcde
201 def fileno(self) -> int: 1abcde
202 return expected_fd 1abcde
204 def close(self) -> None: 1abcde
205 self.closed = True 1abcde
207 fake_tty = FakeTTY() 1abcde
208 real_open = open 1abcde
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
215 monkeypatch.setattr("builtins.open", fake_open) 1abcde
217 def tcgetattr(fd: int) -> str: 1abcde
218 state["tcgetattr_fd"] = fd 1abcde
219 return old_termios 1abcde
221 def setraw(fd: int) -> None: 1abcde
222 state["setraw_fd"] = fd 1abcde
224 def tcsetattr(fd: int, when: int, old: str) -> None: 1abcde
225 state["tcsetattr"] = (fd, when, old) 1abcde
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 )
234 with _termui_impl.raw_terminal() as fd: 1abcde
235 assert fd == expected_fd 1abcde
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
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
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
262 assert _termui_impl.getchar(echo=True) == "q" 1abcde
263 assert written == ["q"] 1abcde
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
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
277 with pytest.raises(EOFError): 1abcde
278 _termui_impl.getchar(echo=False) 1abcde
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
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 )
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
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
320def test_echo_stdout_missing(monkeypatch: pytest.MonkeyPatch) -> None: 1abfgcde
321 monkeypatch.setattr("sys.stdout", None) 1abfgcde
322 typer.echo("ignored") 1abfgcde
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
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
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
344 def flush(self) -> None: 1abfgcde
345 self.flush_count += 1 1abfgcde
346 super().flush() 1abfgcde
348 def write(self, s: str) -> int: 1abfgcde
349 raise AssertionError("Empty output") # pragma: no cover
351 stream = FlushTrackingTextStream() 1abfgcde
352 typer.echo("", file=stream, nl=False) 1abfgcde
353 assert stream.flush_count == 1 1abfgcde
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
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
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
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
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
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
407def test_style_color(): 1abfgcde
408 fg_int = typer.style("x", fg=123) 1abfgcde
409 assert "\x1b[38;5;123m" in fg_int 1abfgcde
411 bg_list = typer.style("x", bg=[1, 2, 3]) 1abfgcde
412 assert "\x1b[48;2;1;2;3m" in bg_list 1abfgcde
414 with pytest.raises(TypeError, match="Unknown color"): 1abfgcde
415 typer.style("x", fg="not-a-color") 1abfgcde
417 with pytest.raises(TypeError, match="Unknown color"): 1abfgcde
418 typer.style("x", bg="not-a-color") 1abfgcde
421def test_termui_launch(monkeypatch): 1abfgcde
422 captured = {} 1abfgcde
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
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 }