Coverage for typer / _click / utils.py: 100%
190 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 os 1cdabefg
2import re 1cdabefg
3import sys 1cdabefg
4from collections.abc import Callable, Iterable, Iterator 1cdabefg
5from functools import update_wrapper 1cdabefg
6from types import ModuleType, TracebackType 1cdabefg
7from typing import ( 1cdabefg
8 IO,
9 Any,
10 AnyStr,
11 BinaryIO,
12 Literal,
13 ParamSpec,
14 TextIO,
15 TypeVar,
16 cast,
17)
19from ._compat import ( 1cdabefg
20 WIN,
21 _default_text_stderr,
22 _default_text_stdout,
23 _find_binary_writer,
24 auto_wrap_for_ansi,
25 binary_streams,
26 open_stream,
27 should_strip_ansi,
28 strip_ansi,
29 text_streams,
30)
31from .globals import resolve_color_default 1cdabefg
33P = ParamSpec("P") 1cdabefg
34R = TypeVar("R") 1cdabefg
37def _escape_control_characters(value: str) -> str: 1cdabefg
38 """Display untrusted diagnostic values without terminal control characters."""
39 return re.sub( 1cdabefg
40 r"[\x00-\x1f\x7f-\x9f]", lambda match: f"\\x{ord(match[0]):02x}", value
41 )
44def _posixify(name: str) -> str: 1cdabefg
45 return "-".join(name.split()).lower() 1cd
48def safecall(func: Callable[P, R]) -> Callable[P, R | None]: 1cdabefg
49 """Wraps a function so that it swallows exceptions."""
51 def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None: 1cdabefg
52 try: 1cdabefg
53 return func(*args, **kwargs) 1cdabefg
54 except Exception: # pragma: no cover
55 pass
56 return None # pragma: no cover
58 return update_wrapper(wrapper, func) 1cdabefg
61def make_default_short_help(help: str, max_length: int = 45) -> str: 1cdabefg
62 """Returns a condensed version of help string."""
63 # Consider only the first paragraph.
64 paragraph_end = help.find("\n\n") 1cdabefg
66 if paragraph_end != -1: 1cdabefg
67 help = help[:paragraph_end] 1cdabefg
69 # Collapse newlines, tabs, and spaces.
70 words = help.split() 1cdabefg
72 if not words: 1cdabefg
73 return "" 1cdabefg
75 # The first paragraph started with a "no rewrap" marker, ignore it.
76 if words[0] == "\b": 1cdabefg
77 words = words[1:] 1cdabefg
79 total_length = 0 1cdabefg
80 last_index = len(words) - 1 1cdabefg
82 for i, word in enumerate(words): 1cdabefg
83 total_length += len(word) + (i > 0) 1cdabefg
85 if total_length > max_length: # too long, truncate 1cdabefg
86 break 1cdabefg
88 if word[-1] == ".": # sentence end, truncate without "..." 1cdabefg
89 return " ".join(words[: i + 1]) 1cdabefg
91 if total_length == max_length and i != last_index: 1cdabefg
92 break # not at sentence end, truncate with "..." 1cdabefg
93 else:
94 return " ".join(words) # no truncation needed 1cdabefg
96 # Account for the length of the suffix.
97 total_length += len("...") 1cdabefg
99 # remove words until the length is short enough
100 while i > 0: 1cdabefg
101 total_length -= len(words[i]) + (i > 0) 1cdabefg
103 if total_length <= max_length: 1cdabefg
104 break 1cdabefg
106 i -= 1 1cdabefg
108 return " ".join(words[:i]) + "..." 1cdabefg
111class LazyFile: 1cdabefg
112 """A lazy file works like a regular file but it does not fully open
113 the file but it does perform some basic checks early to see if the
114 filename parameter does make sense. This is useful for safely opening
115 files for writing.
116 """
118 def __init__( 1cdabefg
119 self,
120 filename: str | os.PathLike[str],
121 mode: str = "r",
122 encoding: str | None = None,
123 errors: str | None = "strict",
124 atomic: bool = False,
125 ):
126 self.name: str = os.fspath(filename) 1cdabefg
127 self.mode = mode 1cdabefg
128 self.encoding = encoding 1cdabefg
129 self.errors = errors 1cdabefg
130 self.atomic = atomic 1cdabefg
131 self._f: IO[Any] | None 1cdabefg
132 self.should_close: bool 1cdabefg
134 if self.name == "-": 1cdabefg
135 self._f, self.should_close = open_stream(filename, mode, encoding, errors) 1cdabefg
136 else:
137 if "r" in mode: 1cdabefg
138 # Open and close the file in case we're opening it for
139 # reading so that we can catch at least some errors in
140 # some cases early.
141 open(filename, mode).close() 1cdabefg
142 self._f = None 1cdabefg
143 self.should_close = True 1cdabefg
145 def __getattr__(self, name: str) -> Any: 1cdabefg
146 return getattr(self.open(), name) 1cdabefg
148 def __repr__(self) -> str: 1cdabefg
149 if self._f is not None: 1cdabefg
150 return repr(self._f) 1cdabefg
151 return f"<unopened file '{format_filename(self.name)}' {self.mode}>" 1cdabefg
153 def open(self) -> IO[Any]: 1cdabefg
154 """Opens the file if it's not yet open. This call might fail with
155 a `FileError`. Not handling this error will produce an error
156 that Click shows.
157 """
158 if self._f is not None: 1cdabefg
159 return self._f 1cdabefg
160 try: 1cdabefg
161 rv, self.should_close = open_stream( 1cdabefg
162 self.name, self.mode, self.encoding, self.errors, atomic=self.atomic
163 )
164 except OSError as e: 1cdabefg
165 from .exceptions import FileError 1cdabefg
167 raise FileError(self.name, hint=e.strerror) from e 1cdabefg
168 self._f = rv 1cdabefg
169 return rv 1cdabefg
171 def close(self) -> None: 1cdabefg
172 """Closes the underlying file, no matter what."""
173 if self._f is not None: 1cdabefg
174 self._f.close() 1cdabefg
176 def close_intelligently(self) -> None: 1cdabefg
177 """This function only closes the file if it was opened by the lazy
178 file wrapper. For instance this will never close stdin.
179 """
180 if self.should_close: 1cdabefg
181 self.close() 1cdabefg
183 def __enter__(self) -> "LazyFile": 1cdabefg
184 return self 1cdabefg
186 def __exit__( 1cdabefg
187 self,
188 exc_type: type[BaseException] | None,
189 exc_value: BaseException | None,
190 tb: TracebackType | None,
191 ) -> None:
192 self.close_intelligently() 1cdabefg
194 def __iter__(self) -> Iterator[AnyStr]: 1cdabefg
195 self.open() 1cdabefg
196 return iter(self._f) # type: ignore 1cdabefg
199def echo( 1cdabefg
200 message: Any | None = None,
201 file: IO[Any] | None = None,
202 nl: bool = True,
203 err: bool = False,
204 color: bool | None = None,
205) -> None:
206 """Print a message and newline to stdout or a file. This should be
207 used instead of `print` because it provides better support
208 for different data, files, and environments.
210 Compared to `print`, this does the following:
212 - Ensures that the output encoding is not misconfigured on Linux.
213 - Supports Unicode in the Windows console.
214 - Supports writing to binary outputs, and supports writing bytes
215 to text outputs.
216 - Supports colors and styles on Windows.
217 - Removes ANSI color and style codes if the output does not look
218 like an interactive terminal.
219 - Always flushes the output.
220 """
221 if file is None: 1cdabefg
222 if err: 1cdabefg
223 file = _default_text_stderr() 1cdabefg
224 else:
225 file = _default_text_stdout() 1cdabefg
227 # There are no standard streams attached to write to. For example,
228 # pythonw on Windows.
229 if file is None: 1cdabefg
230 return 1cdabefg
232 # Convert non bytes/text into the native string type.
233 if message is not None and not isinstance(message, (str, bytes, bytearray)): 1cdabefg
234 out: str | bytes | bytearray | None = str(message) 1cdabefg
235 else:
236 out = message 1cdabefg
238 if nl: 1cdabefg
239 out = out or "" 1cdabefg
240 if isinstance(out, str): 1cdabefg
241 out += "\n" 1cdabefg
242 else:
243 out += b"\n" 1cdabefg
245 if not out: 1cdabefg
246 file.flush() 1cdabefg
247 return 1cdabefg
249 # If there is a message and the value looks like bytes, we manually
250 # need to find the binary stream and write the message in there.
251 # This is done separately so that most stream types will work as you
252 # would expect. Eg: you can write to StringIO for other cases.
253 if isinstance(out, (bytes, bytearray)): 1cdabefg
254 binary_file = _find_binary_writer(file) 1cdabefg
256 if binary_file is not None: 1cdabefg
257 file.flush() 1cdabefg
258 binary_file.write(out) 1cdabefg
259 binary_file.flush() 1cdabefg
260 return 1cdabefg
262 # ANSI style code support. For no message or bytes, nothing happens.
263 # When outputting to a file instead of a terminal, strip codes.
264 else:
265 color = resolve_color_default(color) 1cdabefg
267 if should_strip_ansi(file, color): 1cdabefg
268 out = strip_ansi(out) 1cdabefg
269 elif WIN: 1cdabefg
270 if auto_wrap_for_ansi is not None: 1ab
271 file = auto_wrap_for_ansi(file, color) # type: ignore[arg-type,call-arg] 1ab
272 elif not color: 1ab
273 out = strip_ansi(out) 1ab
275 file.write(out) # type: ignore[arg-type] 1cdabefg
276 file.flush() 1cdabefg
279def get_binary_stream(name: Literal["stdin", "stdout", "stderr"]) -> BinaryIO: 1cdabefg
280 """Returns a system stream for byte processing."""
281 opener = binary_streams.get(name) 1cdabefg
282 if opener is None: 1cdabefg
283 raise TypeError(f"Unknown standard stream '{name}'") 1cdabefg
284 return opener() 1cdabefg
287def get_text_stream( 1cdabefg
288 name: Literal["stdin", "stdout", "stderr"],
289 encoding: str | None = None,
290 errors: str | None = "strict",
291) -> TextIO:
292 """Returns a system stream for text processing. This usually returns
293 a wrapped stream around a binary stream returned from
294 `get_binary_stream` but it also can take shortcuts for already
295 correctly configured streams.
296 """
297 opener = text_streams.get(name) 1cdabefg
298 if opener is None: 1cdabefg
299 raise TypeError(f"Unknown standard stream '{name}'") 1cdabefg
300 return opener(encoding, errors) 1cdabefg
303def format_filename( 1cdabefg
304 filename: str | bytes | os.PathLike[str] | os.PathLike[bytes],
305 shorten: bool = False,
306) -> str:
307 """Format a filename as a string for display. Ensures the filename can be
308 displayed by replacing any invalid bytes or surrogate escapes in the name
309 with the replacement character ``�``.
311 Invalid bytes or surrogate escapes will raise an error when written to a
312 stream with ``errors="strict"``. This will typically happen with ``stdout``
313 when the locale is something like ``en_GB.UTF-8``.
315 Many scenarios *are* safe to write surrogates though, due to PEP 538 and
316 PEP 540, including:
318 - Writing to ``stderr``, which uses ``errors="backslashreplace"``.
319 - The system has ``LANG=C.UTF-8``, ``C``, or ``POSIX``. Python opens
320 stdout and stderr with ``errors="surrogateescape"``.
321 - None of ``LANG/LC_*`` are set. Python assumes ``LANG=C.UTF-8``.
322 - Python is started in UTF-8 mode with ``PYTHONUTF8=1`` or ``-X utf8``.
323 Python opens stdout and stderr with ``errors="surrogateescape"``.
324 """
325 if shorten: 1cdabefg
326 filename = os.path.basename(filename) 1cdabefg
327 else:
328 filename = os.fspath(filename) 1cdabefg
330 if isinstance(filename, bytes): 1cdabefg
331 filename = filename.decode(sys.getfilesystemencoding(), "replace") 1cdabefg
332 else:
333 filename = filename.encode("utf-8", "surrogateescape").decode( 1cdabefg
334 "utf-8", "replace"
335 )
337 return filename 1cdabefg
340def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str: 1cdabefg
341 r"""Returns the config folder for the application. The default behavior
342 is to return whatever is most appropriate for the operating system.
344 To give you an idea, for an app called ``"Foo Bar"``, something like
345 the following folders could be returned:
347 Mac OS X:
348 ``~/Library/Application Support/Foo Bar``
349 Mac OS X (POSIX):
350 ``~/.foo-bar``
351 Unix:
352 ``~/.config/foo-bar``
353 Unix (POSIX):
354 ``~/.foo-bar``
355 Windows (roaming):
356 ``C:\Users\<user>\AppData\Roaming\Foo Bar``
357 Windows (not roaming):
358 ``C:\Users\<user>\AppData\Local\Foo Bar``
359 """
360 if WIN: 1cdabefg
361 key = "APPDATA" if roaming else "LOCALAPPDATA" 1ab
362 folder = os.environ.get(key) 1ab
363 if folder is None: 1ab
364 folder = os.path.expanduser("~") 1ab
365 return os.path.join(folder, app_name) 1ab
366 if force_posix: 1cdefg
367 return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}")) 1cd
368 if sys.platform == "darwin": 1cdefg
369 return os.path.join( 1efg
370 os.path.expanduser("~/Library/Application Support"), app_name
371 )
372 return os.path.join( 1cd
373 os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),
374 _posixify(app_name),
375 )
378class PacifyFlushWrapper: 1cdabefg
379 """This wrapper is used to catch and suppress BrokenPipeErrors resulting
380 from ``.flush()`` being called on broken pipe during the shutdown/final-GC
381 of the Python interpreter. Notably ``.flush()`` is always called on
382 ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any
383 other cleanup code, and the case where the underlying file is not a broken
384 pipe, all calls and attributes are proxied.
385 """
387 def __init__(self, wrapped: IO[Any]) -> None: 1cdabefg
388 self.wrapped = wrapped 1cdabefg
390 def flush(self) -> None: 1cdabefg
391 try: 1cdabefg
392 self.wrapped.flush() 1cdabefg
393 except OSError as e: # pragma: no cover
394 import errno
396 if e.errno != errno.EPIPE:
397 raise
399 def __getattr__(self, attr: str) -> Any: 1cdabefg
400 return getattr(self.wrapped, attr) 1cdabefg
403def _detect_program_name( 1cdabefg
404 path: str | None = None, _main: ModuleType | None = None
405) -> str:
406 """Determine the command used to run the program, for use in help
407 text. If a file or entry point was executed, the file name is
408 returned. If ``python -m`` was used to execute a module or package,
409 ``python -m name`` is returned.
411 This doesn't try to be too precise, the goal is to give a concise
412 name for help text. Files are only shown as their name without the
413 path. ``python`` is only shown for modules, and the full path to
414 ``sys.executable`` is not shown.
415 """
416 if _main is None: 1cdabefg
417 _main = sys.modules["__main__"] 1cdabefg
419 if not path: 1cdabefg
420 path = sys.argv[0] 1cdabefg
422 # The value of __package__ indicates how Python was called. It may
423 # not exist if a setuptools script is installed as an egg. It may be
424 # set incorrectly for entry points created with pip on Windows.
425 # It is set to "" inside a Shiv or PEX zipapp.
426 if getattr(_main, "__package__", None) in {None, ""} or ( 1cdabefg
427 os.name == "nt"
428 and _main.__package__ == ""
429 and not os.path.exists(path)
430 and os.path.exists(f"{path}.exe")
431 ):
432 # Executed a file, like "python app.py".
433 return os.path.basename(path) 1cdabefg
435 # Executed a module, like "python -m example".
436 # Rewritten by Python from "-m script" to "/path/to/script.py".
437 # Need to look at main module to determine how it was executed.
438 py_module = cast(str, _main.__package__) 1cdabefg
439 name = os.path.splitext(os.path.basename(path))[0] 1cdabefg
441 # A submodule like "example.cli".
442 if name != "__main__": 1cdabefg
443 py_module = f"{py_module}.{name}" 1cdabefg
445 return f"python -m {py_module.lstrip('.')}" 1cdabefg
448def _expand_args( 1cdabefg
449 args: Iterable[str],
450 *,
451 user: bool = True,
452 env: bool = True,
453 glob_recursive: bool = True,
454) -> list[str]:
455 """Simulate Unix shell expansion with Python functions."""
456 from glob import glob 1ab
458 out = [] 1ab
460 for arg in args: 1ab
461 if user: 1ab
462 arg = os.path.expanduser(arg) 1ab
464 if env: 1ab
465 arg = os.path.expandvars(arg) 1ab
467 try: 1ab
468 matches = glob(arg, recursive=glob_recursive) 1ab
469 except re.error: # pragma: no cover
470 matches = []
472 if not matches: 1ab
473 out.append(arg) 1ab
474 else:
475 out.extend(matches) 1ab
477 return out 1ab