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

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) 

36 

37from ._compat import _NonClosingTextIOWrapper 1ab

38 

39assert sys.platform == "win32" 1ab

40import msvcrt # noqa: E402 1ab

41from ctypes import WINFUNCTYPE, windll # noqa: E402 1ab

42 

43c_ssize_p = POINTER(c_ssize_t) 1ab

44 

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

56 

57STDIN_HANDLE = GetStdHandle(-10) 1ab

58STDOUT_HANDLE = GetStdHandle(-11) 1ab

59STDERR_HANDLE = GetStdHandle(-12) 1ab

60 

61PyBUF_SIMPLE = 0 1ab

62PyBUF_WRITABLE = 1 1ab

63 

64ERROR_SUCCESS = 0 1ab

65ERROR_NOT_ENOUGH_MEMORY = 8 1ab

66ERROR_OPERATION_ABORTED = 995 1ab

67 

68STDIN_FILENO = 0 1ab

69STDOUT_FILENO = 1 1ab

70STDERR_FILENO = 2 1ab

71 

72EOF = b"\x1a" 1ab

73MAX_BYTES_WRITTEN = 32767 1ab

74 

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 

82 

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: 

90 

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 ] 

105 

106 PyObject_GetBuffer = pythonapi.PyObject_GetBuffer 1ab

107 PyBuffer_Release = pythonapi.PyBuffer_Release 1ab

108 

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

113 

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

120 

121 

122class _WindowsConsoleRawIOBase(io.RawIOBase): 1ab

123 def __init__(self, handle: int | None) -> None: 1ab

124 self.handle = handle 1ab

125 

126 def isatty(self) -> Literal[True]: 1ab

127 super().isatty() 1ab

128 return True 1ab

129 

130 

131class _WindowsConsoleReader(_WindowsConsoleRawIOBase): 1ab

132 def readable(self) -> Literal[True]: 1ab

133 return True 1ab

134 

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 ) 

143 

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

147 

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

160 

161 if buffer[0] == EOF: 1ab

162 return 0 1ab

163 return 2 * code_units_read.value 1ab

164 

165 

166class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): 1ab

167 def writable(self) -> Literal[True]: 1ab

168 return True 1ab

169 

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

177 

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

183 

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

192 

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

196 

197 

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

202 

203 @property 1ab

204 def name(self) -> str: 1ab

205 return self.buffer.name 1ab

206 

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

215 

216 def writelines(self, lines: Iterable[AnyStr]) -> None: 1ab

217 for line in lines: 1ab

218 self.write(line) 1ab

219 

220 def __getattr__(self, name: str) -> Any: 1ab

221 return getattr(self._text_stream, name) 1ab

222 

223 def isatty(self) -> bool: 1ab

224 return self.buffer.isatty() 1ab

225 

226 def __repr__(self) -> str: 1ab

227 return f"<ConsoleStream name={self.name!r} encoding={self.encoding!r}>" 1ab

228 

229 

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

238 

239 

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

248 

249 

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

258 

259 

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} 

265 

266 

267def _is_console(f: TextIO) -> bool: 1ab

268 if not hasattr(f, "fileno"): 1ab

269 return False 1ab

270 

271 try: 1ab

272 fileno = f.fileno() 1ab

273 except (OSError, io.UnsupportedOperation): 1ab

274 return False 1ab

275 

276 handle = msvcrt.get_osfhandle(fileno) 1ab

277 return bool(GetConsoleMode(handle, byref(DWORD()))) 1ab

278 

279 

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

290 

291 func = _stream_factories.get(f.fileno()) 1ab

292 if func is None: 1ab

293 return None 1ab

294 

295 b = getattr(f, "buffer", None) 1ab

296 

297 if b is None: 1ab

298 return None 1ab

299 

300 return func(b) 1ab