Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/capture.py: 62%
567 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1"""Per-test stdout/stderr capturing mechanism."""
2import contextlib
3import functools
4import io
5import os
6import sys
7from io import UnsupportedOperation
8from tempfile import TemporaryFile
9from typing import Any
10from typing import AnyStr
11from typing import Generator
12from typing import Generic
13from typing import Iterator
14from typing import Optional
15from typing import TextIO
16from typing import Tuple
17from typing import TYPE_CHECKING
18from typing import Union
20from _pytest.compat import final
21from _pytest.config import Config
22from _pytest.config import hookimpl
23from _pytest.config.argparsing import Parser
24from _pytest.deprecated import check_ispytest
25from _pytest.fixtures import fixture
26from _pytest.fixtures import SubRequest
27from _pytest.nodes import Collector
28from _pytest.nodes import File
29from _pytest.nodes import Item
31if TYPE_CHECKING:
32 from typing_extensions import Literal
34 _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
37def pytest_addoption(parser: Parser) -> None:
38 group = parser.getgroup("general")
39 group._addoption(
40 "--capture",
41 action="store",
42 default="fd",
43 metavar="method",
44 choices=["fd", "sys", "no", "tee-sys"],
45 help="Per-test capturing method: one of fd|sys|no|tee-sys",
46 )
47 group._addoption(
48 "-s",
49 action="store_const",
50 const="no",
51 dest="capture",
52 help="Shortcut for --capture=no",
53 )
56def _colorama_workaround() -> None:
57 """Ensure colorama is imported so that it attaches to the correct stdio
58 handles on Windows.
60 colorama uses the terminal on import time. So if something does the
61 first import of colorama while I/O capture is active, colorama will
62 fail in various ways.
63 """
64 if sys.platform.startswith("win32"):
65 try:
66 import colorama # noqa: F401
67 except ImportError:
68 pass
71def _windowsconsoleio_workaround(stream: TextIO) -> None:
72 """Workaround for Windows Unicode console handling.
74 Python 3.6 implemented Unicode console handling for Windows. This works
75 by reading/writing to the raw console handle using
76 ``{Read,Write}ConsoleW``.
78 The problem is that we are going to ``dup2`` over the stdio file
79 descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
80 handles used by Python to write to the console. Though there is still some
81 weirdness and the console handle seems to only be closed randomly and not
82 on the first call to ``CloseHandle``, or maybe it gets reopened with the
83 same handle value when we suspend capturing.
85 The workaround in this case will reopen stdio with a different fd which
86 also means a different handle by replicating the logic in
87 "Py_lifecycle.c:initstdio/create_stdio".
89 :param stream:
90 In practice ``sys.stdout`` or ``sys.stderr``, but given
91 here as parameter for unittesting purposes.
93 See https://github.com/pytest-dev/py/issues/103.
94 """
95 if not sys.platform.startswith("win32") or hasattr(sys, "pypy_version_info"):
96 return
98 # Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
99 if not hasattr(stream, "buffer"): # type: ignore[unreachable]
100 return
102 buffered = hasattr(stream.buffer, "raw")
103 raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]
105 if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined]
106 return
108 def _reopen_stdio(f, mode):
109 if not buffered and mode[0] == "w":
110 buffering = 0
111 else:
112 buffering = -1
114 return io.TextIOWrapper(
115 open(os.dup(f.fileno()), mode, buffering),
116 f.encoding,
117 f.errors,
118 f.newlines,
119 f.line_buffering,
120 )
122 sys.stdin = _reopen_stdio(sys.stdin, "rb")
123 sys.stdout = _reopen_stdio(sys.stdout, "wb")
124 sys.stderr = _reopen_stdio(sys.stderr, "wb")
127@hookimpl(hookwrapper=True)
128def pytest_load_initial_conftests(early_config: Config):
129 ns = early_config.known_args_namespace
130 if ns.capture == "fd":
131 _windowsconsoleio_workaround(sys.stdout)
132 _colorama_workaround()
133 pluginmanager = early_config.pluginmanager
134 capman = CaptureManager(ns.capture)
135 pluginmanager.register(capman, "capturemanager")
137 # Make sure that capturemanager is properly reset at final shutdown.
138 early_config.add_cleanup(capman.stop_global_capturing)
140 # Finally trigger conftest loading but while capturing (issue #93).
141 capman.start_global_capturing()
142 outcome = yield
143 capman.suspend_global_capture()
144 if outcome.excinfo is not None:
145 out, err = capman.read_global_capture()
146 sys.stdout.write(out)
147 sys.stderr.write(err)
150# IO Helpers.
153class EncodedFile(io.TextIOWrapper):
154 __slots__ = ()
156 @property
157 def name(self) -> str:
158 # Ensure that file.name is a string. Workaround for a Python bug
159 # fixed in >=3.7.4: https://bugs.python.org/issue36015
160 return repr(self.buffer)
162 @property
163 def mode(self) -> str:
164 # TextIOWrapper doesn't expose a mode, but at least some of our
165 # tests check it.
166 return self.buffer.mode.replace("b", "")
169class CaptureIO(io.TextIOWrapper):
170 def __init__(self) -> None:
171 super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True)
173 def getvalue(self) -> str:
174 assert isinstance(self.buffer, io.BytesIO)
175 return self.buffer.getvalue().decode("UTF-8")
178class TeeCaptureIO(CaptureIO):
179 def __init__(self, other: TextIO) -> None:
180 self._other = other
181 super().__init__()
183 def write(self, s: str) -> int:
184 super().write(s)
185 return self._other.write(s)
188class DontReadFromInput:
189 encoding = None
191 def read(self, *args):
192 raise OSError(
193 "pytest: reading from stdin while output is captured! Consider using `-s`."
194 )
196 readline = read
197 readlines = read
198 __next__ = read
200 def __iter__(self):
201 return self
203 def fileno(self) -> int:
204 raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")
206 def flush(self) -> None:
207 raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()")
209 def isatty(self) -> bool:
210 return False
212 def close(self) -> None:
213 pass
215 def readable(self) -> bool:
216 return False
218 def seek(self, offset: int) -> int:
219 raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
221 def seekable(self) -> bool:
222 return False
224 def tell(self) -> int:
225 raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
227 def truncate(self, size: int) -> None:
228 raise UnsupportedOperation("cannont truncate stdin")
230 def write(self, *args) -> None:
231 raise UnsupportedOperation("cannot write to stdin")
233 def writelines(self, *args) -> None:
234 raise UnsupportedOperation("Cannot write to stdin")
236 def writable(self) -> bool:
237 return False
239 @property
240 def buffer(self):
241 return self
244# Capture classes.
247patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
250class NoCapture:
251 EMPTY_BUFFER = None
252 __init__ = start = done = suspend = resume = lambda *args: None
255class SysCaptureBinary:
257 EMPTY_BUFFER = b""
259 def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
260 name = patchsysdict[fd]
261 self._old = getattr(sys, name)
262 self.name = name
263 if tmpfile is None:
264 if name == "stdin":
265 tmpfile = DontReadFromInput()
266 else:
267 tmpfile = CaptureIO() if not tee else TeeCaptureIO(self._old)
268 self.tmpfile = tmpfile
269 self._state = "initialized"
271 def repr(self, class_name: str) -> str:
272 return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
273 class_name,
274 self.name,
275 hasattr(self, "_old") and repr(self._old) or "<UNSET>",
276 self._state,
277 self.tmpfile,
278 )
280 def __repr__(self) -> str:
281 return "<{} {} _old={} _state={!r} tmpfile={!r}>".format(
282 self.__class__.__name__,
283 self.name,
284 hasattr(self, "_old") and repr(self._old) or "<UNSET>",
285 self._state,
286 self.tmpfile,
287 )
289 def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
290 assert (
291 self._state in states
292 ), "cannot {} in state {!r}: expected one of {}".format(
293 op, self._state, ", ".join(states)
294 )
296 def start(self) -> None:
297 self._assert_state("start", ("initialized",))
298 setattr(sys, self.name, self.tmpfile)
299 self._state = "started"
301 def snap(self):
302 self._assert_state("snap", ("started", "suspended"))
303 self.tmpfile.seek(0)
304 res = self.tmpfile.buffer.read()
305 self.tmpfile.seek(0)
306 self.tmpfile.truncate()
307 return res
309 def done(self) -> None:
310 self._assert_state("done", ("initialized", "started", "suspended", "done"))
311 if self._state == "done":
312 return
313 setattr(sys, self.name, self._old)
314 del self._old
315 self.tmpfile.close()
316 self._state = "done"
318 def suspend(self) -> None:
319 self._assert_state("suspend", ("started", "suspended"))
320 setattr(sys, self.name, self._old)
321 self._state = "suspended"
323 def resume(self) -> None:
324 self._assert_state("resume", ("started", "suspended"))
325 if self._state == "started":
326 return
327 setattr(sys, self.name, self.tmpfile)
328 self._state = "started"
330 def writeorg(self, data) -> None:
331 self._assert_state("writeorg", ("started", "suspended"))
332 self._old.flush()
333 self._old.buffer.write(data)
334 self._old.buffer.flush()
337class SysCapture(SysCaptureBinary):
338 EMPTY_BUFFER = "" # type: ignore[assignment]
340 def snap(self):
341 res = self.tmpfile.getvalue()
342 self.tmpfile.seek(0)
343 self.tmpfile.truncate()
344 return res
346 def writeorg(self, data):
347 self._assert_state("writeorg", ("started", "suspended"))
348 self._old.write(data)
349 self._old.flush()
352class FDCaptureBinary:
353 """Capture IO to/from a given OS-level file descriptor.
355 snap() produces `bytes`.
356 """
358 EMPTY_BUFFER = b""
360 def __init__(self, targetfd: int) -> None:
361 self.targetfd = targetfd
363 try:
364 os.fstat(targetfd)
365 except OSError:
366 # FD capturing is conceptually simple -- create a temporary file,
367 # redirect the FD to it, redirect back when done. But when the
368 # target FD is invalid it throws a wrench into this lovely scheme.
369 #
370 # Tests themselves shouldn't care if the FD is valid, FD capturing
371 # should work regardless of external circumstances. So falling back
372 # to just sys capturing is not a good option.
373 #
374 # Further complications are the need to support suspend() and the
375 # possibility of FD reuse (e.g. the tmpfile getting the very same
376 # target FD). The following approach is robust, I believe.
377 self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR)
378 os.dup2(self.targetfd_invalid, targetfd)
379 else:
380 self.targetfd_invalid = None
381 self.targetfd_save = os.dup(targetfd)
383 if targetfd == 0:
384 self.tmpfile = open(os.devnull, encoding="utf-8")
385 self.syscapture = SysCapture(targetfd)
386 else:
387 self.tmpfile = EncodedFile(
388 TemporaryFile(buffering=0),
389 encoding="utf-8",
390 errors="replace",
391 newline="",
392 write_through=True,
393 )
394 if targetfd in patchsysdict:
395 self.syscapture = SysCapture(targetfd, self.tmpfile)
396 else:
397 self.syscapture = NoCapture()
399 self._state = "initialized"
401 def __repr__(self) -> str:
402 return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format(
403 self.__class__.__name__,
404 self.targetfd,
405 self.targetfd_save,
406 self._state,
407 self.tmpfile,
408 )
410 def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
411 assert (
412 self._state in states
413 ), "cannot {} in state {!r}: expected one of {}".format(
414 op, self._state, ", ".join(states)
415 )
417 def start(self) -> None:
418 """Start capturing on targetfd using memorized tmpfile."""
419 self._assert_state("start", ("initialized",))
420 os.dup2(self.tmpfile.fileno(), self.targetfd)
421 self.syscapture.start()
422 self._state = "started"
424 def snap(self):
425 self._assert_state("snap", ("started", "suspended"))
426 self.tmpfile.seek(0)
427 res = self.tmpfile.buffer.read()
428 self.tmpfile.seek(0)
429 self.tmpfile.truncate()
430 return res
432 def done(self) -> None:
433 """Stop capturing, restore streams, return original capture file,
434 seeked to position zero."""
435 self._assert_state("done", ("initialized", "started", "suspended", "done"))
436 if self._state == "done":
437 return
438 os.dup2(self.targetfd_save, self.targetfd)
439 os.close(self.targetfd_save)
440 if self.targetfd_invalid is not None:
441 if self.targetfd_invalid != self.targetfd:
442 os.close(self.targetfd)
443 os.close(self.targetfd_invalid)
444 self.syscapture.done()
445 self.tmpfile.close()
446 self._state = "done"
448 def suspend(self) -> None:
449 self._assert_state("suspend", ("started", "suspended"))
450 if self._state == "suspended":
451 return
452 self.syscapture.suspend()
453 os.dup2(self.targetfd_save, self.targetfd)
454 self._state = "suspended"
456 def resume(self) -> None:
457 self._assert_state("resume", ("started", "suspended"))
458 if self._state == "started":
459 return
460 self.syscapture.resume()
461 os.dup2(self.tmpfile.fileno(), self.targetfd)
462 self._state = "started"
464 def writeorg(self, data):
465 """Write to original file descriptor."""
466 self._assert_state("writeorg", ("started", "suspended"))
467 os.write(self.targetfd_save, data)
470class FDCapture(FDCaptureBinary):
471 """Capture IO to/from a given OS-level file descriptor.
473 snap() produces text.
474 """
476 # Ignore type because it doesn't match the type in the superclass (bytes).
477 EMPTY_BUFFER = "" # type: ignore
479 def snap(self):
480 self._assert_state("snap", ("started", "suspended"))
481 self.tmpfile.seek(0)
482 res = self.tmpfile.read()
483 self.tmpfile.seek(0)
484 self.tmpfile.truncate()
485 return res
487 def writeorg(self, data):
488 """Write to original file descriptor."""
489 super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
492# MultiCapture
495# This class was a namedtuple, but due to mypy limitation[0] it could not be
496# made generic, so was replaced by a regular class which tries to emulate the
497# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
498# make it a namedtuple again.
499# [0]: https://github.com/python/mypy/issues/685
500@final
501@functools.total_ordering
502class CaptureResult(Generic[AnyStr]):
503 """The result of :method:`CaptureFixture.readouterr`."""
505 __slots__ = ("out", "err")
507 def __init__(self, out: AnyStr, err: AnyStr) -> None:
508 self.out: AnyStr = out
509 self.err: AnyStr = err
511 def __len__(self) -> int:
512 return 2
514 def __iter__(self) -> Iterator[AnyStr]:
515 return iter((self.out, self.err))
517 def __getitem__(self, item: int) -> AnyStr:
518 return tuple(self)[item]
520 def _replace(
521 self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None
522 ) -> "CaptureResult[AnyStr]":
523 return CaptureResult(
524 out=self.out if out is None else out, err=self.err if err is None else err
525 )
527 def count(self, value: AnyStr) -> int:
528 return tuple(self).count(value)
530 def index(self, value) -> int:
531 return tuple(self).index(value)
533 def __eq__(self, other: object) -> bool:
534 if not isinstance(other, (CaptureResult, tuple)):
535 return NotImplemented
536 return tuple(self) == tuple(other)
538 def __hash__(self) -> int:
539 return hash(tuple(self))
541 def __lt__(self, other: object) -> bool:
542 if not isinstance(other, (CaptureResult, tuple)):
543 return NotImplemented
544 return tuple(self) < tuple(other)
546 def __repr__(self) -> str:
547 return f"CaptureResult(out={self.out!r}, err={self.err!r})"
550class MultiCapture(Generic[AnyStr]):
551 _state = None
552 _in_suspended = False
554 def __init__(self, in_, out, err) -> None:
555 self.in_ = in_
556 self.out = out
557 self.err = err
559 def __repr__(self) -> str:
560 return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
561 self.out,
562 self.err,
563 self.in_,
564 self._state,
565 self._in_suspended,
566 )
568 def start_capturing(self) -> None:
569 self._state = "started"
570 if self.in_:
571 self.in_.start()
572 if self.out:
573 self.out.start()
574 if self.err:
575 self.err.start()
577 def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]:
578 """Pop current snapshot out/err capture and flush to orig streams."""
579 out, err = self.readouterr()
580 if out:
581 self.out.writeorg(out)
582 if err:
583 self.err.writeorg(err)
584 return out, err
586 def suspend_capturing(self, in_: bool = False) -> None:
587 self._state = "suspended"
588 if self.out:
589 self.out.suspend()
590 if self.err:
591 self.err.suspend()
592 if in_ and self.in_:
593 self.in_.suspend()
594 self._in_suspended = True
596 def resume_capturing(self) -> None:
597 self._state = "started"
598 if self.out:
599 self.out.resume()
600 if self.err:
601 self.err.resume()
602 if self._in_suspended:
603 self.in_.resume()
604 self._in_suspended = False
606 def stop_capturing(self) -> None:
607 """Stop capturing and reset capturing streams."""
608 if self._state == "stopped":
609 raise ValueError("was already stopped")
610 self._state = "stopped"
611 if self.out:
612 self.out.done()
613 if self.err:
614 self.err.done()
615 if self.in_:
616 self.in_.done()
618 def is_started(self) -> bool:
619 """Whether actively capturing -- not suspended or stopped."""
620 return self._state == "started"
622 def readouterr(self) -> CaptureResult[AnyStr]:
623 out = self.out.snap() if self.out else ""
624 err = self.err.snap() if self.err else ""
625 return CaptureResult(out, err)
628def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
629 if method == "fd":
630 return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
631 elif method == "sys":
632 return MultiCapture(in_=SysCapture(0), out=SysCapture(1), err=SysCapture(2))
633 elif method == "no":
634 return MultiCapture(in_=None, out=None, err=None)
635 elif method == "tee-sys":
636 return MultiCapture(
637 in_=None, out=SysCapture(1, tee=True), err=SysCapture(2, tee=True)
638 )
639 raise ValueError(f"unknown capturing method: {method!r}")
642# CaptureManager and CaptureFixture
645class CaptureManager:
646 """The capture plugin.
648 Manages that the appropriate capture method is enabled/disabled during
649 collection and each test phase (setup, call, teardown). After each of
650 those points, the captured output is obtained and attached to the
651 collection/runtest report.
653 There are two levels of capture:
655 * global: enabled by default and can be suppressed by the ``-s``
656 option. This is always enabled/disabled during collection and each test
657 phase.
659 * fixture: when a test function or one of its fixture depend on the
660 ``capsys`` or ``capfd`` fixtures. In this case special handling is
661 needed to ensure the fixtures take precedence over the global capture.
662 """
664 def __init__(self, method: "_CaptureMethod") -> None:
665 self._method = method
666 self._global_capturing: Optional[MultiCapture[str]] = None
667 self._capture_fixture: Optional[CaptureFixture[Any]] = None
669 def __repr__(self) -> str:
670 return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
671 self._method, self._global_capturing, self._capture_fixture
672 )
674 def is_capturing(self) -> Union[str, bool]:
675 if self.is_globally_capturing():
676 return "global"
677 if self._capture_fixture:
678 return "fixture %s" % self._capture_fixture.request.fixturename
679 return False
681 # Global capturing control
683 def is_globally_capturing(self) -> bool:
684 return self._method != "no"
686 def start_global_capturing(self) -> None:
687 assert self._global_capturing is None
688 self._global_capturing = _get_multicapture(self._method)
689 self._global_capturing.start_capturing()
691 def stop_global_capturing(self) -> None:
692 if self._global_capturing is not None:
693 self._global_capturing.pop_outerr_to_orig()
694 self._global_capturing.stop_capturing()
695 self._global_capturing = None
697 def resume_global_capture(self) -> None:
698 # During teardown of the python process, and on rare occasions, capture
699 # attributes can be `None` while trying to resume global capture.
700 if self._global_capturing is not None:
701 self._global_capturing.resume_capturing()
703 def suspend_global_capture(self, in_: bool = False) -> None:
704 if self._global_capturing is not None:
705 self._global_capturing.suspend_capturing(in_=in_)
707 def suspend(self, in_: bool = False) -> None:
708 # Need to undo local capsys-et-al if it exists before disabling global capture.
709 self.suspend_fixture()
710 self.suspend_global_capture(in_)
712 def resume(self) -> None:
713 self.resume_global_capture()
714 self.resume_fixture()
716 def read_global_capture(self) -> CaptureResult[str]:
717 assert self._global_capturing is not None
718 return self._global_capturing.readouterr()
720 # Fixture Control
722 def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None:
723 if self._capture_fixture:
724 current_fixture = self._capture_fixture.request.fixturename
725 requested_fixture = capture_fixture.request.fixturename
726 capture_fixture.request.raiseerror(
727 "cannot use {} and {} at the same time".format(
728 requested_fixture, current_fixture
729 )
730 )
731 self._capture_fixture = capture_fixture
733 def unset_fixture(self) -> None:
734 self._capture_fixture = None
736 def activate_fixture(self) -> None:
737 """If the current item is using ``capsys`` or ``capfd``, activate
738 them so they take precedence over the global capture."""
739 if self._capture_fixture:
740 self._capture_fixture._start()
742 def deactivate_fixture(self) -> None:
743 """Deactivate the ``capsys`` or ``capfd`` fixture of this item, if any."""
744 if self._capture_fixture:
745 self._capture_fixture.close()
747 def suspend_fixture(self) -> None:
748 if self._capture_fixture:
749 self._capture_fixture._suspend()
751 def resume_fixture(self) -> None:
752 if self._capture_fixture:
753 self._capture_fixture._resume()
755 # Helper context managers
757 @contextlib.contextmanager
758 def global_and_fixture_disabled(self) -> Generator[None, None, None]:
759 """Context manager to temporarily disable global and current fixture capturing."""
760 do_fixture = self._capture_fixture and self._capture_fixture._is_started()
761 if do_fixture:
762 self.suspend_fixture()
763 do_global = self._global_capturing and self._global_capturing.is_started()
764 if do_global:
765 self.suspend_global_capture()
766 try:
767 yield
768 finally:
769 if do_global:
770 self.resume_global_capture()
771 if do_fixture:
772 self.resume_fixture()
774 @contextlib.contextmanager
775 def item_capture(self, when: str, item: Item) -> Generator[None, None, None]:
776 self.resume_global_capture()
777 self.activate_fixture()
778 try:
779 yield
780 finally:
781 self.deactivate_fixture()
782 self.suspend_global_capture(in_=False)
784 out, err = self.read_global_capture()
785 item.add_report_section(when, "stdout", out)
786 item.add_report_section(when, "stderr", err)
788 # Hooks
790 @hookimpl(hookwrapper=True)
791 def pytest_make_collect_report(self, collector: Collector):
792 if isinstance(collector, File):
793 self.resume_global_capture()
794 outcome = yield
795 self.suspend_global_capture()
796 out, err = self.read_global_capture()
797 rep = outcome.get_result()
798 if out:
799 rep.sections.append(("Captured stdout", out))
800 if err:
801 rep.sections.append(("Captured stderr", err))
802 else:
803 yield
805 @hookimpl(hookwrapper=True)
806 def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
807 with self.item_capture("setup", item):
808 yield
810 @hookimpl(hookwrapper=True)
811 def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
812 with self.item_capture("call", item):
813 yield
815 @hookimpl(hookwrapper=True)
816 def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
817 with self.item_capture("teardown", item):
818 yield
820 @hookimpl(tryfirst=True)
821 def pytest_keyboard_interrupt(self) -> None:
822 self.stop_global_capturing()
824 @hookimpl(tryfirst=True)
825 def pytest_internalerror(self) -> None:
826 self.stop_global_capturing()
829class CaptureFixture(Generic[AnyStr]):
830 """Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,
831 :fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
833 def __init__(
834 self, captureclass, request: SubRequest, *, _ispytest: bool = False
835 ) -> None:
836 check_ispytest(_ispytest)
837 self.captureclass = captureclass
838 self.request = request
839 self._capture: Optional[MultiCapture[AnyStr]] = None
840 self._captured_out = self.captureclass.EMPTY_BUFFER
841 self._captured_err = self.captureclass.EMPTY_BUFFER
843 def _start(self) -> None:
844 if self._capture is None:
845 self._capture = MultiCapture(
846 in_=None,
847 out=self.captureclass(1),
848 err=self.captureclass(2),
849 )
850 self._capture.start_capturing()
852 def close(self) -> None:
853 if self._capture is not None:
854 out, err = self._capture.pop_outerr_to_orig()
855 self._captured_out += out
856 self._captured_err += err
857 self._capture.stop_capturing()
858 self._capture = None
860 def readouterr(self) -> CaptureResult[AnyStr]:
861 """Read and return the captured output so far, resetting the internal
862 buffer.
864 :returns:
865 The captured content as a namedtuple with ``out`` and ``err``
866 string attributes.
867 """
868 captured_out, captured_err = self._captured_out, self._captured_err
869 if self._capture is not None:
870 out, err = self._capture.readouterr()
871 captured_out += out
872 captured_err += err
873 self._captured_out = self.captureclass.EMPTY_BUFFER
874 self._captured_err = self.captureclass.EMPTY_BUFFER
875 return CaptureResult(captured_out, captured_err)
877 def _suspend(self) -> None:
878 """Suspend this fixture's own capturing temporarily."""
879 if self._capture is not None:
880 self._capture.suspend_capturing()
882 def _resume(self) -> None:
883 """Resume this fixture's own capturing temporarily."""
884 if self._capture is not None:
885 self._capture.resume_capturing()
887 def _is_started(self) -> bool:
888 """Whether actively capturing -- not disabled or closed."""
889 if self._capture is not None:
890 return self._capture.is_started()
891 return False
893 @contextlib.contextmanager
894 def disabled(self) -> Generator[None, None, None]:
895 """Temporarily disable capturing while inside the ``with`` block."""
896 capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
897 with capmanager.global_and_fixture_disabled():
898 yield
901# The fixtures.
904@fixture
905def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
906 r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
908 The captured output is made available via ``capsys.readouterr()`` method
909 calls, which return a ``(out, err)`` namedtuple.
910 ``out`` and ``err`` will be ``text`` objects.
912 Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
914 Example:
916 .. code-block:: python
918 def test_output(capsys):
919 print("hello")
920 captured = capsys.readouterr()
921 assert captured.out == "hello\n"
922 """
923 capman = request.config.pluginmanager.getplugin("capturemanager")
924 capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
925 capman.set_fixture(capture_fixture)
926 capture_fixture._start()
927 yield capture_fixture
928 capture_fixture.close()
929 capman.unset_fixture()
932@fixture
933def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
934 r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
936 The captured output is made available via ``capsysbinary.readouterr()``
937 method calls, which return a ``(out, err)`` namedtuple.
938 ``out`` and ``err`` will be ``bytes`` objects.
940 Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
942 Example:
944 .. code-block:: python
946 def test_output(capsysbinary):
947 print("hello")
948 captured = capsysbinary.readouterr()
949 assert captured.out == b"hello\n"
950 """
951 capman = request.config.pluginmanager.getplugin("capturemanager")
952 capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
953 capman.set_fixture(capture_fixture)
954 capture_fixture._start()
955 yield capture_fixture
956 capture_fixture.close()
957 capman.unset_fixture()
960@fixture
961def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
962 r"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
964 The captured output is made available via ``capfd.readouterr()`` method
965 calls, which return a ``(out, err)`` namedtuple.
966 ``out`` and ``err`` will be ``text`` objects.
968 Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
970 Example:
972 .. code-block:: python
974 def test_system_echo(capfd):
975 os.system('echo "hello"')
976 captured = capfd.readouterr()
977 assert captured.out == "hello\n"
978 """
979 capman = request.config.pluginmanager.getplugin("capturemanager")
980 capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
981 capman.set_fixture(capture_fixture)
982 capture_fixture._start()
983 yield capture_fixture
984 capture_fixture.close()
985 capman.unset_fixture()
988@fixture
989def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
990 r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
992 The captured output is made available via ``capfd.readouterr()`` method
993 calls, which return a ``(out, err)`` namedtuple.
994 ``out`` and ``err`` will be ``byte`` objects.
996 Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
998 Example:
1000 .. code-block:: python
1002 def test_system_echo(capfdbinary):
1003 os.system('echo "hello"')
1004 captured = capfdbinary.readouterr()
1005 assert captured.out == b"hello\n"
1007 """
1008 capman = request.config.pluginmanager.getplugin("capturemanager")
1009 capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
1010 capman.set_fixture(capture_fixture)
1011 capture_fixture._start()
1012 yield capture_fixture
1013 capture_fixture.close()
1014 capman.unset_fixture()