testing.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. from __future__ import annotations
  2. import collections.abc as cabc
  3. import contextlib
  4. import io
  5. import os
  6. import pdb
  7. import shlex
  8. import sys
  9. import tempfile
  10. import typing as t
  11. from types import TracebackType
  12. from . import _compat
  13. from . import formatting
  14. from . import termui
  15. from . import utils
  16. from ._compat import _find_binary_reader
  17. if t.TYPE_CHECKING:
  18. from _typeshed import ReadableBuffer
  19. from .core import Command
  20. if sys.platform == "win32":
  21. CaptureMode: t.TypeAlias = t.Literal["sys"] # pyright: ignore[reportRedeclaration]
  22. else:
  23. CaptureMode: t.TypeAlias = t.Literal["sys", "fd"] # pyright: ignore[reportRedeclaration]
  24. ExceptionInfo: t.TypeAlias = tuple[type[BaseException], BaseException, TracebackType]
  25. class EchoingStdin:
  26. _input: t.BinaryIO
  27. _output: t.BinaryIO
  28. _paused: bool
  29. def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None:
  30. self._input = input
  31. self._output = output
  32. self._paused = False
  33. def __getattr__(self, x: str) -> t.Any:
  34. return getattr(self._input, x)
  35. def _echo(self, rv: bytes) -> bytes:
  36. if not self._paused:
  37. self._output.write(rv)
  38. return rv
  39. def read(self, n: int = -1) -> bytes:
  40. return self._echo(self._input.read(n))
  41. def read1(self, n: int = -1) -> bytes:
  42. return self._echo(self._input.read1(n)) # type: ignore
  43. def readline(self, n: int = -1) -> bytes:
  44. return self._echo(self._input.readline(n))
  45. def readlines(self) -> list[bytes]:
  46. return [self._echo(x) for x in self._input.readlines()]
  47. def __iter__(self) -> cabc.Iterator[bytes]:
  48. return iter(self._echo(x) for x in self._input)
  49. def __repr__(self) -> str:
  50. return repr(self._input)
  51. @contextlib.contextmanager
  52. def _pause_echo(stream: EchoingStdin | None) -> cabc.Generator[None]:
  53. if stream is None:
  54. yield
  55. else:
  56. stream._paused = True
  57. yield
  58. stream._paused = False
  59. class _FDCapture:
  60. """Redirect a file descriptor to a temporary file for capture.
  61. Saves the current target of *targetfd* via :func:`os.dup`, then
  62. redirects it to a temporary file via :func:`os.dup2`. On
  63. :meth:`stop`, restores the original ``fd`` and returns the captured
  64. bytes. Inspired by Pytest's ``FDCapture``.
  65. .. versionadded:: 8.4.0
  66. """
  67. _targetfd: int
  68. saved_fd: int
  69. _tmpfile: t.BinaryIO | None
  70. def __init__(self, targetfd: int) -> None:
  71. self._targetfd = targetfd
  72. self.saved_fd = -1
  73. self._tmpfile = None
  74. def start(self) -> None:
  75. self.saved_fd = os.dup(self._targetfd)
  76. self._tmpfile = tempfile.TemporaryFile(buffering=0)
  77. os.dup2(self._tmpfile.fileno(), self._targetfd)
  78. def stop(self) -> bytes:
  79. assert self._tmpfile is not None, "_FDCapture.start() was not called"
  80. os.dup2(self.saved_fd, self._targetfd)
  81. os.close(self.saved_fd)
  82. self.saved_fd = -1
  83. self._tmpfile.seek(0)
  84. data = self._tmpfile.read()
  85. self._tmpfile.close()
  86. self._tmpfile = None
  87. return data
  88. class BytesIOCopy(io.BytesIO):
  89. """Patch ``io.BytesIO`` to let the written stream be copied to another.
  90. .. versionadded:: 8.2
  91. """
  92. copy_to: io.BytesIO
  93. def __init__(self, copy_to: io.BytesIO) -> None:
  94. super().__init__()
  95. self.copy_to = copy_to
  96. def flush(self) -> None:
  97. super().flush()
  98. self.copy_to.flush()
  99. def write(self, b: ReadableBuffer) -> int:
  100. self.copy_to.write(b)
  101. return super().write(b)
  102. class StreamMixer:
  103. """Mixes `<stdout>` and `<stderr>` streams.
  104. The result is available in the ``output`` attribute.
  105. .. versionadded:: 8.2
  106. """
  107. output: io.BytesIO
  108. stdout: BytesIOCopy
  109. stderr: BytesIOCopy
  110. def __init__(self) -> None:
  111. self.output = io.BytesIO()
  112. self.stdout = BytesIOCopy(copy_to=self.output)
  113. self.stderr = BytesIOCopy(copy_to=self.output)
  114. class _NamedTextIOWrapper(io.TextIOWrapper):
  115. """A :class:`~io.TextIOWrapper` with custom ``name`` and ``mode``
  116. that does not close its underlying buffer.
  117. When ``CliRunner`` runs in ``fd`` mode, ``_original_fd`` is patched to
  118. point at the saved (pre-redirection) ``fd``, so C-level consumers that call
  119. :meth:`fileno` (like ``faulthandler`` or ``subprocess``) keep working. In
  120. the default ``sys`` mode ``_original_fd`` stays at ``-1`` and
  121. :meth:`fileno` raises :exc:`io.UnsupportedOperation`, matching the
  122. pre-``8.3.3`` behavior.
  123. """
  124. _name: str
  125. _mode: str
  126. _original_fd: int
  127. def __init__(
  128. self,
  129. buffer: t.BinaryIO,
  130. name: str,
  131. mode: str,
  132. **kwargs: t.Any,
  133. ) -> None:
  134. super().__init__(buffer, **kwargs)
  135. self._name = name
  136. self._mode = mode
  137. self._original_fd = -1
  138. def close(self) -> None:
  139. """The buffer this object contains belongs to some other object,
  140. so prevent the default ``__del__`` implementation from closing
  141. that buffer.
  142. .. versionadded:: 8.3.2
  143. """
  144. def fileno(self) -> int:
  145. """Return the file descriptor of the saved original stream when
  146. ``CliRunner`` runs in ``fd`` mode. Otherwise delegate to
  147. :class:`~io.TextIOWrapper`, which raises
  148. :exc:`io.UnsupportedOperation` for a ``BytesIO``-backed buffer.
  149. """
  150. if self._original_fd >= 0:
  151. return self._original_fd
  152. return super().fileno()
  153. @property
  154. def name(self) -> str:
  155. return self._name
  156. @property
  157. def mode(self) -> str:
  158. return self._mode
  159. def make_input_stream(
  160. input: str | bytes | t.IO[t.Any] | None, charset: str
  161. ) -> t.BinaryIO:
  162. # Is already an input stream.
  163. if hasattr(input, "read"):
  164. rv = _find_binary_reader(t.cast("t.IO[t.Any]", input))
  165. if rv is not None:
  166. return rv
  167. raise TypeError("Could not find binary reader for input stream.")
  168. if input is None:
  169. input = b""
  170. elif isinstance(input, str):
  171. input = input.encode(charset)
  172. return io.BytesIO(input)
  173. class Result:
  174. """Holds the captured result of an invoked CLI script.
  175. :param runner: The runner that created the result
  176. :param stdout_bytes: The standard output as bytes.
  177. :param stderr_bytes: The standard error as bytes.
  178. :param output_bytes: A mix of ``stdout_bytes`` and ``stderr_bytes``, as the
  179. user would see it in its terminal.
  180. :param return_value: The value returned from the invoked command.
  181. :param exit_code: The exit code as integer.
  182. :param exception: The exception that happened if one did.
  183. :param exc_info: Exception information (exception type, exception instance,
  184. traceback type).
  185. .. versionchanged:: 8.2
  186. ``stderr_bytes`` no longer optional, ``output_bytes`` introduced and
  187. ``mix_stderr`` has been removed.
  188. .. versionadded:: 8.0
  189. Added ``return_value``.
  190. """
  191. runner: CliRunner
  192. stdout_bytes: bytes
  193. stderr_bytes: bytes
  194. output_bytes: bytes
  195. return_value: t.Any
  196. exit_code: int
  197. exception: BaseException | None
  198. exc_info: ExceptionInfo | None
  199. def __init__(
  200. self,
  201. runner: CliRunner,
  202. stdout_bytes: bytes,
  203. stderr_bytes: bytes,
  204. output_bytes: bytes,
  205. return_value: t.Any,
  206. exit_code: int,
  207. exception: BaseException | None,
  208. exc_info: ExceptionInfo | None = None,
  209. ) -> None:
  210. self.runner = runner
  211. self.stdout_bytes = stdout_bytes
  212. self.stderr_bytes = stderr_bytes
  213. self.output_bytes = output_bytes
  214. self.return_value = return_value
  215. self.exit_code = exit_code
  216. self.exception = exception
  217. self.exc_info = exc_info
  218. @property
  219. def output(self) -> str:
  220. """The terminal output as unicode string, as the user would see it.
  221. .. versionchanged:: 8.2
  222. No longer a proxy for ``self.stdout``. Now has its own independent stream
  223. that is mixing `<stdout>` and `<stderr>`, in the order they were written.
  224. """
  225. return self.output_bytes.decode(self.runner.charset, "replace").replace(
  226. "\r\n", "\n"
  227. )
  228. @property
  229. def stdout(self) -> str:
  230. """The standard output as unicode string."""
  231. return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
  232. "\r\n", "\n"
  233. )
  234. @property
  235. def stderr(self) -> str:
  236. """The standard error as unicode string.
  237. .. versionchanged:: 8.2
  238. No longer raise an exception, always returns the `<stderr>` string.
  239. """
  240. return self.stderr_bytes.decode(self.runner.charset, "replace").replace(
  241. "\r\n", "\n"
  242. )
  243. def __repr__(self) -> str:
  244. exc_str = repr(self.exception) if self.exception else "okay"
  245. return f"<{type(self).__name__} {exc_str}>"
  246. class CliRunner:
  247. """The CLI runner provides functionality to invoke a Click command line
  248. script for unittesting purposes in a isolated environment. This only
  249. works in single-threaded systems without any concurrency as it changes the
  250. global interpreter state.
  251. :param charset: the character set for the input and output data.
  252. :param env: a dictionary with environment variables for overriding.
  253. :param echo_stdin: if this is set to `True`, then reading from `<stdin>` writes
  254. to `<stdout>`. This is useful for showing examples in
  255. some circumstances. Note that regular prompts
  256. will automatically echo the input.
  257. :param catch_exceptions: Whether to catch any exceptions other than
  258. ``SystemExit`` when running :meth:`~CliRunner.invoke`.
  259. :param capture: Selects the output capture strategy. ``sys`` (default)
  260. captures Python-level writes only and leaves
  261. :meth:`sys.stdout.fileno` raising :exc:`io.UnsupportedOperation`, so
  262. user code that calls :func:`os.dup2` on ``sys.stdout.fileno()`` cannot
  263. clobber the host runner's stdout. ``fd`` redirects file descriptors
  264. ``1`` and ``2`` via :func:`os.dup2` to a temporary file, also catching
  265. output from stale stream references, C extensions, and subprocesses.
  266. ``fd`` is not supported on Windows.
  267. .. versionchanged:: 8.4.0
  268. Added the ``capture`` parameter. The default ``sys`` mode no longer
  269. exposes the original fd through :meth:`fileno`, reverting the change
  270. introduced in ``8.3.3`` that broke Pytest's ``fd``-level capture
  271. teardown. Use ``capture="fd"`` to restore that behavior with proper
  272. isolation. :issue:`3384`
  273. .. versionchanged:: 8.2
  274. Added the ``catch_exceptions`` parameter.
  275. .. versionchanged:: 8.2
  276. ``mix_stderr`` parameter has been removed.
  277. """
  278. charset: str
  279. env: cabc.Mapping[str, str | None]
  280. echo_stdin: bool
  281. catch_exceptions: bool
  282. capture: CaptureMode
  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. capture: CaptureMode = "sys",
  290. ) -> None:
  291. if capture not in {"sys", "fd"}:
  292. raise ValueError(
  293. f"capture={capture!r} is not valid. Choose from 'sys' or 'fd'."
  294. )
  295. if capture == "fd" and sys.platform == "win32":
  296. raise ValueError(
  297. f"capture={capture!r} is not supported on Windows. Use 'sys'."
  298. )
  299. self.charset = charset
  300. self.env = env or {}
  301. self.echo_stdin = echo_stdin
  302. self.catch_exceptions = catch_exceptions
  303. self.capture = capture
  304. def get_default_prog_name(self, cli: Command) -> str:
  305. """Given a command object it will return the default program name
  306. for it. The default is the `name` attribute or ``"root"`` if not
  307. set.
  308. """
  309. return cli.name or "root"
  310. def make_env(
  311. self, overrides: cabc.Mapping[str, str | None] | None = None
  312. ) -> cabc.Mapping[str, str | None]:
  313. """Returns the environment overrides for invoking a script."""
  314. rv = dict(self.env)
  315. if overrides:
  316. rv.update(overrides)
  317. return rv
  318. @contextlib.contextmanager
  319. def isolation(
  320. self,
  321. input: str | bytes | t.IO[t.Any] | None = None,
  322. env: cabc.Mapping[str, str | None] | None = None,
  323. color: bool = False,
  324. ) -> cabc.Generator[tuple[io.BytesIO, io.BytesIO, io.BytesIO]]:
  325. """A context manager that sets up the isolation for invoking of a
  326. command line tool. This sets up `<stdin>` with the given input data
  327. and `os.environ` with the overrides from the given dictionary.
  328. This also rebinds some internals in Click to be mocked (like the
  329. prompt functionality).
  330. This is automatically done in the :meth:`invoke` method.
  331. :param input: the input stream to put into `sys.stdin`.
  332. :param env: the environment overrides as dictionary.
  333. :param color: whether the output should contain color codes. The
  334. application can still override this explicitly.
  335. .. versionadded:: 8.2
  336. An additional output stream is returned, which is a mix of
  337. `<stdout>` and `<stderr>` streams.
  338. .. versionchanged:: 8.2
  339. Always returns the `<stderr>` stream.
  340. .. versionchanged:: 8.0
  341. `<stderr>` is opened with ``errors="backslashreplace"``
  342. instead of the default ``"strict"``.
  343. .. versionchanged:: 4.0
  344. Added the ``color`` parameter.
  345. """
  346. bytes_input = make_input_stream(input, self.charset)
  347. echo_input = None
  348. old_stdin = sys.stdin
  349. old_stdout = sys.stdout
  350. old_stderr = sys.stderr
  351. old_forced_width = formatting.FORCED_WIDTH
  352. formatting.FORCED_WIDTH = 80
  353. env = self.make_env(env)
  354. stream_mixer = StreamMixer()
  355. if self.echo_stdin:
  356. bytes_input = echo_input = t.cast(
  357. t.BinaryIO, EchoingStdin(bytes_input, stream_mixer.stdout)
  358. )
  359. sys.stdin = text_input = _NamedTextIOWrapper(
  360. bytes_input, encoding=self.charset, name="<stdin>", mode="r"
  361. )
  362. if self.echo_stdin:
  363. # Force unbuffered reads, otherwise TextIOWrapper reads a
  364. # large chunk which is echoed early.
  365. text_input._CHUNK_SIZE = 1 # type: ignore
  366. sys.stdout = _NamedTextIOWrapper(
  367. stream_mixer.stdout,
  368. encoding=self.charset,
  369. name="<stdout>",
  370. mode="w",
  371. )
  372. sys.stderr = _NamedTextIOWrapper(
  373. stream_mixer.stderr,
  374. encoding=self.charset,
  375. name="<stderr>",
  376. mode="w",
  377. errors="backslashreplace",
  378. )
  379. @_pause_echo(echo_input) # type: ignore
  380. def visible_input(prompt: str | None = None) -> str:
  381. sys.stdout.write(prompt or "")
  382. try:
  383. val = next(text_input).rstrip("\r\n")
  384. except StopIteration as e:
  385. raise EOFError() from e
  386. sys.stdout.write(f"{val}\n")
  387. sys.stdout.flush()
  388. return val
  389. @_pause_echo(echo_input) # type: ignore
  390. def hidden_input(prompt: str | None = None) -> str:
  391. sys.stdout.write(f"{prompt or ''}\n")
  392. sys.stdout.flush()
  393. try:
  394. return next(text_input).rstrip("\r\n")
  395. except StopIteration as e:
  396. raise EOFError() from e
  397. @_pause_echo(echo_input) # type: ignore
  398. def _getchar(echo: bool) -> str:
  399. char = sys.stdin.read(1)
  400. if echo:
  401. sys.stdout.write(char)
  402. sys.stdout.flush()
  403. return char
  404. default_color = color
  405. def should_strip_ansi(
  406. stream: t.IO[t.Any] | None = None, color: bool | None = None
  407. ) -> bool:
  408. if color is None:
  409. return not default_color
  410. return not color
  411. old_visible_prompt_func = termui.visible_prompt_func
  412. old_hidden_prompt_func = termui.hidden_prompt_func
  413. old__getchar_func = termui._getchar
  414. old_should_strip_ansi = utils.should_strip_ansi # type: ignore
  415. old__compat_should_strip_ansi = _compat.should_strip_ansi
  416. old_pdb_init = pdb.Pdb.__init__
  417. termui.visible_prompt_func = visible_input
  418. termui.hidden_prompt_func = hidden_input
  419. termui._getchar = _getchar
  420. utils.should_strip_ansi = should_strip_ansi # type: ignore
  421. _compat.should_strip_ansi = should_strip_ansi
  422. def _patched_pdb_init(
  423. self: pdb.Pdb,
  424. completekey: str = "tab",
  425. stdin: t.IO[str] | None = None,
  426. stdout: t.IO[str] | None = None,
  427. **kwargs: t.Any,
  428. ) -> None:
  429. """Default ``pdb.Pdb`` to real terminal streams during
  430. ``CliRunner`` isolation.
  431. Without this patch, ``pdb.Pdb.__init__`` inherits from
  432. ``cmd.Cmd`` which falls back to ``sys.stdin``/``sys.stdout``
  433. when no explicit streams are provided. During isolation
  434. those are ``BytesIO``-backed wrappers, so the debugger
  435. reads from an empty buffer and writes to captured output,
  436. making interactive debugging impossible.
  437. By defaulting to ``sys.__stdin__``/``sys.__stdout__`` (the
  438. original terminal streams Python preserves regardless of
  439. redirection), debuggers can interact with the user while
  440. ``click.echo`` output is still captured normally.
  441. This covers ``pdb.set_trace()``, ``breakpoint()``,
  442. ``pdb.post_mortem()``, and debuggers that subclass
  443. ``pdb.Pdb`` (ipdb, pdbpp). Explicit ``stdin``/``stdout``
  444. arguments are honored and not overridden. Debuggers that
  445. do not subclass ``pdb.Pdb`` (pudb, debugpy) are not
  446. covered.
  447. """
  448. if stdin is None:
  449. stdin = sys.__stdin__
  450. if stdout is None:
  451. stdout = sys.__stdout__
  452. old_pdb_init(
  453. self, completekey=completekey, stdin=stdin, stdout=stdout, **kwargs
  454. )
  455. pdb.Pdb.__init__ = _patched_pdb_init # type: ignore[assignment]
  456. old_env = {}
  457. try:
  458. for key, value in env.items():
  459. old_env[key] = os.environ.get(key)
  460. if value is None:
  461. try:
  462. del os.environ[key]
  463. except Exception:
  464. pass
  465. else:
  466. os.environ[key] = value
  467. yield (stream_mixer.stdout, stream_mixer.stderr, stream_mixer.output)
  468. finally:
  469. for key, value in old_env.items():
  470. if value is None:
  471. try:
  472. del os.environ[key]
  473. except Exception:
  474. pass
  475. else:
  476. os.environ[key] = value
  477. sys.stdout = old_stdout
  478. sys.stderr = old_stderr
  479. sys.stdin = old_stdin
  480. termui.visible_prompt_func = old_visible_prompt_func
  481. termui.hidden_prompt_func = old_hidden_prompt_func
  482. termui._getchar = old__getchar_func
  483. utils.should_strip_ansi = old_should_strip_ansi # type: ignore
  484. _compat.should_strip_ansi = old__compat_should_strip_ansi
  485. formatting.FORCED_WIDTH = old_forced_width
  486. pdb.Pdb.__init__ = old_pdb_init # type: ignore[method-assign]
  487. def invoke(
  488. self,
  489. cli: Command,
  490. args: str | cabc.Sequence[str] | None = None,
  491. input: str | bytes | t.IO[t.Any] | None = None,
  492. env: cabc.Mapping[str, str | None] | None = None,
  493. catch_exceptions: bool | None = None,
  494. color: bool = False,
  495. **extra: t.Any,
  496. ) -> Result:
  497. """Invokes a command in an isolated environment. The arguments are
  498. forwarded directly to the command line script, the `extra` keyword
  499. arguments are passed to the :meth:`~clickpkg.Command.main` function of
  500. the command.
  501. This returns a :class:`Result` object.
  502. :param cli: the command to invoke
  503. :param args: the arguments to invoke. It may be given as an iterable
  504. or a string. When given as string it will be interpreted
  505. as a Unix shell command. More details at
  506. :func:`shlex.split`.
  507. :param input: the input data for `sys.stdin`.
  508. :param env: the environment overrides.
  509. :param catch_exceptions: Whether to catch any other exceptions than
  510. ``SystemExit``. If :data:`None`, the value
  511. from :class:`CliRunner` is used.
  512. :param extra: the keyword arguments to pass to :meth:`main`.
  513. :param color: whether the output should contain color codes. The
  514. application can still override this explicitly.
  515. .. versionadded:: 8.2
  516. The result object has the ``output_bytes`` attribute with
  517. the mix of ``stdout_bytes`` and ``stderr_bytes``, as the user would
  518. see it in its terminal.
  519. .. versionchanged:: 8.2
  520. The result object always returns the ``stderr_bytes`` stream.
  521. .. versionchanged:: 8.0
  522. The result object has the ``return_value`` attribute with
  523. the value returned from the invoked command.
  524. .. versionchanged:: 4.0
  525. Added the ``color`` parameter.
  526. .. versionchanged:: 3.0
  527. Added the ``catch_exceptions`` parameter.
  528. .. versionchanged:: 3.0
  529. The result object has the ``exc_info`` attribute with the
  530. traceback if available.
  531. """
  532. exc_info = None
  533. if catch_exceptions is None:
  534. catch_exceptions = self.catch_exceptions
  535. # Set up fd capture before isolation replaces sys.stdout and sys.stderr.
  536. cap_out: _FDCapture | None = None
  537. cap_err: _FDCapture | None = None
  538. if self.capture == "fd":
  539. cap_out = _FDCapture(1)
  540. cap_err = _FDCapture(2)
  541. try:
  542. cap_out.start()
  543. cap_err.start()
  544. except OSError:
  545. cap_out = cap_err = None
  546. with self.isolation(input=input, env=env, color=color) as outstreams:
  547. # Point the captured streams' fileno() at the saved (original)
  548. # fd so that C-level consumers like faulthandler keep working
  549. # while fd 1/2 are redirected to the capture tmpfile.
  550. if cap_out is not None and cap_err is not None:
  551. sys.stdout._original_fd = cap_out.saved_fd # type: ignore[union-attr]
  552. sys.stderr._original_fd = cap_err.saved_fd # type: ignore[union-attr]
  553. return_value = None
  554. exception: BaseException | None = None
  555. exit_code = 0
  556. if isinstance(args, str):
  557. args = shlex.split(args)
  558. try:
  559. prog_name = extra.pop("prog_name")
  560. except KeyError:
  561. prog_name = self.get_default_prog_name(cli)
  562. try:
  563. return_value = cli.main(args=args or (), prog_name=prog_name, **extra)
  564. except SystemExit as e:
  565. exc_info = sys.exc_info()
  566. e_code = t.cast("int | t.Any | None", e.code)
  567. if e_code is None:
  568. e_code = 0
  569. if e_code != 0:
  570. exception = e
  571. if not isinstance(e_code, int):
  572. sys.stdout.write(str(e_code))
  573. sys.stdout.write("\n")
  574. e_code = 1
  575. exit_code = e_code
  576. except Exception as e:
  577. if not catch_exceptions:
  578. raise
  579. exception = e
  580. exit_code = 1
  581. exc_info = sys.exc_info()
  582. finally:
  583. sys.stdout.flush()
  584. sys.stderr.flush()
  585. # Stop fd capture and merge the captured bytes into
  586. # the stdout/stderr BytesIO streams. BytesIOCopy mirrors
  587. # those writes into outstreams[2] automatically.
  588. if cap_out is not None and cap_err is not None:
  589. fd_out = cap_out.stop()
  590. fd_err = cap_err.stop()
  591. if fd_out:
  592. outstreams[0].write(fd_out)
  593. if fd_err:
  594. outstreams[1].write(fd_err)
  595. stdout = outstreams[0].getvalue()
  596. stderr = outstreams[1].getvalue()
  597. output = outstreams[2].getvalue()
  598. return Result(
  599. runner=self,
  600. stdout_bytes=stdout,
  601. stderr_bytes=stderr,
  602. output_bytes=output,
  603. return_value=return_value,
  604. exit_code=exit_code,
  605. exception=exception,
  606. exc_info=exc_info, # type: ignore
  607. )
  608. @contextlib.contextmanager
  609. def isolated_filesystem(
  610. self, temp_dir: str | os.PathLike[str] | None = None
  611. ) -> cabc.Generator[str]:
  612. """A context manager that creates a temporary directory and
  613. changes the current working directory to it. This isolates tests
  614. that affect the contents of the CWD to prevent them from
  615. interfering with each other.
  616. .. warning::
  617. This helper predates Python 3 and modern pytest, and is not
  618. thread-safe: it relies on :func:`os.chdir`, which mutates
  619. process-global state, and :meth:`invoke` swaps the
  620. process-global standard streams too. Parallelize tests with
  621. processes (``pytest-xdist``), not threads. Locking the
  622. runner (:pr:`3511`, :pr:`3520`, :pr:`3530`) and a
  623. ``set_filesystem()`` API (:issue:`3123`) were declined:
  624. neither removes the global-state mutation. See :issue:`3700`
  625. and :issue:`3501`.
  626. :param temp_dir: Create the temporary directory under this
  627. directory. If given, the created directory is not removed
  628. when exiting.
  629. .. deprecated:: 8.5.0
  630. Will be removed in Click 9.0. Use
  631. :class:`tempfile.TemporaryDirectory` or pytest's
  632. ``tmp_path`` fixture with absolute paths instead.
  633. .. versionchanged:: 8.0
  634. Added the ``temp_dir`` parameter.
  635. """
  636. import warnings
  637. warnings.warn(
  638. "'isolated_filesystem' is deprecated and will be removed in Click"
  639. " 9.0. Use 'tempfile.TemporaryDirectory' or pytest's 'tmp_path'"
  640. " fixture with absolute paths instead.",
  641. DeprecationWarning,
  642. stacklevel=3,
  643. )
  644. cwd = os.getcwd()
  645. dt = tempfile.mkdtemp(dir=temp_dir)
  646. os.chdir(dt)
  647. try:
  648. yield dt
  649. finally:
  650. os.chdir(cwd)
  651. if temp_dir is None:
  652. import shutil
  653. try:
  654. shutil.rmtree(dt)
  655. except OSError:
  656. pass