testing.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  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. class EchoingStdin:
  21. def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None:
  22. self._input = input
  23. self._output = output
  24. self._paused = False
  25. def __getattr__(self, x: str) -> t.Any:
  26. return getattr(self._input, x)
  27. def _echo(self, rv: bytes) -> bytes:
  28. if not self._paused:
  29. self._output.write(rv)
  30. return rv
  31. def read(self, n: int = -1) -> bytes:
  32. return self._echo(self._input.read(n))
  33. def read1(self, n: int = -1) -> bytes:
  34. return self._echo(self._input.read1(n)) # type: ignore
  35. def readline(self, n: int = -1) -> bytes:
  36. return self._echo(self._input.readline(n))
  37. def readlines(self) -> list[bytes]:
  38. return [self._echo(x) for x in self._input.readlines()]
  39. def __iter__(self) -> cabc.Iterator[bytes]:
  40. return iter(self._echo(x) for x in self._input)
  41. def __repr__(self) -> str:
  42. return repr(self._input)
  43. @contextlib.contextmanager
  44. def _pause_echo(stream: EchoingStdin | None) -> cabc.Iterator[None]:
  45. if stream is None:
  46. yield
  47. else:
  48. stream._paused = True
  49. yield
  50. stream._paused = False
  51. class BytesIOCopy(io.BytesIO):
  52. """Patch ``io.BytesIO`` to let the written stream be copied to another.
  53. .. versionadded:: 8.2
  54. """
  55. def __init__(self, copy_to: io.BytesIO) -> None:
  56. super().__init__()
  57. self.copy_to = copy_to
  58. def flush(self) -> None:
  59. super().flush()
  60. self.copy_to.flush()
  61. def write(self, b: ReadableBuffer) -> int:
  62. self.copy_to.write(b)
  63. return super().write(b)
  64. class StreamMixer:
  65. """Mixes `<stdout>` and `<stderr>` streams.
  66. The result is available in the ``output`` attribute.
  67. .. versionadded:: 8.2
  68. """
  69. def __init__(self) -> None:
  70. self.output: io.BytesIO = io.BytesIO()
  71. self.stdout: io.BytesIO = BytesIOCopy(copy_to=self.output)
  72. self.stderr: io.BytesIO = BytesIOCopy(copy_to=self.output)
  73. class _NamedTextIOWrapper(io.TextIOWrapper):
  74. """A :class:`~io.TextIOWrapper` with custom ``name`` and ``mode``
  75. that does not close its underlying buffer.
  76. An optional ``original_fd`` preserves the file descriptor of the
  77. stream being replaced, so that C-level consumers that call
  78. :meth:`fileno` (``faulthandler``, ``subprocess``, ...) still work.
  79. Inspired by pytest's ``capsys``/``capfd`` split: see :doc:`/testing`
  80. for details.
  81. .. versionchanged:: 8.3.3
  82. Added ``original_fd`` parameter and :meth:`fileno` override.
  83. """
  84. def __init__(
  85. self,
  86. buffer: t.BinaryIO,
  87. name: str,
  88. mode: str,
  89. *,
  90. original_fd: int = -1,
  91. **kwargs: t.Any,
  92. ) -> None:
  93. super().__init__(buffer, **kwargs)
  94. self._name = name
  95. self._mode = mode
  96. self._original_fd = original_fd
  97. def close(self) -> None:
  98. """The buffer this object contains belongs to some other object,
  99. so prevent the default ``__del__`` implementation from closing
  100. that buffer.
  101. .. versionadded:: 8.3.2
  102. """
  103. def fileno(self) -> int:
  104. """Return the file descriptor of the original stream, if one was
  105. provided at construction time.
  106. This allows C-level consumers (``faulthandler``, ``subprocess``,
  107. signal handlers, ...) to obtain a valid fd without crashing, even
  108. though the Python-level writes are redirected to an in-memory
  109. buffer.
  110. .. versionadded:: 8.3.3
  111. """
  112. if self._original_fd >= 0:
  113. return self._original_fd
  114. return super().fileno()
  115. @property
  116. def name(self) -> str:
  117. return self._name
  118. @property
  119. def mode(self) -> str:
  120. return self._mode
  121. def make_input_stream(
  122. input: str | bytes | t.IO[t.Any] | None, charset: str
  123. ) -> t.BinaryIO:
  124. # Is already an input stream.
  125. if hasattr(input, "read"):
  126. rv = _find_binary_reader(t.cast("t.IO[t.Any]", input))
  127. if rv is not None:
  128. return rv
  129. raise TypeError("Could not find binary reader for input stream.")
  130. if input is None:
  131. input = b""
  132. elif isinstance(input, str):
  133. input = input.encode(charset)
  134. return io.BytesIO(input)
  135. class Result:
  136. """Holds the captured result of an invoked CLI script.
  137. :param runner: The runner that created the result
  138. :param stdout_bytes: The standard output as bytes.
  139. :param stderr_bytes: The standard error as bytes.
  140. :param output_bytes: A mix of ``stdout_bytes`` and ``stderr_bytes``, as the
  141. user would see it in its terminal.
  142. :param return_value: The value returned from the invoked command.
  143. :param exit_code: The exit code as integer.
  144. :param exception: The exception that happened if one did.
  145. :param exc_info: Exception information (exception type, exception instance,
  146. traceback type).
  147. .. versionchanged:: 8.2
  148. ``stderr_bytes`` no longer optional, ``output_bytes`` introduced and
  149. ``mix_stderr`` has been removed.
  150. .. versionadded:: 8.0
  151. Added ``return_value``.
  152. """
  153. def __init__(
  154. self,
  155. runner: CliRunner,
  156. stdout_bytes: bytes,
  157. stderr_bytes: bytes,
  158. output_bytes: bytes,
  159. return_value: t.Any,
  160. exit_code: int,
  161. exception: BaseException | None,
  162. exc_info: tuple[type[BaseException], BaseException, TracebackType]
  163. | None = None,
  164. ):
  165. self.runner = runner
  166. self.stdout_bytes = stdout_bytes
  167. self.stderr_bytes = stderr_bytes
  168. self.output_bytes = output_bytes
  169. self.return_value = return_value
  170. self.exit_code = exit_code
  171. self.exception = exception
  172. self.exc_info = exc_info
  173. @property
  174. def output(self) -> str:
  175. """The terminal output as unicode string, as the user would see it.
  176. .. versionchanged:: 8.2
  177. No longer a proxy for ``self.stdout``. Now has its own independent stream
  178. that is mixing `<stdout>` and `<stderr>`, in the order they were written.
  179. """
  180. return self.output_bytes.decode(self.runner.charset, "replace").replace(
  181. "\r\n", "\n"
  182. )
  183. @property
  184. def stdout(self) -> str:
  185. """The standard output as unicode string."""
  186. return self.stdout_bytes.decode(self.runner.charset, "replace").replace(
  187. "\r\n", "\n"
  188. )
  189. @property
  190. def stderr(self) -> str:
  191. """The standard error as unicode string.
  192. .. versionchanged:: 8.2
  193. No longer raise an exception, always returns the `<stderr>` string.
  194. """
  195. return self.stderr_bytes.decode(self.runner.charset, "replace").replace(
  196. "\r\n", "\n"
  197. )
  198. def __repr__(self) -> str:
  199. exc_str = repr(self.exception) if self.exception else "okay"
  200. return f"<{type(self).__name__} {exc_str}>"
  201. class CliRunner:
  202. """The CLI runner provides functionality to invoke a Click command line
  203. script for unittesting purposes in a isolated environment. This only
  204. works in single-threaded systems without any concurrency as it changes the
  205. global interpreter state.
  206. :param charset: the character set for the input and output data.
  207. :param env: a dictionary with environment variables for overriding.
  208. :param echo_stdin: if this is set to `True`, then reading from `<stdin>` writes
  209. to `<stdout>`. This is useful for showing examples in
  210. some circumstances. Note that regular prompts
  211. will automatically echo the input.
  212. :param catch_exceptions: Whether to catch any exceptions other than
  213. ``SystemExit`` when running :meth:`~CliRunner.invoke`.
  214. .. versionchanged:: 8.2
  215. Added the ``catch_exceptions`` parameter.
  216. .. versionchanged:: 8.2
  217. ``mix_stderr`` parameter has been removed.
  218. """
  219. def __init__(
  220. self,
  221. charset: str = "utf-8",
  222. env: cabc.Mapping[str, str | None] | None = None,
  223. echo_stdin: bool = False,
  224. catch_exceptions: bool = True,
  225. ) -> None:
  226. self.charset = charset
  227. self.env: cabc.Mapping[str, str | None] = env or {}
  228. self.echo_stdin = echo_stdin
  229. self.catch_exceptions = catch_exceptions
  230. def get_default_prog_name(self, cli: Command) -> str:
  231. """Given a command object it will return the default program name
  232. for it. The default is the `name` attribute or ``"root"`` if not
  233. set.
  234. """
  235. return cli.name or "root"
  236. def make_env(
  237. self, overrides: cabc.Mapping[str, str | None] | None = None
  238. ) -> cabc.Mapping[str, str | None]:
  239. """Returns the environment overrides for invoking a script."""
  240. rv = dict(self.env)
  241. if overrides:
  242. rv.update(overrides)
  243. return rv
  244. @contextlib.contextmanager
  245. def isolation(
  246. self,
  247. input: str | bytes | t.IO[t.Any] | None = None,
  248. env: cabc.Mapping[str, str | None] | None = None,
  249. color: bool = False,
  250. ) -> cabc.Iterator[tuple[io.BytesIO, io.BytesIO, io.BytesIO]]:
  251. """A context manager that sets up the isolation for invoking of a
  252. command line tool. This sets up `<stdin>` with the given input data
  253. and `os.environ` with the overrides from the given dictionary.
  254. This also rebinds some internals in Click to be mocked (like the
  255. prompt functionality).
  256. This is automatically done in the :meth:`invoke` method.
  257. :param input: the input stream to put into `sys.stdin`.
  258. :param env: the environment overrides as dictionary.
  259. :param color: whether the output should contain color codes. The
  260. application can still override this explicitly.
  261. .. versionadded:: 8.2
  262. An additional output stream is returned, which is a mix of
  263. `<stdout>` and `<stderr>` streams.
  264. .. versionchanged:: 8.2
  265. Always returns the `<stderr>` stream.
  266. .. versionchanged:: 8.0
  267. `<stderr>` is opened with ``errors="backslashreplace"``
  268. instead of the default ``"strict"``.
  269. .. versionchanged:: 4.0
  270. Added the ``color`` parameter.
  271. """
  272. bytes_input = make_input_stream(input, self.charset)
  273. echo_input = None
  274. old_stdin = sys.stdin
  275. old_stdout = sys.stdout
  276. old_stderr = sys.stderr
  277. old_forced_width = formatting.FORCED_WIDTH
  278. formatting.FORCED_WIDTH = 80
  279. env = self.make_env(env)
  280. stream_mixer = StreamMixer()
  281. # Preserve the original file descriptors so that C-level
  282. # consumers (faulthandler, subprocess, etc.) can still obtain a
  283. # valid fd from the redirected streams. The original streams
  284. # may themselves lack a fileno() (e.g. when CliRunner is used
  285. # inside pytest's capsys), so we fall back to -1.
  286. def _safe_fileno(stream: t.IO[t.Any]) -> int:
  287. try:
  288. return stream.fileno()
  289. except (AttributeError, io.UnsupportedOperation):
  290. return -1
  291. old_stdout_fd = _safe_fileno(old_stdout)
  292. old_stderr_fd = _safe_fileno(old_stderr)
  293. if self.echo_stdin:
  294. bytes_input = echo_input = t.cast(
  295. t.BinaryIO, EchoingStdin(bytes_input, stream_mixer.stdout)
  296. )
  297. sys.stdin = text_input = _NamedTextIOWrapper(
  298. bytes_input, encoding=self.charset, name="<stdin>", mode="r"
  299. )
  300. if self.echo_stdin:
  301. # Force unbuffered reads, otherwise TextIOWrapper reads a
  302. # large chunk which is echoed early.
  303. text_input._CHUNK_SIZE = 1 # type: ignore
  304. sys.stdout = _NamedTextIOWrapper(
  305. stream_mixer.stdout,
  306. encoding=self.charset,
  307. name="<stdout>",
  308. mode="w",
  309. original_fd=old_stdout_fd,
  310. )
  311. sys.stderr = _NamedTextIOWrapper(
  312. stream_mixer.stderr,
  313. encoding=self.charset,
  314. name="<stderr>",
  315. mode="w",
  316. errors="backslashreplace",
  317. original_fd=old_stderr_fd,
  318. )
  319. @_pause_echo(echo_input) # type: ignore
  320. def visible_input(prompt: str | None = None) -> str:
  321. sys.stdout.write(prompt or "")
  322. try:
  323. val = next(text_input).rstrip("\r\n")
  324. except StopIteration as e:
  325. raise EOFError() from e
  326. sys.stdout.write(f"{val}\n")
  327. sys.stdout.flush()
  328. return val
  329. @_pause_echo(echo_input) # type: ignore
  330. def hidden_input(prompt: str | None = None) -> str:
  331. sys.stdout.write(f"{prompt or ''}\n")
  332. sys.stdout.flush()
  333. try:
  334. return next(text_input).rstrip("\r\n")
  335. except StopIteration as e:
  336. raise EOFError() from e
  337. @_pause_echo(echo_input) # type: ignore
  338. def _getchar(echo: bool) -> str:
  339. char = sys.stdin.read(1)
  340. if echo:
  341. sys.stdout.write(char)
  342. sys.stdout.flush()
  343. return char
  344. default_color = color
  345. def should_strip_ansi(
  346. stream: t.IO[t.Any] | None = None, color: bool | None = None
  347. ) -> bool:
  348. if color is None:
  349. return not default_color
  350. return not color
  351. old_visible_prompt_func = termui.visible_prompt_func
  352. old_hidden_prompt_func = termui.hidden_prompt_func
  353. old__getchar_func = termui._getchar
  354. old_should_strip_ansi = utils.should_strip_ansi # type: ignore
  355. old__compat_should_strip_ansi = _compat.should_strip_ansi
  356. old_pdb_init = pdb.Pdb.__init__
  357. termui.visible_prompt_func = visible_input
  358. termui.hidden_prompt_func = hidden_input
  359. termui._getchar = _getchar
  360. utils.should_strip_ansi = should_strip_ansi # type: ignore
  361. _compat.should_strip_ansi = should_strip_ansi
  362. def _patched_pdb_init(
  363. self: pdb.Pdb,
  364. completekey: str = "tab",
  365. stdin: t.IO[str] | None = None,
  366. stdout: t.IO[str] | None = None,
  367. **kwargs: t.Any,
  368. ) -> None:
  369. """Default ``pdb.Pdb`` to real terminal streams during
  370. ``CliRunner`` isolation.
  371. Without this patch, ``pdb.Pdb.__init__`` inherits from
  372. ``cmd.Cmd`` which falls back to ``sys.stdin``/``sys.stdout``
  373. when no explicit streams are provided. During isolation
  374. those are ``BytesIO``-backed wrappers, so the debugger
  375. reads from an empty buffer and writes to captured output,
  376. making interactive debugging impossible.
  377. By defaulting to ``sys.__stdin__``/``sys.__stdout__`` (the
  378. original terminal streams Python preserves regardless of
  379. redirection), debuggers can interact with the user while
  380. ``click.echo`` output is still captured normally.
  381. This covers ``pdb.set_trace()``, ``breakpoint()``,
  382. ``pdb.post_mortem()``, and debuggers that subclass
  383. ``pdb.Pdb`` (ipdb, pdbpp). Explicit ``stdin``/``stdout``
  384. arguments are honored and not overridden. Debuggers that
  385. do not subclass ``pdb.Pdb`` (pudb, debugpy) are not
  386. covered.
  387. """
  388. if stdin is None:
  389. stdin = sys.__stdin__
  390. if stdout is None:
  391. stdout = sys.__stdout__
  392. old_pdb_init(
  393. self, completekey=completekey, stdin=stdin, stdout=stdout, **kwargs
  394. )
  395. pdb.Pdb.__init__ = _patched_pdb_init # type: ignore[assignment]
  396. old_env = {}
  397. try:
  398. for key, value in env.items():
  399. old_env[key] = os.environ.get(key)
  400. if value is None:
  401. try:
  402. del os.environ[key]
  403. except Exception:
  404. pass
  405. else:
  406. os.environ[key] = value
  407. yield (stream_mixer.stdout, stream_mixer.stderr, stream_mixer.output)
  408. finally:
  409. for key, value in old_env.items():
  410. if value is None:
  411. try:
  412. del os.environ[key]
  413. except Exception:
  414. pass
  415. else:
  416. os.environ[key] = value
  417. sys.stdout = old_stdout
  418. sys.stderr = old_stderr
  419. sys.stdin = old_stdin
  420. termui.visible_prompt_func = old_visible_prompt_func
  421. termui.hidden_prompt_func = old_hidden_prompt_func
  422. termui._getchar = old__getchar_func
  423. utils.should_strip_ansi = old_should_strip_ansi # type: ignore
  424. _compat.should_strip_ansi = old__compat_should_strip_ansi
  425. formatting.FORCED_WIDTH = old_forced_width
  426. pdb.Pdb.__init__ = old_pdb_init # type: ignore[method-assign]
  427. def invoke(
  428. self,
  429. cli: Command,
  430. args: str | cabc.Sequence[str] | None = None,
  431. input: str | bytes | t.IO[t.Any] | None = None,
  432. env: cabc.Mapping[str, str | None] | None = None,
  433. catch_exceptions: bool | None = None,
  434. color: bool = False,
  435. **extra: t.Any,
  436. ) -> Result:
  437. """Invokes a command in an isolated environment. The arguments are
  438. forwarded directly to the command line script, the `extra` keyword
  439. arguments are passed to the :meth:`~clickpkg.Command.main` function of
  440. the command.
  441. This returns a :class:`Result` object.
  442. :param cli: the command to invoke
  443. :param args: the arguments to invoke. It may be given as an iterable
  444. or a string. When given as string it will be interpreted
  445. as a Unix shell command. More details at
  446. :func:`shlex.split`.
  447. :param input: the input data for `sys.stdin`.
  448. :param env: the environment overrides.
  449. :param catch_exceptions: Whether to catch any other exceptions than
  450. ``SystemExit``. If :data:`None`, the value
  451. from :class:`CliRunner` is used.
  452. :param extra: the keyword arguments to pass to :meth:`main`.
  453. :param color: whether the output should contain color codes. The
  454. application can still override this explicitly.
  455. .. versionadded:: 8.2
  456. The result object has the ``output_bytes`` attribute with
  457. the mix of ``stdout_bytes`` and ``stderr_bytes``, as the user would
  458. see it in its terminal.
  459. .. versionchanged:: 8.2
  460. The result object always returns the ``stderr_bytes`` stream.
  461. .. versionchanged:: 8.0
  462. The result object has the ``return_value`` attribute with
  463. the value returned from the invoked command.
  464. .. versionchanged:: 4.0
  465. Added the ``color`` parameter.
  466. .. versionchanged:: 3.0
  467. Added the ``catch_exceptions`` parameter.
  468. .. versionchanged:: 3.0
  469. The result object has the ``exc_info`` attribute with the
  470. traceback if available.
  471. """
  472. exc_info = None
  473. if catch_exceptions is None:
  474. catch_exceptions = self.catch_exceptions
  475. with self.isolation(input=input, env=env, color=color) as outstreams:
  476. return_value = None
  477. exception: BaseException | None = None
  478. exit_code = 0
  479. if isinstance(args, str):
  480. args = shlex.split(args)
  481. try:
  482. prog_name = extra.pop("prog_name")
  483. except KeyError:
  484. prog_name = self.get_default_prog_name(cli)
  485. try:
  486. return_value = cli.main(args=args or (), prog_name=prog_name, **extra)
  487. except SystemExit as e:
  488. exc_info = sys.exc_info()
  489. e_code = t.cast("int | t.Any | None", e.code)
  490. if e_code is None:
  491. e_code = 0
  492. if e_code != 0:
  493. exception = e
  494. if not isinstance(e_code, int):
  495. sys.stdout.write(str(e_code))
  496. sys.stdout.write("\n")
  497. e_code = 1
  498. exit_code = e_code
  499. except Exception as e:
  500. if not catch_exceptions:
  501. raise
  502. exception = e
  503. exit_code = 1
  504. exc_info = sys.exc_info()
  505. finally:
  506. sys.stdout.flush()
  507. sys.stderr.flush()
  508. stdout = outstreams[0].getvalue()
  509. stderr = outstreams[1].getvalue()
  510. output = outstreams[2].getvalue()
  511. return Result(
  512. runner=self,
  513. stdout_bytes=stdout,
  514. stderr_bytes=stderr,
  515. output_bytes=output,
  516. return_value=return_value,
  517. exit_code=exit_code,
  518. exception=exception,
  519. exc_info=exc_info, # type: ignore
  520. )
  521. @contextlib.contextmanager
  522. def isolated_filesystem(
  523. self, temp_dir: str | os.PathLike[str] | None = None
  524. ) -> cabc.Iterator[str]:
  525. """A context manager that creates a temporary directory and
  526. changes the current working directory to it. This isolates tests
  527. that affect the contents of the CWD to prevent them from
  528. interfering with each other.
  529. :param temp_dir: Create the temporary directory under this
  530. directory. If given, the created directory is not removed
  531. when exiting.
  532. .. versionchanged:: 8.0
  533. Added the ``temp_dir`` parameter.
  534. """
  535. cwd = os.getcwd()
  536. dt = tempfile.mkdtemp(dir=temp_dir)
  537. os.chdir(dt)
  538. try:
  539. yield dt
  540. finally:
  541. os.chdir(cwd)
  542. if temp_dir is None:
  543. import shutil
  544. try:
  545. shutil.rmtree(dt)
  546. except OSError:
  547. pass