Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
utils.py631 linesDownload Raw Back to click
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 
codekingpro/portable-devtools · Team Ai