Coverage for typer / _click / _compat.py: 100%
248 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
1import codecs 1cdabefg
2import io 1cdabefg
3import os 1cdabefg
4import re 1cdabefg
5import sys 1cdabefg
6from collections.abc import Callable, Mapping, MutableMapping 1cdabefg
7from types import TracebackType 1cdabefg
8from typing import ( 1cdabefg
9 IO,
10 Any,
11 BinaryIO,
12 TextIO,
13 cast,
14)
15from weakref import WeakKeyDictionary 1cdabefg
17CYGWIN = sys.platform.startswith("cygwin") 1cdabefg
18WIN = sys.platform.startswith("win") 1cdabefg
19auto_wrap_for_ansi: Callable[[TextIO], TextIO] | None = None 1cdabefg
20_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") 1cdabefg
23def _make_text_stream( 1cdabefg
24 stream: BinaryIO,
25 encoding: str | None,
26 errors: str,
27) -> TextIO:
28 if encoding is None: 1cdabefg
29 encoding = get_best_encoding(stream) 1cdabefg
30 return _NonClosingTextIOWrapper( 1cdabefg
31 stream,
32 encoding,
33 errors,
34 line_buffering=True,
35 )
38def is_ascii_encoding(encoding: str) -> bool: 1cdabefg
39 """Checks if a given encoding is ascii."""
40 try: 1cdabefg
41 return codecs.lookup(encoding).name == "ascii" 1cdabefg
42 except LookupError: 1cdabefg
43 return False 1cdabefg
46def get_best_encoding(stream: IO[Any]) -> str: 1cdabefg
47 """Returns the default stream encoding if not found."""
48 rv = getattr(stream, "encoding", None) or sys.getdefaultencoding() 1cdabefg
49 if is_ascii_encoding(rv): 1cdabefg
50 return "utf-8" 1cdabefg
51 return rv 1cdabefg
54class _NonClosingTextIOWrapper(io.TextIOWrapper): 1cdabefg
55 def __init__( 1cdabefg
56 self,
57 stream: BinaryIO,
58 encoding: str | None,
59 errors: str | None,
60 **extra: Any,
61 ) -> None:
62 self._stream = stream = cast(BinaryIO, _FixupStream(stream)) 1cdabefg
63 super().__init__(stream, encoding, errors, **extra) 1cdabefg
65 def __del__(self) -> None: 1cdabefg
66 try: 1cdabefg
67 self.detach() 1cdabefg
68 except Exception: # pragma: no cover 1cdabefg
69 pass 1cdabefg
71 def isatty(self) -> bool: 1cdabefg
72 # https://bitbucket.org/pypy/pypy/issue/1803
73 return self._stream.isatty() 1cdabefg
76class _FixupStream: 1cdabefg
77 """The new io interface needs more from streams than streams
78 traditionally implement. As such, this fix-up code is necessary in
79 some circumstances.
80 """
82 def __init__( 1cdabefg
83 self,
84 stream: BinaryIO,
85 ):
86 self._stream = stream 1cdabefg
88 def __getattr__(self, name: str) -> Any: 1cdabefg
89 return getattr(self._stream, name) 1cdabefg
91 def read1(self, size: int) -> bytes: 1cdabefg
92 f = getattr(self._stream, "read1", None) 1cdabefg
94 if f is not None: 1cdabefg
95 return cast(bytes, f(size)) 1ab
97 return self._stream.read(size) 1cdabefg
99 def readable(self) -> bool: 1cdabefg
100 return True 1cdabefg
102 def writable(self) -> bool: 1cdabefg
103 return True 1cdabefg
105 def seekable(self) -> bool: 1cdabefg
106 x = getattr(self._stream, "seekable", None) 1cdabefg
107 if x is not None: 1cdabefg
108 return cast(bool, x()) 1cdabefg
109 return False 1cdabefg
112def _is_binary_reader(stream: IO[Any], default: bool = False) -> bool: 1cdabefg
113 try: 1cdabefg
114 return isinstance(stream.read(0), bytes) 1cdabefg
115 except Exception: # pragma: no cover 1cdabefg
116 return default 1cdabefg
117 # This happens in some cases where the stream was already
118 # closed. In this case, we assume the default.
121def _is_binary_writer(stream: IO[Any], default: bool = False) -> bool: 1cdabefg
122 try: 1cdabefg
123 stream.write(b"") 1cdabefg
124 except Exception: # pragma: no cover 1cdabefg
125 try: 1cdabefg
126 stream.write("") 1cdabefg
127 return False 1cdabefg
128 except Exception: 1cdabefg
129 pass 1cdabefg
130 return default 1cdabefg
131 return True 1cdabefg
134def _find_binary_reader(stream: IO[Any]) -> BinaryIO | None: 1cdabefg
135 # We need to figure out if the given stream is already binary.
136 # This can happen because the official docs recommend detaching
137 # the streams to get binary streams. Some code might do this, so
138 # we need to deal with this case explicitly.
139 if _is_binary_reader(stream, False): 1cdabefg
140 return cast(BinaryIO, stream) 1cdabefg
142 buf = getattr(stream, "buffer", None) 1cdabefg
144 # Same situation here; this time we assume that the buffer is
145 # actually binary in case it's closed.
146 if buf is not None and _is_binary_reader(buf, True): 1cdabefg
147 return cast(BinaryIO, buf) 1cdabefg
149 return None 1cdabefg
152def _find_binary_writer(stream: IO[Any]) -> BinaryIO | None: 1cdabefg
153 # We need to figure out if the given stream is already binary.
154 # This can happen because the official docs recommend detaching
155 # the streams to get binary streams. Some code might do this, so
156 # we need to deal with this case explicitly.
157 if _is_binary_writer(stream, False): 1cdabefg
158 return cast(BinaryIO, stream) 1cdabefg
160 buf = getattr(stream, "buffer", None) 1cdabefg
162 # Same situation here; this time we assume that the buffer is
163 # actually binary in case it's closed.
164 if buf is not None and _is_binary_writer(buf, True): 1cdabefg
165 return cast(BinaryIO, buf) 1cdabefg
167 return None 1cdabefg
170def _stream_is_misconfigured(stream: TextIO) -> bool: 1cdabefg
171 """A stream is misconfigured if its encoding is ASCII."""
172 # If the stream does not have an encoding set, we assume it's set
173 # to ASCII. This appears to happen in certain unittest
174 # environments. It's not quite clear what the correct behavior is
175 # but this at least will force Click to recover somehow.
176 return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii") 1cdabefg
179def _is_compat_stream_attr(stream: TextIO, attr: str, value: str | None) -> bool: 1cdabefg
180 """A stream attribute is compatible if it is equal to the
181 desired value or the desired value is unset and the attribute
182 has a value.
183 """
184 stream_value = getattr(stream, attr, None) 1cdabefg
185 return stream_value == value or (value is None and stream_value is not None) 1cdabefg
188def _is_compatible_text_stream( 1cdabefg
189 stream: TextIO, encoding: str | None, errors: str | None
190) -> bool:
191 """Check if a stream's encoding and errors attributes are
192 compatible with the desired values.
193 """
194 return _is_compat_stream_attr( 1cdabefg
195 stream, "encoding", encoding
196 ) and _is_compat_stream_attr(stream, "errors", errors)
199def _force_correct_text_stream( 1cdabefg
200 text_stream: IO[Any],
201 encoding: str | None,
202 errors: str | None,
203 is_binary: Callable[[IO[Any], bool], bool],
204 find_binary: Callable[[IO[Any]], BinaryIO | None],
205) -> TextIO:
206 if is_binary(text_stream, False): 1cdabefg
207 binary_reader = cast(BinaryIO, text_stream) 1cdabefg
208 else:
209 text_stream = cast(TextIO, text_stream) 1cdabefg
210 # If the stream looks compatible, and won't default to a
211 # misconfigured ascii encoding, return it as-is.
212 if _is_compatible_text_stream(text_stream, encoding, errors) and not ( 1cdabefg
213 encoding is None and _stream_is_misconfigured(text_stream)
214 ):
215 return text_stream 1cdabefg
217 # Otherwise, get the underlying binary reader.
218 possible_binary_reader = find_binary(text_stream) 1cdabefg
220 # If that's not possible, silently use the original reader
221 # and get mojibake instead of exceptions.
222 if possible_binary_reader is None: 1cdabefg
223 return text_stream 1cdabefg
225 binary_reader = possible_binary_reader 1cdabefg
227 # Default errors to replace instead of strict in order to get
228 # something that works.
229 if errors is None: 1cdabefg
230 errors = "replace" 1cdabefg
232 # Wrap the binary stream in a text stream with the correct
233 # encoding parameters.
234 return _make_text_stream( 1cdabefg
235 binary_reader,
236 encoding,
237 errors,
238 )
241def _force_correct_text_reader( 1cdabefg
242 text_reader: IO[Any],
243 encoding: str | None,
244 errors: str | None,
245) -> TextIO:
246 return _force_correct_text_stream( 1cdabefg
247 text_reader,
248 encoding,
249 errors,
250 _is_binary_reader,
251 _find_binary_reader,
252 )
255def _force_correct_text_writer( 1cdabefg
256 text_writer: IO[Any],
257 encoding: str | None,
258 errors: str | None,
259) -> TextIO:
260 return _force_correct_text_stream( 1cdabefg
261 text_writer,
262 encoding,
263 errors,
264 _is_binary_writer,
265 _find_binary_writer,
266 )
269def get_binary_stdin() -> BinaryIO: 1cdabefg
270 reader = _find_binary_reader(sys.stdin) 1cdabefg
271 if reader is None: # pragma: no cover 1cdabefg
272 raise RuntimeError("Was not able to determine binary stream for sys.stdin.") 1cdabefg
273 return reader 1cdabefg
276def get_binary_stdout() -> BinaryIO: 1cdabefg
277 writer = _find_binary_writer(sys.stdout) 1cdabefg
278 if writer is None: # pragma: no cover 1cdabefg
279 raise RuntimeError("Was not able to determine binary stream for sys.stdout.")
280 return writer 1cdabefg
283def get_binary_stderr() -> BinaryIO: 1cdabefg
284 writer = _find_binary_writer(sys.stderr) 1cdabefg
285 if writer is None: # pragma: no cover 1cdabefg
286 raise RuntimeError("Was not able to determine binary stream for sys.stderr.")
287 return writer 1cdabefg
290def get_text_stdin(encoding: str | None = None, errors: str | None = None) -> TextIO: 1cdabefg
291 rv = _get_windows_console_stream(sys.stdin, encoding, errors) 1cdabefg
292 if rv is not None: 1cdabefg
293 return rv 1ab
294 return _force_correct_text_reader(sys.stdin, encoding, errors) 1cdabefg
297def get_text_stdout(encoding: str | None = None, errors: str | None = None) -> TextIO: 1cdabefg
298 rv = _get_windows_console_stream(sys.stdout, encoding, errors) 1cdabefg
299 if rv is not None: 1cdabefg
300 return rv 1ab
301 return _force_correct_text_writer(sys.stdout, encoding, errors) 1cdabefg
304def get_text_stderr(encoding: str | None = None, errors: str | None = None) -> TextIO: 1cdabefg
305 rv = _get_windows_console_stream(sys.stderr, encoding, errors) 1cdabefg
306 if rv is not None: 1cdabefg
307 return rv 1ab
308 return _force_correct_text_writer(sys.stderr, encoding, errors) 1cdabefg
311def _wrap_io_open( 1cdabefg
312 file: str | os.PathLike[str] | int,
313 mode: str,
314 encoding: str | None,
315 errors: str | None,
316) -> IO[Any]:
317 """Handles not passing ``encoding`` and ``errors`` in binary mode."""
318 if "b" in mode: 1cdabefg
319 return open(file, mode) 1cdabefg
321 return open(file, mode, encoding=encoding, errors=errors) 1cdabefg
324def open_stream( 1cdabefg
325 filename: str | os.PathLike[str],
326 mode: str = "r",
327 encoding: str | None = None,
328 errors: str | None = "strict",
329 atomic: bool = False,
330) -> tuple[IO[Any], bool]:
331 binary = "b" in mode 1cdabefg
332 filename = os.fspath(filename) 1cdabefg
334 # Standard streams first, ignoring the atomic flag.
335 if os.fsdecode(filename) == "-": 1cdabefg
336 if any(m in mode for m in ["w", "a", "x"]): 1cdabefg
337 if binary: 1cdabefg
338 return get_binary_stdout(), False 1cdabefg
339 return get_text_stdout(encoding=encoding, errors=errors), False 1cdabefg
340 if binary: 1cdabefg
341 return get_binary_stdin(), False 1cdabefg
342 return get_text_stdin(encoding=encoding, errors=errors), False 1cdabefg
344 # Non-atomic writes directly go out through the regular open functions.
345 if not atomic: 1cdabefg
346 return _wrap_io_open(filename, mode, encoding, errors), True 1cdabefg
348 # Some usability stuff for atomic writes
349 if "a" in mode: 1cdabefg
350 raise ValueError( 1cdabefg
351 "Appending to an existing file is not supported, because that"
352 " would involve an expensive `copy`-operation to a temporary"
353 " file. Open the file in normal `w`-mode and copy explicitly"
354 " if that's what you're after."
355 )
356 if "x" in mode: 1cdabefg
357 raise ValueError("Use the `overwrite`-parameter instead.") 1cdabefg
358 if "w" not in mode: 1cdabefg
359 raise ValueError("Atomic writes only make sense with `w`-mode.") 1cdabefg
361 # Atomic writes are more complicated. They work by opening a file
362 # as a proxy in the same folder and then using the fdopen
363 # functionality to wrap it in a Python file. Then we wrap it in an
364 # atomic file that moves the file over on close.
365 import errno 1cdabefg
366 import random 1cdabefg
368 try: 1cdabefg
369 perm: int | None = os.stat(filename).st_mode 1cdabefg
370 except OSError: # pragma: no cover 1cdabefg
371 perm = None 1cdabefg
373 flags = os.O_RDWR | os.O_CREAT | os.O_EXCL 1cdabefg
375 if binary: 1cdabefg
376 flags |= getattr(os, "O_BINARY", 0) 1cdabefg
378 while True: 1cdabefg
379 tmp_filename = os.path.join( 1cdabefg
380 os.path.dirname(filename),
381 f".__atomic-write{random.randrange(1 << 32):08x}",
382 )
383 try: 1cdabefg
384 fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm) 1cdabefg
385 break 1cdabefg
386 except OSError as e: # pragma: no cover
387 if e.errno == errno.EEXIST or (
388 os.name == "nt"
389 and e.errno == errno.EACCES
390 and os.path.isdir(e.filename)
391 and os.access(e.filename, os.W_OK)
392 ):
393 continue
394 raise
396 if perm is not None: 1cdabefg
397 os.chmod(tmp_filename, perm) # in case perm includes bits in umask 1cdabefg
399 f = _wrap_io_open(fd, mode, encoding, errors) 1cdabefg
400 af = _AtomicFile(f, tmp_filename, os.path.realpath(filename)) 1cdabefg
401 return cast(IO[Any], af), True 1cdabefg
404class _AtomicFile: 1cdabefg
405 def __init__(self, f: IO[Any], tmp_filename: str, real_filename: str) -> None: 1cdabefg
406 self._f = f 1cdabefg
407 self._tmp_filename = tmp_filename 1cdabefg
408 self._real_filename = real_filename 1cdabefg
409 self.closed = False 1cdabefg
411 @property 1cdabefg
412 def name(self) -> str: 1cdabefg
413 return self._real_filename 1cdabefg
415 def close(self, delete: bool = False) -> None: 1cdabefg
416 if self.closed: 1cdabefg
417 return # pragma: no cover 1cdabefg
418 self._f.close() 1cdabefg
419 os.replace(self._tmp_filename, self._real_filename) 1cdabefg
420 self.closed = True 1cdabefg
422 def __getattr__(self, name: str) -> Any: 1cdabefg
423 return getattr(self._f, name) 1cdabefg
425 def __enter__(self) -> "_AtomicFile": 1cdabefg
426 return self 1cdabefg
428 def __exit__( 1cdabefg
429 self,
430 exc_type: type[BaseException] | None,
431 exc_value: BaseException | None,
432 tb: TracebackType | None,
433 ) -> None:
434 self.close(delete=exc_type is not None) 1cdabefg
436 def __repr__(self) -> str: 1cdabefg
437 return repr(self._f) 1cdabefg
440def strip_ansi(value: str) -> str: 1cdabefg
441 return _ansi_re.sub("", value) 1cdabefg
444def _is_jupyter_kernel_output(stream: IO[Any]) -> bool: 1cdabefg
445 while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)): 1cdabefg
446 stream = stream._stream 1cdabefg
448 return stream.__class__.__module__.startswith("ipykernel.") 1cdabefg
451def should_strip_ansi(stream: IO[Any] | None = None, color: bool | None = None) -> bool: 1cdabefg
452 if color is None: 1cdabefg
453 if stream is None: 1cdabefg
454 stream = sys.stdin 1cdabefg
455 return not isatty(stream) and not _is_jupyter_kernel_output(stream) 1cdabefg
456 return not color 1cdabefg
459# On Windows, wrap the output streams with colorama to support ANSI
460# color codes.
461# NOTE: double check is needed so mypy does not analyze this on Linux
462if sys.platform.startswith("win") and WIN: 1cdabefg
463 from ._winconsole import _get_windows_console_stream 1ab
465 def _get_argv_encoding() -> str: 1ab
466 import locale 1ab
468 return locale.getpreferredencoding() 1ab
470 _ansi_stream_wrappers: MutableMapping[TextIO, TextIO] = WeakKeyDictionary() 1ab
472 def auto_wrap_for_ansi(stream: TextIO, color: bool | None = None) -> TextIO: 1ab
473 """Support ANSI color and style codes on Windows by wrapping a
474 stream with colorama.
475 """
476 try: 1ab
477 cached = _ansi_stream_wrappers.get(stream) 1ab
478 except Exception: # pragma: no cover
479 cached = None
481 if cached is not None: 1ab
482 return cached 1ab
484 import colorama 1ab
486 strip = should_strip_ansi(stream, color) 1ab
487 ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) 1ab
488 rv = cast(TextIO, ansi_wrapper.stream) 1ab
489 _write = rv.write 1ab
491 def _safe_write(s: str) -> int: 1ab
492 try: 1ab
493 return _write(s) 1ab
494 except BaseException: # pragma: no cover
495 ansi_wrapper.reset_all()
496 raise
498 rv.write = _safe_write # type: ignore[method-assign] 1ab
500 try: 1ab
501 _ansi_stream_wrappers[stream] = rv 1ab
502 except Exception: # pragma: no cover
503 pass
505 return rv 1ab
507else:
509 def _get_argv_encoding() -> str: 1cdefg
510 return getattr(sys.stdin, "encoding", None) or sys.getfilesystemencoding() 1cd
512 def _get_windows_console_stream( 1cdefg
513 f: TextIO, encoding: str | None, errors: str | None
514 ) -> TextIO | None:
515 return None 1cdefg
518def term_len(x: str) -> int: 1cdabefg
519 return len(strip_ansi(x)) 1cdabefg
522def isatty(stream: IO[Any]) -> bool: 1cdabefg
523 try: 1cdabefg
524 return stream.isatty() 1cdabefg
525 except Exception: # pragma: no cover
526 return False
529def _make_cached_stream_func( 1cdabefg
530 src_func: Callable[[], TextIO],
531 wrapper_func: Callable[[], TextIO],
532) -> Callable[[], TextIO]:
533 cache: MutableMapping[TextIO, TextIO] = WeakKeyDictionary() 1cdabefg
535 def func() -> TextIO: 1cdabefg
536 stream = src_func() 1cdabefg
538 try: 1cdabefg
539 rv = cache.get(stream) 1cdabefg
540 except Exception: # pragma: no cover 1cdabefg
541 rv = None 1cdabefg
542 if rv is not None: 1cdabefg
543 return rv 1cdabefg
544 rv = wrapper_func() 1cdabefg
545 try: 1cdabefg
546 cache[stream] = rv 1cdabefg
547 except Exception: # pragma: no cover 1cdabefg
548 pass 1cdabefg
549 return rv 1cdabefg
551 return func 1cdabefg
554_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin) 1cdabefg
555_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout) 1cdabefg
556_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr) 1cdabefg
559binary_streams: Mapping[str, Callable[[], BinaryIO]] = { 1cdabefg
560 "stdin": get_binary_stdin,
561 "stdout": get_binary_stdout,
562 "stderr": get_binary_stderr,
563}
565text_streams: Mapping[str, Callable[[str | None, str | None], TextIO]] = { 1cdabefg
566 "stdin": get_text_stdin,
567 "stdout": get_text_stdout,
568 "stderr": get_text_stderr,
569}