codekingpro/portable-devtools
115k
1from __future__ import annotations2 3import collections.abc as cabc4import os5import re6import sys7import typing as t8from functools import update_wrapper9from types import ModuleType10from types import TracebackType11 12from ._compat import _default_text_stderr13from ._compat import _default_text_stdout14from ._compat import _find_binary_writer15from ._compat import auto_wrap_for_ansi16from ._compat import binary_streams17from ._compat import open_stream18from ._compat import should_strip_ansi19from ._compat import strip_ansi20from ._compat import text_streams21from ._compat import WIN22from .globals import resolve_color_default23 24if t.TYPE_CHECKING:25 import typing_extensions as te26 27 P = te.ParamSpec("P")28 29R = t.TypeVar("R")30 31 32def _posixify(name: str) -> str:33 return "-".join(name.split()).lower()34 35 36def safecall(func: t.Callable[P, R]) -> t.Callable[P, R | None]:37 """Wraps a function so that it swallows exceptions."""38 39 def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None:40 try:41 return func(*args, **kwargs)42 except Exception:43 pass44 return None45 46 return update_wrapper(wrapper, func)47 48 49def make_str(value: t.Any) -> str:50 """Converts a value into a valid string."""51 if isinstance(value, bytes):52 try:53 return value.decode(sys.getfilesystemencoding())54 except UnicodeError:55 return value.decode("utf-8", "replace")56 return str(value)57 58 59def make_default_short_help(help: str, max_length: int = 45) -> str:60 """Returns a condensed version of help string.61 62 :meta private:63 """64 # Consider only the first paragraph.65 paragraph_end = help.find("\n\n")66 67 if paragraph_end != -1:68 help = help[:paragraph_end]69 70 # Collapse newlines, tabs, and spaces.71 words = help.split()72 73 if not words:74 return ""75 76 # The first paragraph started with a "no rewrap" marker, ignore it.77 if words[0] == "\b":78 words = words[1:]79 80 total_length = 081 last_index = len(words) - 182 83 for i, word in enumerate(words):84 total_length += len(word) + (i > 0)85 86 if total_length > max_length: # too long, truncate87 break88 89 if word[-1] == ".": # sentence end, truncate without "..."90 return " ".join(words[: i + 1])91 92 if total_length == max_length and i != last_index:93 break # not at sentence end, truncate with "..."94 else:95 return " ".join(words) # no truncation needed96 97 # Account for the length of the suffix.98 total_length += len("...")99 100 # remove words until the length is short enough101 while i > 0:102 total_length -= len(words[i]) + (i > 0)103 104 if total_length <= max_length:105 break106 107 i -= 1108 109 return " ".join(words[:i]) + "..."110 111 112class LazyFile:113 """A lazy file works like a regular file but it does not fully open114 the file but it does perform some basic checks early to see if the115 filename parameter does make sense. This is useful for safely opening116 files for writing.117 """118 119 def __init__(120 self,121 filename: str | os.PathLike[str],122 mode: str = "r",123 encoding: str | None = None,124 errors: str | None = "strict",125 atomic: bool = False,126 ):127 self.name: str = os.fspath(filename)128 self.mode = mode129 self.encoding = encoding130 self.errors = errors131 self.atomic = atomic132 self._f: t.IO[t.Any] | None133 self.should_close: bool134 135 if self.name == "-":136 self._f, self.should_close = open_stream(filename, mode, encoding, errors)137 else:138 if "r" in mode:139 # Open and close the file in case we're opening it for140 # reading so that we can catch at least some errors in141 # some cases early.142 open(filename, mode).close()143 self._f = None144 self.should_close = True145 146 def __getattr__(self, name: str) -> t.Any:147 return getattr(self.open(), name)148 149 def __repr__(self) -> str:150 if self._f is not None:151 return repr(self._f)152 return f"<unopened file '{format_filename(self.name)}' {self.mode}>"153 154 def open(self) -> t.IO[t.Any]:155 """Opens the file if it's not yet open. This call might fail with156 a :exc:`FileError`. Not handling this error will produce an error157 that Click shows.158 """159 if self._f is not None:160 return self._f161 try:162 rv, self.should_close = open_stream(163 self.name, self.mode, self.encoding, self.errors, atomic=self.atomic164 )165 except OSError as e:166 from .exceptions import FileError167 168 raise FileError(self.name, hint=e.strerror) from e169 self._f = rv170 return rv171 172 def close(self) -> None:173 """Closes the underlying file, no matter what."""174 if self._f is not None:175 self._f.close()176 177 def close_intelligently(self) -> None:178 """This function only closes the file if it was opened by the lazy179 file wrapper. For instance this will never close stdin.180 """181 if self.should_close:182 self.close()183 184 def __enter__(self) -> LazyFile:185 return self186 187 def __exit__(188 self,189 exc_type: type[BaseException] | None,190 exc_value: BaseException | None,191 tb: TracebackType | None,192 ) -> None:193 self.close_intelligently()194 195 def __iter__(self) -> cabc.Iterator[t.AnyStr]:196 self.open()197 return iter(self._f) # type: ignore198 199 200class KeepOpenFile:201 def __init__(self, file: t.IO[t.Any]) -> None:202 self._file: t.IO[t.Any] = file203 204 def __getattr__(self, name: str) -> t.Any:205 return getattr(self._file, name)206 207 def __enter__(self) -> KeepOpenFile:208 return self209 210 def __exit__(211 self,212 exc_type: type[BaseException] | None,213 exc_value: BaseException | None,214 tb: TracebackType | None,215 ) -> None:216 pass217 218 def __repr__(self) -> str:219 return repr(self._file)220 221 def __iter__(self) -> cabc.Iterator[t.AnyStr]:222 return iter(self._file)223 224 225def echo(226 message: t.Any | None = None,227 file: t.IO[t.Any] | None = None,228 nl: bool = True,229 err: bool = False,230 color: bool | None = None,231) -> None:232 """Print a message and newline to stdout or a file. This should be233 used instead of :func:`print` because it provides better support234 for different data, files, and environments.235 236 Compared to :func:`print`, this does the following:237 238 - Ensures that the output encoding is not misconfigured on Linux.239 - Supports Unicode in the Windows console.240 - Supports writing to binary outputs, and supports writing bytes241 to text outputs.242 - Supports colors and styles on Windows.243 - Removes ANSI color and style codes if the output does not look244 like an interactive terminal.245 - Always flushes the output.246 247 :param message: The string or bytes to output. Other objects are248 converted to strings.249 :param file: The file to write to. Defaults to ``stdout``.250 :param err: Write to ``stderr`` instead of ``stdout``.251 :param nl: Print a newline after the message. Enabled by default.252 :param color: Force showing or hiding colors and other styles. By253 default Click will remove color if the output does not look like254 an interactive terminal.255 256 .. versionchanged:: 6.0257 Support Unicode output on the Windows console. Click does not258 modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()``259 will still not support Unicode.260 261 .. versionchanged:: 4.0262 Added the ``color`` parameter.263 264 .. versionadded:: 3.0265 Added the ``err`` parameter.266 267 .. versionchanged:: 2.0268 Support colors on Windows if colorama is installed.269 """270 if file is None:271 if err:272 file = _default_text_stderr()273 else:274 file = _default_text_stdout()275 276 # There are no standard streams attached to write to. For example,277 # pythonw on Windows.278 if file is None:279 return280 281 # Convert non bytes/text into the native string type.282 if message is not None and not isinstance(message, (str, bytes, bytearray)):283 out: str | bytes | bytearray | None = str(message)284 else:285 out = message286 287 if nl:288 out = out or ""289 if isinstance(out, str):290 out += "\n"291 else:292 out += b"\n"293 294 if not out:295 file.flush()296 return297 298 # If there is a message and the value looks like bytes, we manually299 # need to find the binary stream and write the message in there.300 # This is done separately so that most stream types will work as you301 # would expect. Eg: you can write to StringIO for other cases.302 if isinstance(out, (bytes, bytearray)):303 binary_file = _find_binary_writer(file)304 305 if binary_file is not None:306 file.flush()307 binary_file.write(out)308 binary_file.flush()309 return310 311 # ANSI style code support. For no message or bytes, nothing happens.312 # When outputting to a file instead of a terminal, strip codes.313 else:314 color = resolve_color_default(color)315 316 if should_strip_ansi(file, color):317 out = strip_ansi(out)318 elif WIN:319 if auto_wrap_for_ansi is not None:320 file = auto_wrap_for_ansi(file, color) # type: ignore321 elif not color:322 out = strip_ansi(out)323 324 file.write(out) # type: ignore325 file.flush()326 327 328def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryIO:329 """Returns a system stream for byte processing.330 331 :param name: the name of the stream to open. Valid names are ``'stdin'``,332 ``'stdout'`` and ``'stderr'``333 """334 opener = binary_streams.get(name)335 if opener is None:336 raise TypeError(f"Unknown standard stream '{name}'")337 return opener()338 339 340def get_text_stream(341 name: t.Literal["stdin", "stdout", "stderr"],342 encoding: str | None = None,343 errors: str | None = "strict",344) -> t.TextIO:345 """Returns a system stream for text processing. This usually returns346 a wrapped stream around a binary stream returned from347 :func:`get_binary_stream` but it also can take shortcuts for already348 correctly configured streams.349 350 :param name: the name of the stream to open. Valid names are ``'stdin'``,351 ``'stdout'`` and ``'stderr'``352 :param encoding: overrides the detected default encoding.353 :param errors: overrides the default error mode.354 """355 opener = text_streams.get(name)356 if opener is None:357 raise TypeError(f"Unknown standard stream '{name}'")358 return opener(encoding, errors)359 360 361def open_file(362 filename: str | os.PathLike[str],363 mode: str = "r",364 encoding: str | None = None,365 errors: str | None = "strict",366 lazy: bool = False,367 atomic: bool = False,368) -> t.IO[t.Any]:369 """Open a file, with extra behavior to handle ``'-'`` to indicate370 a standard stream, lazy open on write, and atomic write. Similar to371 the behavior of the :class:`~click.File` param type.372 373 If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is374 wrapped so that using it in a context manager will not close it.375 This makes it possible to use the function without accidentally376 closing a standard stream:377 378 .. code-block:: python379 380 with open_file(filename) as f:381 ...382 383 :param filename: The name or Path of the file to open, or ``'-'`` for384 ``stdin``/``stdout``.385 :param mode: The mode in which to open the file.386 :param encoding: The encoding to decode or encode a file opened in387 text mode.388 :param errors: The error handling mode.389 :param lazy: Wait to open the file until it is accessed. For read390 mode, the file is temporarily opened to raise access errors391 early, then closed until it is read again.392 :param atomic: Write to a temporary file and replace the given file393 on close.394 395 .. versionadded:: 3.0396 """397 if lazy:398 return t.cast(399 "t.IO[t.Any]", LazyFile(filename, mode, encoding, errors, atomic=atomic)400 )401 402 f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)403 404 if not should_close:405 f = t.cast("t.IO[t.Any]", KeepOpenFile(f))406 407 return f408 409 410def format_filename(411 filename: str | bytes | os.PathLike[str] | os.PathLike[bytes],412 shorten: bool = False,413) -> str:414 """Format a filename as a string for display. Ensures the filename can be415 displayed by replacing any invalid bytes or surrogate escapes in the name416 with the replacement character ``�``.417 418 Invalid bytes or surrogate escapes will raise an error when written to a419 stream with ``errors="strict"``. This will typically happen with ``stdout``420 when the locale is something like ``en_GB.UTF-8``.421 422 Many scenarios *are* safe to write surrogates though, due to PEP 538 and423 PEP 540, including:424 425 - Writing to ``stderr``, which uses ``errors="backslashreplace"``.426 - The system has ``LANG=C.UTF-8``, ``C``, or ``POSIX``. Python opens427 stdout and stderr with ``errors="surrogateescape"``.428 - None of ``LANG/LC_*`` are set. Python assumes ``LANG=C.UTF-8``.429 - Python is started in UTF-8 mode with ``PYTHONUTF8=1`` or ``-X utf8``.430 Python opens stdout and stderr with ``errors="surrogateescape"``.431 432 :param filename: formats a filename for UI display. This will also convert433 the filename into unicode without failing.434 :param shorten: this optionally shortens the filename to strip of the435 path that leads up to it.436 """437 if shorten:438 filename = os.path.basename(filename)439 else:440 filename = os.fspath(filename)441 442 if isinstance(filename, bytes):443 filename = filename.decode(sys.getfilesystemencoding(), "replace")444 else:445 filename = filename.encode("utf-8", "surrogateescape").decode(446 "utf-8", "replace"447 )448 449 return filename450 451 452def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str:453 r"""Returns the config folder for the application. The default behavior454 is to return whatever is most appropriate for the operating system.455 456 To give you an idea, for an app called ``"Foo Bar"``, something like457 the following folders could be returned:458 459 Mac OS X:460 ``~/Library/Application Support/Foo Bar``461 Mac OS X (POSIX):462 ``~/.foo-bar``463 Unix:464 ``~/.config/foo-bar``465 Unix (POSIX):466 ``~/.foo-bar``467 Windows (roaming):468 ``C:\Users\<user>\AppData\Roaming\Foo Bar``469 Windows (not roaming):470 ``C:\Users\<user>\AppData\Local\Foo Bar``471 472 .. versionadded:: 2.0473 474 :param app_name: the application name. This should be properly capitalized475 and can contain whitespace.476 :param roaming: controls if the folder should be roaming or not on Windows.477 Has no effect otherwise.478 :param force_posix: if this is set to `True` then on any POSIX system the479 folder will be stored in the home folder with a leading480 dot instead of the XDG config home or darwin's481 application support folder.482 """483 if WIN:484 key = "APPDATA" if roaming else "LOCALAPPDATA"485 folder = os.environ.get(key)486 if folder is None:487 folder = os.path.expanduser("~")488 return os.path.join(folder, app_name)489 if force_posix:490 return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}"))491 if sys.platform == "darwin":492 return os.path.join(493 os.path.expanduser("~/Library/Application Support"), app_name494 )495 return os.path.join(496 os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")),497 _posixify(app_name),498 )499 500 501class PacifyFlushWrapper:502 """This wrapper is used to catch and suppress BrokenPipeErrors resulting503 from ``.flush()`` being called on broken pipe during the shutdown/final-GC504 of the Python interpreter. Notably ``.flush()`` is always called on505 ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any506 other cleanup code, and the case where the underlying file is not a broken507 pipe, all calls and attributes are proxied.508 """509 510 def __init__(self, wrapped: t.IO[t.Any]) -> None:511 self.wrapped = wrapped512 513 def flush(self) -> None:514 try:515 self.wrapped.flush()516 except OSError as e:517 import errno518 519 if e.errno != errno.EPIPE:520 raise521 522 def __getattr__(self, attr: str) -> t.Any:523 return getattr(self.wrapped, attr)524 525 526def _detect_program_name(527 path: str | None = None, _main: ModuleType | None = None528) -> str:529 """Determine the command used to run the program, for use in help530 text. If a file or entry point was executed, the file name is531 returned. If ``python -m`` was used to execute a module or package,532 ``python -m name`` is returned.533 534 This doesn't try to be too precise, the goal is to give a concise535 name for help text. Files are only shown as their name without the536 path. ``python`` is only shown for modules, and the full path to537 ``sys.executable`` is not shown.538 539 :param path: The Python file being executed. Python puts this in540 ``sys.argv[0]``, which is used by default.541 :param _main: The ``__main__`` module. This should only be passed542 during internal testing.543 544 .. versionadded:: 8.0545 Based on command args detection in the Werkzeug reloader.546 547 :meta private:548 """549 if _main is None:550 _main = sys.modules["__main__"]551 552 if not path:553 path = sys.argv[0]554 555 # The value of __package__ indicates how Python was called. It may556 # not exist if a setuptools script is installed as an egg. It may be557 # set incorrectly for entry points created with pip on Windows.558 # It is set to "" inside a Shiv or PEX zipapp.559 if getattr(_main, "__package__", None) in {None, ""} or (560 os.name == "nt"561 and _main.__package__ == ""562 and not os.path.exists(path)563 and os.path.exists(f"{path}.exe")564 ):565 # Executed a file, like "python app.py".566 return os.path.basename(path)567 568 # Executed a module, like "python -m example".569 # Rewritten by Python from "-m script" to "/path/to/script.py".570 # Need to look at main module to determine how it was executed.571 py_module = t.cast(str, _main.__package__)572 name = os.path.splitext(os.path.basename(path))[0]573 574 # A submodule like "example.cli".575 if name != "__main__":576 py_module = f"{py_module}.{name}"577 578 return f"python -m {py_module.lstrip('.')}"579 580 581def _expand_args(582 args: cabc.Iterable[str],583 *,584 user: bool = True,585 env: bool = True,586 glob_recursive: bool = True,587) -> list[str]:588 """Simulate Unix shell expansion with Python functions.589 590 See :func:`glob.glob`, :func:`os.path.expanduser`, and591 :func:`os.path.expandvars`.592 593 This is intended for use on Windows, where the shell does not do any594 expansion. It may not exactly match what a Unix shell would do.595 596 :param args: List of command line arguments to expand.597 :param user: Expand user home directory.598 :param env: Expand environment variables.599 :param glob_recursive: ``**`` matches directories recursively.600 601 .. versionchanged:: 8.1602 Invalid glob patterns are treated as empty expansions rather603 than raising an error.604 605 .. versionadded:: 8.0606 607 :meta private:608 """609 from glob import glob610 611 out = []612 613 for arg in args:614 if user:615 arg = os.path.expanduser(arg)616 617 if env:618 arg = os.path.expandvars(arg)619 620 try:621 matches = glob(arg, recursive=glob_recursive)622 except re.error:623 matches = []624 625 if not matches:626 out.append(arg)627 else:628 out.extend(matches)629 630 return out631 