Coverage for typer / _click / _winconsole.py: 100%
145 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# This module is based on the excellent work by Adam Bartoš who
2# provided a lot of what went into the implementation here in
3# the discussion to issue1602 in the Python bug tracker.
4#
5# There are some general differences in regards to how this works
6# compared to the original patches as we do not need to patch
7# the entire interpreter but just work in our little world of
8# echo and prompt.
9import io 1ab
10import sys 1ab
11import time 1ab
12from collections.abc import Callable, Iterable, Mapping 1ab
13from ctypes import ( 1ab
14 POINTER,
15 Array,
16 Structure,
17 byref,
18 c_char,
19 c_char_p,
20 c_int,
21 c_ssize_t,
22 c_ulong,
23 c_void_p,
24 py_object,
25)
26from ctypes.wintypes import DWORD, HANDLE, LPCWSTR, LPWSTR 1ab
27from typing import ( 1ab
28 TYPE_CHECKING,
29 Any,
30 AnyStr,
31 BinaryIO,
32 Literal,
33 TextIO,
34 cast,
35)
37from ._compat import _NonClosingTextIOWrapper 1ab
39assert sys.platform == "win32" 1ab
40import msvcrt # noqa: E402 1ab
41from ctypes import WINFUNCTYPE, windll # noqa: E402 1ab
43c_ssize_p = POINTER(c_ssize_t) 1ab
45kernel32 = windll.kernel32 1ab
46GetStdHandle = kernel32.GetStdHandle 1ab
47ReadConsoleW = kernel32.ReadConsoleW 1ab
48WriteConsoleW = kernel32.WriteConsoleW 1ab
49GetConsoleMode = kernel32.GetConsoleMode 1ab
50GetLastError = kernel32.GetLastError 1ab
51GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) 1ab
52CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( 1ab
53 ("CommandLineToArgvW", windll.shell32)
54)
55LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32)) 1ab
57STDIN_HANDLE = GetStdHandle(-10) 1ab
58STDOUT_HANDLE = GetStdHandle(-11) 1ab
59STDERR_HANDLE = GetStdHandle(-12) 1ab
61PyBUF_SIMPLE = 0 1ab
62PyBUF_WRITABLE = 1 1ab
64ERROR_SUCCESS = 0 1ab
65ERROR_NOT_ENOUGH_MEMORY = 8 1ab
66ERROR_OPERATION_ABORTED = 995 1ab
68STDIN_FILENO = 0 1ab
69STDOUT_FILENO = 1 1ab
70STDERR_FILENO = 2 1ab
72EOF = b"\x1a" 1ab
73MAX_BYTES_WRITTEN = 32767 1ab
75if TYPE_CHECKING: 1ab
76 try:
77 # Using `typing_extensions.Buffer` instead of `collections.abc`
78 # on Windows for some reason does not have `Sized` implemented.
79 from collections.abc import Buffer # type: ignore
80 except ImportError:
81 from typing_extensions import Buffer
83try: 1ab
84 from ctypes import pythonapi 1ab
85except ImportError: # pragma: no cover
86 # On PyPy we cannot get buffers so our ability to operate here is
87 # severely limited.
88 get_buffer = None
89else:
91 class Py_buffer(Structure): 1ab
92 _fields_ = [ # noqa: RUF012 1ab
93 ("buf", c_void_p),
94 ("obj", py_object),
95 ("len", c_ssize_t),
96 ("itemsize", c_ssize_t),
97 ("readonly", c_int),
98 ("ndim", c_int),
99 ("format", c_char_p),
100 ("shape", c_ssize_p),
101 ("strides", c_ssize_p),
102 ("suboffsets", c_ssize_p),
103 ("internal", c_void_p),
104 ]
106 PyObject_GetBuffer = pythonapi.PyObject_GetBuffer 1ab
107 PyBuffer_Release = pythonapi.PyBuffer_Release 1ab
109 def get_buffer(obj: "Buffer", writable: bool = False) -> Array[c_char]: 1ab
110 buf = Py_buffer() 1ab
111 flags: int = PyBUF_WRITABLE if writable else PyBUF_SIMPLE 1ab
112 PyObject_GetBuffer(py_object(obj), byref(buf), flags) 1ab
114 try: 1ab
115 buffer_type = c_char * buf.len 1ab
116 out: Array[c_char] = buffer_type.from_address(buf.buf) 1ab
117 return out 1ab
118 finally:
119 PyBuffer_Release(byref(buf)) 1ab
122class _WindowsConsoleRawIOBase(io.RawIOBase): 1ab
123 def __init__(self, handle: int | None) -> None: 1ab
124 self.handle = handle 1ab
126 def isatty(self) -> Literal[True]: 1ab
127 super().isatty() 1ab
128 return True 1ab
131class _WindowsConsoleReader(_WindowsConsoleRawIOBase): 1ab
132 def readable(self) -> Literal[True]: 1ab
133 return True 1ab
135 def readinto(self, b: "Buffer") -> int: 1ab
136 bytes_to_be_read = len(b) 1ab
137 if not bytes_to_be_read: 1ab
138 return 0 1ab
139 elif bytes_to_be_read % 2: 1ab
140 raise ValueError( 1ab
141 "cannot read odd number of bytes from UTF-16-LE encoded console"
142 )
144 buffer = get_buffer(b, writable=True) 1ab
145 code_units_to_be_read = bytes_to_be_read // 2 1ab
146 code_units_read = c_ulong() 1ab
148 rv = ReadConsoleW( 1ab
149 HANDLE(self.handle),
150 buffer,
151 code_units_to_be_read,
152 byref(code_units_read),
153 None,
154 )
155 if GetLastError() == ERROR_OPERATION_ABORTED: 1ab
156 # wait for KeyboardInterrupt
157 time.sleep(0.1) 1ab
158 if not rv: 1ab
159 raise OSError(f"Windows error: {GetLastError()}") 1ab
161 if buffer[0] == EOF: 1ab
162 return 0 1ab
163 return 2 * code_units_read.value 1ab
166class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): 1ab
167 def writable(self) -> Literal[True]: 1ab
168 return True 1ab
170 @staticmethod 1ab
171 def _get_error_message(errno: int) -> str: 1ab
172 if errno == ERROR_SUCCESS: 1ab
173 return "ERROR_SUCCESS" 1ab
174 elif errno == ERROR_NOT_ENOUGH_MEMORY: 1ab
175 return "ERROR_NOT_ENOUGH_MEMORY" 1ab
176 return f"Windows error {errno}" 1ab
178 def write(self, b: "Buffer") -> int: 1ab
179 bytes_to_be_written = len(b) 1ab
180 buf = get_buffer(b) 1ab
181 code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2 1ab
182 code_units_written = c_ulong() 1ab
184 WriteConsoleW( 1ab
185 HANDLE(self.handle),
186 buf,
187 code_units_to_be_written,
188 byref(code_units_written),
189 None,
190 )
191 bytes_written = 2 * code_units_written.value 1ab
193 if bytes_written == 0 and bytes_to_be_written > 0: 1ab
194 raise OSError(self._get_error_message(GetLastError())) # pragma: no cover
195 return bytes_written 1ab
198class ConsoleStream: 1ab
199 def __init__(self, text_stream: TextIO, byte_stream: BinaryIO) -> None: 1ab
200 self._text_stream = text_stream 1ab
201 self.buffer = byte_stream 1ab
203 @property 1ab
204 def name(self) -> str: 1ab
205 return self.buffer.name 1ab
207 def write(self, x: AnyStr) -> int: 1ab
208 if isinstance(x, str): 1ab
209 return self._text_stream.write(x) 1ab
210 try: 1ab
211 self.flush() 1ab
212 except Exception: # pragma: no cover
213 pass
214 return self.buffer.write(x) 1ab
216 def writelines(self, lines: Iterable[AnyStr]) -> None: 1ab
217 for line in lines: 1ab
218 self.write(line) 1ab
220 def __getattr__(self, name: str) -> Any: 1ab
221 return getattr(self._text_stream, name) 1ab
223 def isatty(self) -> bool: 1ab
224 return self.buffer.isatty() 1ab
226 def __repr__(self) -> str: 1ab
227 return f"<ConsoleStream name={self.name!r} encoding={self.encoding!r}>" 1ab
230def _get_text_stdin(buffer_stream: BinaryIO) -> TextIO: 1ab
231 text_stream = _NonClosingTextIOWrapper( 1ab
232 io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)),
233 "utf-16-le",
234 "strict",
235 line_buffering=True,
236 )
237 return cast(TextIO, ConsoleStream(text_stream, buffer_stream)) 1ab
240def _get_text_stdout(buffer_stream: BinaryIO) -> TextIO: 1ab
241 text_stream = _NonClosingTextIOWrapper( 1ab
242 io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)),
243 "utf-16-le",
244 "strict",
245 line_buffering=True,
246 )
247 return cast(TextIO, ConsoleStream(text_stream, buffer_stream)) 1ab
250def _get_text_stderr(buffer_stream: BinaryIO) -> TextIO: 1ab
251 text_stream = _NonClosingTextIOWrapper( 1ab
252 io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)),
253 "utf-16-le",
254 "strict",
255 line_buffering=True,
256 )
257 return cast(TextIO, ConsoleStream(text_stream, buffer_stream)) 1ab
260_stream_factories: Mapping[int, Callable[[BinaryIO], TextIO]] = { 1ab
261 0: _get_text_stdin,
262 1: _get_text_stdout,
263 2: _get_text_stderr,
264}
267def _is_console(f: TextIO) -> bool: 1ab
268 if not hasattr(f, "fileno"): 1ab
269 return False 1ab
271 try: 1ab
272 fileno = f.fileno() 1ab
273 except (OSError, io.UnsupportedOperation): 1ab
274 return False 1ab
276 handle = msvcrt.get_osfhandle(fileno) 1ab
277 return bool(GetConsoleMode(handle, byref(DWORD()))) 1ab
280def _get_windows_console_stream( 1ab
281 f: TextIO, encoding: str | None, errors: str | None
282) -> TextIO | None:
283 if (
284 get_buffer is None
285 or encoding not in {"utf-16-le", None}
286 or errors not in {"strict", None}
287 or not _is_console(f)
288 ):
289 return None 1ab
291 func = _stream_factories.get(f.fileno()) 1ab
292 if func is None: 1ab
293 return None 1ab
295 b = getattr(f, "buffer", None) 1ab
297 if b is None: 1ab
298 return None 1ab
300 return func(b) 1ab