Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
testing.py670 linesDownload Raw Back to click
1from __future__ import annotations2 3import collections.abc as cabc4import contextlib5import io6import os7import pdb8import shlex9import sys10import tempfile11import typing as t12from types import TracebackType13 14from . import _compat15from . import formatting16from . import termui17from . import utils18from ._compat import _find_binary_reader19 20if t.TYPE_CHECKING:21    from _typeshed import ReadableBuffer22 23    from .core import Command24 25 26class EchoingStdin:27    def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None:28        self._input = input29        self._output = output30        self._paused = False31 32    def __getattr__(self, x: str) -> t.Any:33        return getattr(self._input, x)34 35    def _echo(self, rv: bytes) -> bytes:36        if not self._paused:37            self._output.write(rv)38 39        return rv40 41    def read(self, n: int = -1) -> bytes:42        return self._echo(self._input.read(n))43 44    def read1(self, n: int = -1) -> bytes:45        return self._echo(self._input.read1(n))  # type: ignore46 47    def readline(self, n: int = -1) -> bytes:48        return self._echo(self._input.readline(n))49 50    def readlines(self) -> list[bytes]:51        return [self._echo(x) for x in self._input.readlines()]52 53    def __iter__(self) -> cabc.Iterator[bytes]:54        return iter(self._echo(x) for x in self._input)55 56    def __repr__(self) -> str:57        return repr(self._input)58 59 60@contextlib.contextmanager61def _pause_echo(stream: EchoingStdin | None) -> cabc.Iterator[None]:62    if stream is None:63        yield64    else:65        stream._paused = True66        yield67        stream._paused = False68 69 70class BytesIOCopy(io.BytesIO):71    """Patch ``io.BytesIO`` to let the written stream be copied to another.72 73    .. versionadded:: 8.274    """75 76    def __init__(self, copy_to: io.BytesIO) -> None:77        super().__init__()78        self.copy_to = copy_to79 80    def flush(self) -> None:81        super().flush()82        self.copy_to.flush()83 84    def write(self, b: ReadableBuffer) -> int:85        self.copy_to.write(b)86        return super().write(b)87 88 89class StreamMixer:90    """Mixes `<stdout>` and `<stderr>` streams.91 92    The result is available in the ``output`` attribute.93 94    .. versionadded:: 8.295    """96 97    def __init__(self) -> None:98        self.output: io.BytesIO = io.BytesIO()99        self.stdout: io.BytesIO = BytesIOCopy(copy_to=self.output)100        self.stderr: io.BytesIO = BytesIOCopy(copy_to=self.output)101 102 103class _NamedTextIOWrapper(io.TextIOWrapper):104    """A :class:`~io.TextIOWrapper` with custom ``name`` and ``mode``105    that does not close its underlying buffer.106 107    An optional ``original_fd`` preserves the file descriptor of the108    stream being replaced, so that C-level consumers that call109    :meth:`fileno` (``faulthandler``, ``subprocess``, ...) still work.110    Inspired by pytest's ``capsys``/``capfd`` split: see :doc:`/testing`111    for details.112 113    .. versionchanged:: 8.3.3114        Added ``original_fd`` parameter and :meth:`fileno` override.115    """116 117    def __init__(118        self,119        buffer: t.BinaryIO,120        name: str,121        mode: str,122        *,123        original_fd: int = -1,124        **kwargs: t.Any,125    ) -> None:126        super().__init__(buffer, **kwargs)127        self._name = name128        self._mode = mode129        self._original_fd = original_fd130 131    def close(self) -> None:132        """The buffer this object contains belongs to some other object,133        so prevent the default ``__del__`` implementation from closing134        that buffer.135 136        .. versionadded:: 8.3.2137        """138 139    def fileno(self) -> int:140        """Return the file descriptor of the original stream, if one was141        provided at construction time.142 143        This allows C-level consumers (``faulthandler``, ``subprocess``,144        signal handlers, ...) to obtain a valid fd without crashing, even145        though the Python-level writes are redirected to an in-memory146        buffer.147 148        .. versionadded:: 8.3.3149        """150        if self._original_fd >= 0:151            return self._original_fd152        return super().fileno()153 154    @property155    def name(self) -> str:156        return self._name157 158    @property159    def mode(self) -> str:160        return self._mode161 162 163def make_input_stream(164    input: str | bytes | t.IO[t.Any] | None, charset: str165) -> t.BinaryIO:166    # Is already an input stream.167    if hasattr(input, "read"):168        rv = _find_binary_reader(t.cast("t.IO[t.Any]", input))169 170        if rv is not None:171            return rv172 173        raise TypeError("Could not find binary reader for input stream.")174 175    if input is None:176        input = b""177    elif isinstance(input, str):178        input = input.encode(charset)179 180    return io.BytesIO(input)181 182 183class Result:184    """Holds the captured result of an invoked CLI script.185 186    :param runner: The runner that created the result187    :param stdout_bytes: The standard output as bytes.188    :param stderr_bytes: The standard error as bytes.189    :param output_bytes: A mix of ``stdout_bytes`` and ``stderr_bytes``, as the190        user would see  it in its terminal.191    :param return_value: The value returned from the invoked command.192    :param exit_code: The exit code as integer.193    :param exception: The exception that happened if one did.194    :param exc_info: Exception information (exception type, exception instance,195        traceback type).196 197    .. versionchanged:: 8.2198        ``stderr_bytes`` no longer optional, ``output_bytes`` introduced and199        ``mix_stderr`` has been removed.200 201    .. versionadded:: 8.0202        Added ``return_value``.203    """204 205    def __init__(206        self,207        runner: CliRunner,208        stdout_bytes: bytes,209        stderr_bytes: bytes,210        output_bytes: bytes,211        return_value: t.Any,212        exit_code: int,213        exception: BaseException | None,214        exc_info: tuple[type[BaseException], BaseException, TracebackType]215        | None = None,216    ):217        self.runner = runner218        self.stdout_bytes = stdout_bytes219        self.stderr_bytes = stderr_bytes220        self.output_bytes = output_bytes221        self.return_value = return_value222        self.exit_code = exit_code223        self.exception = exception224        self.exc_info = exc_info225 226    @property227    def output(self) -> str:228        """The terminal output as unicode string, as the user would see it.229 230        .. versionchanged:: 8.2231            No longer a proxy for ``self.stdout``. Now has its own independent stream232            that is mixing `<stdout>` and `<stderr>`, in the order they were written.233        """234        return self.output_bytes.decode(self.runner.charset, "replace").replace(235            "\r\n", "\n"236        )237 238    @property239    def stdout(self) -> str:240        """The standard output as unicode string."""241        return self.stdout_bytes.decode(self.runner.charset, "replace").replace(242            "\r\n", "\n"243        )244 245    @property246    def stderr(self) -> str:247        """The standard error as unicode string.248 249        .. versionchanged:: 8.2250            No longer raise an exception, always returns the `<stderr>` string.251        """252        return self.stderr_bytes.decode(self.runner.charset, "replace").replace(253            "\r\n", "\n"254        )255 256    def __repr__(self) -> str:257        exc_str = repr(self.exception) if self.exception else "okay"258        return f"<{type(self).__name__} {exc_str}>"259 260 261class CliRunner:262    """The CLI runner provides functionality to invoke a Click command line263    script for unittesting purposes in a isolated environment.  This only264    works in single-threaded systems without any concurrency as it changes the265    global interpreter state.266 267    :param charset: the character set for the input and output data.268    :param env: a dictionary with environment variables for overriding.269    :param echo_stdin: if this is set to `True`, then reading from `<stdin>` writes270                       to `<stdout>`.  This is useful for showing examples in271                       some circumstances.  Note that regular prompts272                       will automatically echo the input.273    :param catch_exceptions: Whether to catch any exceptions other than274                             ``SystemExit`` when running :meth:`~CliRunner.invoke`.275 276    .. versionchanged:: 8.2277        Added the ``catch_exceptions`` parameter.278 279    .. versionchanged:: 8.2280        ``mix_stderr`` parameter has been removed.281    """282 283    def __init__(284        self,285        charset: str = "utf-8",286        env: cabc.Mapping[str, str | None] | None = None,287        echo_stdin: bool = False,288        catch_exceptions: bool = True,289    ) -> None:290        self.charset = charset291        self.env: cabc.Mapping[str, str | None] = env or {}292        self.echo_stdin = echo_stdin293        self.catch_exceptions = catch_exceptions294 295    def get_default_prog_name(self, cli: Command) -> str:296        """Given a command object it will return the default program name297        for it.  The default is the `name` attribute or ``"root"`` if not298        set.299        """300        return cli.name or "root"301 302    def make_env(303        self, overrides: cabc.Mapping[str, str | None] | None = None304    ) -> cabc.Mapping[str, str | None]:305        """Returns the environment overrides for invoking a script."""306        rv = dict(self.env)307        if overrides:308            rv.update(overrides)309        return rv310 311    @contextlib.contextmanager312    def isolation(313        self,314        input: str | bytes | t.IO[t.Any] | None = None,315        env: cabc.Mapping[str, str | None] | None = None,316        color: bool = False,317    ) -> cabc.Iterator[tuple[io.BytesIO, io.BytesIO, io.BytesIO]]:318        """A context manager that sets up the isolation for invoking of a319        command line tool.  This sets up `<stdin>` with the given input data320        and `os.environ` with the overrides from the given dictionary.321        This also rebinds some internals in Click to be mocked (like the322        prompt functionality).323 324        This is automatically done in the :meth:`invoke` method.325 326        :param input: the input stream to put into `sys.stdin`.327        :param env: the environment overrides as dictionary.328        :param color: whether the output should contain color codes. The329                      application can still override this explicitly.330 331        .. versionadded:: 8.2332            An additional output stream is returned, which is a mix of333            `<stdout>` and `<stderr>` streams.334 335        .. versionchanged:: 8.2336            Always returns the `<stderr>` stream.337 338        .. versionchanged:: 8.0339            `<stderr>` is opened with ``errors="backslashreplace"``340            instead of the default ``"strict"``.341 342        .. versionchanged:: 4.0343            Added the ``color`` parameter.344        """345        bytes_input = make_input_stream(input, self.charset)346        echo_input = None347 348        old_stdin = sys.stdin349        old_stdout = sys.stdout350        old_stderr = sys.stderr351        old_forced_width = formatting.FORCED_WIDTH352        formatting.FORCED_WIDTH = 80353 354        env = self.make_env(env)355 356        stream_mixer = StreamMixer()357 358        # Preserve the original file descriptors so that C-level359        # consumers (faulthandler, subprocess, etc.) can still obtain a360        # valid fd from the redirected streams. The original streams361        # may themselves lack a fileno() (e.g. when CliRunner is used362        # inside pytest's capsys), so we fall back to -1.363        def _safe_fileno(stream: t.IO[t.Any]) -> int:364            try:365                return stream.fileno()366            except (AttributeError, io.UnsupportedOperation):367                return -1368 369        old_stdout_fd = _safe_fileno(old_stdout)370        old_stderr_fd = _safe_fileno(old_stderr)371 372        if self.echo_stdin:373            bytes_input = echo_input = t.cast(374                t.BinaryIO, EchoingStdin(bytes_input, stream_mixer.stdout)375            )376 377        sys.stdin = text_input = _NamedTextIOWrapper(378            bytes_input, encoding=self.charset, name="<stdin>", mode="r"379        )380 381        if self.echo_stdin:382            # Force unbuffered reads, otherwise TextIOWrapper reads a383            # large chunk which is echoed early.384            text_input._CHUNK_SIZE = 1  # type: ignore385 386        sys.stdout = _NamedTextIOWrapper(387            stream_mixer.stdout,388            encoding=self.charset,389            name="<stdout>",390            mode="w",391            original_fd=old_stdout_fd,392        )393 394        sys.stderr = _NamedTextIOWrapper(395            stream_mixer.stderr,396            encoding=self.charset,397            name="<stderr>",398            mode="w",399            errors="backslashreplace",400            original_fd=old_stderr_fd,401        )402 403        @_pause_echo(echo_input)  # type: ignore404        def visible_input(prompt: str | None = None) -> str:405            sys.stdout.write(prompt or "")406            try:407                val = next(text_input).rstrip("\r\n")408            except StopIteration as e:409                raise EOFError() from e410            sys.stdout.write(f"{val}\n")411            sys.stdout.flush()412            return val413 414        @_pause_echo(echo_input)  # type: ignore415        def hidden_input(prompt: str | None = None) -> str:416            sys.stdout.write(f"{prompt or ''}\n")417            sys.stdout.flush()418            try:419                return next(text_input).rstrip("\r\n")420            except StopIteration as e:421                raise EOFError() from e422 423        @_pause_echo(echo_input)  # type: ignore424        def _getchar(echo: bool) -> str:425            char = sys.stdin.read(1)426 427            if echo:428                sys.stdout.write(char)429 430            sys.stdout.flush()431            return char432 433        default_color = color434 435        def should_strip_ansi(436            stream: t.IO[t.Any] | None = None, color: bool | None = None437        ) -> bool:438            if color is None:439                return not default_color440            return not color441 442        old_visible_prompt_func = termui.visible_prompt_func443        old_hidden_prompt_func = termui.hidden_prompt_func444        old__getchar_func = termui._getchar445        old_should_strip_ansi = utils.should_strip_ansi  # type: ignore446        old__compat_should_strip_ansi = _compat.should_strip_ansi447        old_pdb_init = pdb.Pdb.__init__448        termui.visible_prompt_func = visible_input449        termui.hidden_prompt_func = hidden_input450        termui._getchar = _getchar451        utils.should_strip_ansi = should_strip_ansi  # type: ignore452        _compat.should_strip_ansi = should_strip_ansi453 454        def _patched_pdb_init(455            self: pdb.Pdb,456            completekey: str = "tab",457            stdin: t.IO[str] | None = None,458            stdout: t.IO[str] | None = None,459            **kwargs: t.Any,460        ) -> None:461            """Default ``pdb.Pdb`` to real terminal streams during462            ``CliRunner`` isolation.463 464            Without this patch, ``pdb.Pdb.__init__`` inherits from465            ``cmd.Cmd`` which falls back to ``sys.stdin``/``sys.stdout``466            when no explicit streams are provided. During isolation467            those are ``BytesIO``-backed wrappers, so the debugger468            reads from an empty buffer and writes to captured output,469            making interactive debugging impossible.470 471            By defaulting to ``sys.__stdin__``/``sys.__stdout__`` (the472            original terminal streams Python preserves regardless of473            redirection), debuggers can interact with the user while474            ``click.echo`` output is still captured normally.475 476            This covers ``pdb.set_trace()``, ``breakpoint()``,477            ``pdb.post_mortem()``, and debuggers that subclass478            ``pdb.Pdb`` (ipdb, pdbpp). Explicit ``stdin``/``stdout``479            arguments are honored and not overridden. Debuggers that480            do not subclass ``pdb.Pdb`` (pudb, debugpy) are not481            covered.482            """483            if stdin is None:484                stdin = sys.__stdin__485            if stdout is None:486                stdout = sys.__stdout__487            old_pdb_init(488                self, completekey=completekey, stdin=stdin, stdout=stdout, **kwargs489            )490 491        pdb.Pdb.__init__ = _patched_pdb_init  # type: ignore[assignment]492 493        old_env = {}494        try:495            for key, value in env.items():496                old_env[key] = os.environ.get(key)497                if value is None:498                    try:499                        del os.environ[key]500                    except Exception:501                        pass502                else:503                    os.environ[key] = value504            yield (stream_mixer.stdout, stream_mixer.stderr, stream_mixer.output)505        finally:506            for key, value in old_env.items():507                if value is None:508                    try:509                        del os.environ[key]510                    except Exception:511                        pass512                else:513                    os.environ[key] = value514            sys.stdout = old_stdout515            sys.stderr = old_stderr516            sys.stdin = old_stdin517            termui.visible_prompt_func = old_visible_prompt_func518            termui.hidden_prompt_func = old_hidden_prompt_func519            termui._getchar = old__getchar_func520            utils.should_strip_ansi = old_should_strip_ansi  # type: ignore521            _compat.should_strip_ansi = old__compat_should_strip_ansi522            formatting.FORCED_WIDTH = old_forced_width523            pdb.Pdb.__init__ = old_pdb_init  # type: ignore[method-assign]524 525    def invoke(526        self,527        cli: Command,528        args: str | cabc.Sequence[str] | None = None,529        input: str | bytes | t.IO[t.Any] | None = None,530        env: cabc.Mapping[str, str | None] | None = None,531        catch_exceptions: bool | None = None,532        color: bool = False,533        **extra: t.Any,534    ) -> Result:535        """Invokes a command in an isolated environment.  The arguments are536        forwarded directly to the command line script, the `extra` keyword537        arguments are passed to the :meth:`~clickpkg.Command.main` function of538        the command.539 540        This returns a :class:`Result` object.541 542        :param cli: the command to invoke543        :param args: the arguments to invoke. It may be given as an iterable544                     or a string. When given as string it will be interpreted545                     as a Unix shell command. More details at546                     :func:`shlex.split`.547        :param input: the input data for `sys.stdin`.548        :param env: the environment overrides.549        :param catch_exceptions: Whether to catch any other exceptions than550                                 ``SystemExit``. If :data:`None`, the value551                                 from :class:`CliRunner` is used.552        :param extra: the keyword arguments to pass to :meth:`main`.553        :param color: whether the output should contain color codes. The554                      application can still override this explicitly.555 556        .. versionadded:: 8.2557            The result object has the ``output_bytes`` attribute with558            the mix of ``stdout_bytes`` and ``stderr_bytes``, as the user would559            see it in its terminal.560 561        .. versionchanged:: 8.2562            The result object always returns the ``stderr_bytes`` stream.563 564        .. versionchanged:: 8.0565            The result object has the ``return_value`` attribute with566            the value returned from the invoked command.567 568        .. versionchanged:: 4.0569            Added the ``color`` parameter.570 571        .. versionchanged:: 3.0572            Added the ``catch_exceptions`` parameter.573 574        .. versionchanged:: 3.0575            The result object has the ``exc_info`` attribute with the576            traceback if available.577        """578        exc_info = None579        if catch_exceptions is None:580            catch_exceptions = self.catch_exceptions581 582        with self.isolation(input=input, env=env, color=color) as outstreams:583            return_value = None584            exception: BaseException | None = None585            exit_code = 0586 587            if isinstance(args, str):588                args = shlex.split(args)589 590            try:591                prog_name = extra.pop("prog_name")592            except KeyError:593                prog_name = self.get_default_prog_name(cli)594 595            try:596                return_value = cli.main(args=args or (), prog_name=prog_name, **extra)597            except SystemExit as e:598                exc_info = sys.exc_info()599                e_code = t.cast("int | t.Any | None", e.code)600 601                if e_code is None:602                    e_code = 0603 604                if e_code != 0:605                    exception = e606 607                if not isinstance(e_code, int):608                    sys.stdout.write(str(e_code))609                    sys.stdout.write("\n")610                    e_code = 1611 612                exit_code = e_code613 614            except Exception as e:615                if not catch_exceptions:616                    raise617                exception = e618                exit_code = 1619                exc_info = sys.exc_info()620            finally:621                sys.stdout.flush()622                sys.stderr.flush()623                stdout = outstreams[0].getvalue()624                stderr = outstreams[1].getvalue()625                output = outstreams[2].getvalue()626 627        return Result(628            runner=self,629            stdout_bytes=stdout,630            stderr_bytes=stderr,631            output_bytes=output,632            return_value=return_value,633            exit_code=exit_code,634            exception=exception,635            exc_info=exc_info,  # type: ignore636        )637 638    @contextlib.contextmanager639    def isolated_filesystem(640        self, temp_dir: str | os.PathLike[str] | None = None641    ) -> cabc.Iterator[str]:642        """A context manager that creates a temporary directory and643        changes the current working directory to it. This isolates tests644        that affect the contents of the CWD to prevent them from645        interfering with each other.646 647        :param temp_dir: Create the temporary directory under this648            directory. If given, the created directory is not removed649            when exiting.650 651        .. versionchanged:: 8.0652            Added the ``temp_dir`` parameter.653        """654        cwd = os.getcwd()655        dt = tempfile.mkdtemp(dir=temp_dir)656        os.chdir(dt)657 658        try:659            yield dt660        finally:661            os.chdir(cwd)662 663            if temp_dir is None:664                import shutil665 666                try:667                    shutil.rmtree(dt)668                except OSError:669                    pass670 
codekingpro/portable-devtools · Team Ai