From e4ba7cb4bd1ca1fc31b1fffbcd961328d7b58a54 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 4 Oct 2024 20:37:31 -0400 Subject: [PATCH 1/9] Allow CaptureFixture() to init() with config dict The config dict is passed alongside the class that the fixture will eventually initialize. It can use the config dict for optional arguments to the implementation's constructor. --- src/_pytest/capture.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 506c0b3d287..adf319d514c 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -902,11 +902,13 @@ def __init__( captureclass: type[CaptureBase[AnyStr]], request: SubRequest, *, + config: dict[str,any] | None = None, _ispytest: bool = False, ) -> None: check_ispytest(_ispytest) self.captureclass: type[CaptureBase[AnyStr]] = captureclass self.request = request + self._config = config if config else {} self._capture: MultiCapture[AnyStr] | None = None self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER @@ -915,8 +917,8 @@ def _start(self) -> None: if self._capture is None: self._capture = MultiCapture( in_=None, - out=self.captureclass(1), - err=self.captureclass(2), + out=self.captureclass(1, **self._config), + err=self.captureclass(2, **self._config), ) self._capture.start_capturing() From da5a0e94ed836b48ca56155e3d5940d34491640e Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 4 Oct 2024 20:40:21 -0400 Subject: [PATCH 2/9] Add capteesys fixture equal to --capture=tee-sys --- src/_pytest/capture.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index adf319d514c..f7fde39b0f2 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -902,7 +902,7 @@ def __init__( captureclass: type[CaptureBase[AnyStr]], request: SubRequest, *, - config: dict[str,any] | None = None, + config: dict[str, Any] | None = None, _ispytest: bool = False, ) -> None: check_ispytest(_ispytest) @@ -1004,6 +1004,40 @@ def test_output(capsys): capman.unset_fixture() +@fixture +def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]: + r"""Enable simultaneous text capturing and pass-through of writes + to ``sys.stdout`` and ``sys.stderr``. + + + The captured output is made available via ``capteesys.readouterr()`` method + calls, which return a ``(out, err)`` namedtuple. + ``out`` and ``err`` will be ``text`` objects. + + The output is also passed-through, allowing it to be "live-printed". + + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_output(capsys): + print("hello") + captured = capteesys.readouterr() + assert captured.out == "hello\n" + """ + capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager") + capture_fixture = CaptureFixture( + SysCapture, request, config=dict(tee=True), _ispytest=True + ) + capman.set_fixture(capture_fixture) + capture_fixture._start() + yield capture_fixture + capture_fixture.close() + capman.unset_fixture() + + @fixture def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]: r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. From b5ae7b4f2e3ee3c7bf8f42ae7b30b224359cc254 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 4 Oct 2024 21:03:30 -0400 Subject: [PATCH 3/9] Improve documentation around capture --- doc/en/how-to/capture-stdout-stderr.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/doc/en/how-to/capture-stdout-stderr.rst b/doc/en/how-to/capture-stdout-stderr.rst index 9f7ddce3499..a991aeffea6 100644 --- a/doc/en/how-to/capture-stdout-stderr.rst +++ b/doc/en/how-to/capture-stdout-stderr.rst @@ -4,6 +4,15 @@ How to capture stdout/stderr output ========================================================= +Pytest can intercept stdout and stderr. Using the `--capture=` command-line +argument (described below) allows pytest to add captured output to reports +generated after tests. The reports can customized by `-r` (described below). + +Built-in fixtures (also described below) can be passed to tests as arguments +to capture stdout and stderr and make it accessible to the developer during +the test for inspection. It will not be available for reports if it is captured, +unless it is re-printed with capturing turned off (see below). + Default stdout/stderr/stdin capturing behaviour --------------------------------------------------------- @@ -106,8 +115,8 @@ of the failing function and hide the other one: Accessing captured output from a test function --------------------------------------------------- -The :fixture:`capsys`, :fixture:`capsysbinary`, :fixture:`capfd`, and :fixture:`capfdbinary` fixtures -allow access to ``stdout``/``stderr`` output created during test execution. +The :fixture:`capsys`, :fixture:`capteesys`, :fixture:`capsysbinary`, :fixture:`capfd`, and :fixture:`capfdbinary` +fixtures allow access to ``stdout``/``stderr`` output created during test execution. Here is an example test function that performs some output related checks: From 76c438720a0d0437c27fc524252d44a654ef39ed Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Fri, 4 Oct 2024 23:33:41 -0400 Subject: [PATCH 4/9] Add test for capteesys --- testing/test_capture.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/testing/test_capture.py b/testing/test_capture.py index 328de740e8a..7c3876045fa 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -445,6 +445,25 @@ def test_hello(capsys): ) reprec.assertoutcome(passed=1) + def test_capteesys(self, pytester: Pytester) -> None: + p = pytester.makepyfile( + """\ + import sys + def test_one(capteesys): + print("sTdoUt") + print("sTdeRr", file=sys.stderr) + out, err = capteesys.readouterr() + assert out == "sTdoUt\\n" + assert err == "sTdeRr\\n" + """ + ) + # -rN and --capture=tee-sys means we'll read them on stdout/stderr, + # as opposed to both being reported on stdout + result = pytester.runpytest(p, "--quiet", "--quiet", "-rN", "--capture=tee-sys") + assert result.ret == ExitCode.OK + result.stdout.fnmatch_lines(["sTdoUt"]) + result.stderr.fnmatch_lines(["sTdeRr"]) + def test_capsyscapfd(self, pytester: Pytester) -> None: p = pytester.makepyfile( """\ From 69b4c87922275ba96b9fcfad3974c7ac16bd6f3f Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sat, 5 Oct 2024 00:28:18 -0400 Subject: [PATCH 5/9] Amend docs to better report new behavior: When I started this feature, I expected passing the "tee" flag to "syscap" would copy output directly to the calling terminal, to the original ORIGINAL sys.stdout/sys.stderr. That's not how it works- it passes output to whatever capture was set earlier! This actually makes the feature more flexible because it enables tee-like behavior as well as pytests reporting behavior. Awesome! --- doc/en/how-to/capture-stdout-stderr.rst | 12 ++++-------- doc/en/reference/fixtures.rst | 4 ++++ doc/en/reference/reference.rst | 10 ++++++++++ src/_pytest/capture.py | 5 +++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/doc/en/how-to/capture-stdout-stderr.rst b/doc/en/how-to/capture-stdout-stderr.rst index a991aeffea6..9f57eceb16e 100644 --- a/doc/en/how-to/capture-stdout-stderr.rst +++ b/doc/en/how-to/capture-stdout-stderr.rst @@ -4,14 +4,10 @@ How to capture stdout/stderr output ========================================================= -Pytest can intercept stdout and stderr. Using the `--capture=` command-line -argument (described below) allows pytest to add captured output to reports -generated after tests. The reports can customized by `-r` (described below). - -Built-in fixtures (also described below) can be passed to tests as arguments -to capture stdout and stderr and make it accessible to the developer during -the test for inspection. It will not be available for reports if it is captured, -unless it is re-printed with capturing turned off (see below). +Pytest can intercept stdout and stderr by using the ``--capture=`` command-line +or argument or by using fixtures. The flag ``--capture=`` configures reporting, +whereas the fixtures offer somewhat more granular control and allow inspection +during testing. The reports can customized with the `-r flag <../reference/reference.html#command-line-flags>`_. Default stdout/stderr/stdin capturing behaviour --------------------------------------------------------- diff --git a/doc/en/reference/fixtures.rst b/doc/en/reference/fixtures.rst index dff93a035ef..566304d3330 100644 --- a/doc/en/reference/fixtures.rst +++ b/doc/en/reference/fixtures.rst @@ -32,6 +32,10 @@ Built-in fixtures :fixture:`capsys` Capture, as text, output to ``sys.stdout`` and ``sys.stderr``. + :fixture:`capteesys` + Capture in the same manner as :fixture:`capsys`, but also pass text + through according to ``--capture=``. + :fixture:`capsysbinary` Capture, as bytes, output to ``sys.stdout`` and ``sys.stderr``. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 3bb03cc0386..4ddded7cedc 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -402,6 +402,16 @@ capsys .. autoclass:: pytest.CaptureFixture() :members: +.. fixture:: capteesys + +capteesys +~~~~~~~~~ + +**Tutorial**: :ref:`captures` + +.. autofunction:: _pytest.capture.capteesys() + :no-auto-options: + .. fixture:: capsysbinary capsysbinary diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index f7fde39b0f2..2fa668508cd 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -1007,14 +1007,15 @@ def test_output(capsys): @fixture def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]: r"""Enable simultaneous text capturing and pass-through of writes - to ``sys.stdout`` and ``sys.stderr``. + to ``sys.stdout`` and ``sys.stderr`` as defined by ``--capture=``. The captured output is made available via ``capteesys.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. - The output is also passed-through, allowing it to be "live-printed". + The output is also passed-through, allowing it to be "live-printed", + reported, or both as defined by ``--capture=``. Returns an instance of :class:`CaptureFixture[str] `. From 60c1bcedd78d5e88d4ad48e0b2805880668bd41d Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sat, 5 Oct 2024 00:40:37 -0400 Subject: [PATCH 6/9] Add changelog + author for fixture capteesys --- AUTHORS | 1 + changelog/12081.feature.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/12081.feature.rst diff --git a/AUTHORS b/AUTHORS index 1ee868448d4..a28b1feba99 100644 --- a/AUTHORS +++ b/AUTHORS @@ -31,6 +31,7 @@ Andras Tim Andrea Cimatoribus Andreas Motl Andreas Zeidler +Andrew Pikul Andrew Shapton Andrey Paramonov Andrzej Klajnert diff --git a/changelog/12081.feature.rst b/changelog/12081.feature.rst new file mode 100644 index 00000000000..3822d60ceb8 --- /dev/null +++ b/changelog/12081.feature.rst @@ -0,0 +1 @@ +Added :fixture:`capteesys` to capture AND feed output to capture handler set by ``--capture=``. From 82afa8f87d7919405925053490dd64525d5f8ac7 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sat, 5 Oct 2024 01:16:09 -0400 Subject: [PATCH 7/9] Rewrite capture how-to for readability --- changelog/12081.feature.rst | 2 +- doc/en/how-to/capture-stdout-stderr.rst | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/changelog/12081.feature.rst b/changelog/12081.feature.rst index 3822d60ceb8..6538fbf30f8 100644 --- a/changelog/12081.feature.rst +++ b/changelog/12081.feature.rst @@ -1 +1 @@ -Added :fixture:`capteesys` to capture AND feed output to capture handler set by ``--capture=``. +Added :fixture:`capteesys` to capture AND pass output to next handler set by ``--capture=``. diff --git a/doc/en/how-to/capture-stdout-stderr.rst b/doc/en/how-to/capture-stdout-stderr.rst index 9f57eceb16e..8f2a1a46680 100644 --- a/doc/en/how-to/capture-stdout-stderr.rst +++ b/doc/en/how-to/capture-stdout-stderr.rst @@ -4,10 +4,11 @@ How to capture stdout/stderr output ========================================================= -Pytest can intercept stdout and stderr by using the ``--capture=`` command-line -or argument or by using fixtures. The flag ``--capture=`` configures reporting, -whereas the fixtures offer somewhat more granular control and allow inspection -during testing. The reports can customized with the `-r flag <../reference/reference.html#command-line-flags>`_. +Pytest intercepts stdout and stderr as configured by the ``--capture=`` +command-line argument or by using fixtures. The ``--capture=`` flag configures +reporting, whereas the fixtures offer more granular control and allows +inspection of output during testing. The reports can be customized with the +`-r flag <../reference/reference.html#command-line-flags>`_. Default stdout/stderr/stdin capturing behaviour --------------------------------------------------------- From 740b951cced2dbc7feb814578717405dd8f737a7 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sat, 5 Oct 2024 10:48:57 -0400 Subject: [PATCH 8/9] Test capteesys behavior under various --capture= --- testing/test_capture.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testing/test_capture.py b/testing/test_capture.py index 7c3876045fa..f71ca842045 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -464,6 +464,12 @@ def test_one(capteesys): result.stdout.fnmatch_lines(["sTdoUt"]) result.stderr.fnmatch_lines(["sTdeRr"]) + # -rA and --capture=sys means we'll read them on stdout. + result = pytester.runpytest(p, "--quiet", "--quiet", "-rA", "--capture=sys") + assert result.ret == ExitCode.OK + result.stdout.fnmatch_lines(["*sTdoUt*", "*sTdeRr*"]) + assert not result.stderr.lines + def test_capsyscapfd(self, pytester: Pytester) -> None: p = pytester.makepyfile( """\ From 9cd57b1bd6455c22cb3037086d9db69d14636f54 Mon Sep 17 00:00:00 2001 From: Andrew Pikul Date: Sun, 6 Oct 2024 23:24:18 -0400 Subject: [PATCH 9/9] Test another fixture/--capture configuration --- testing/test_capture.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index f71ca842045..ac43ae1ea5d 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -461,13 +461,20 @@ def test_one(capteesys): # as opposed to both being reported on stdout result = pytester.runpytest(p, "--quiet", "--quiet", "-rN", "--capture=tee-sys") assert result.ret == ExitCode.OK - result.stdout.fnmatch_lines(["sTdoUt"]) - result.stderr.fnmatch_lines(["sTdeRr"]) + result.stdout.fnmatch_lines(["sTdoUt"]) # tee'd out + result.stderr.fnmatch_lines(["sTdeRr"]) # tee'd out + + result = pytester.runpytest(p, "--quiet", "--quiet", "-rA", "--capture=tee-sys") + assert result.ret == ExitCode.OK + result.stdout.fnmatch_lines( + ["sTdoUt", "sTdoUt", "sTdeRr"] + ) # tee'd out, the next two reported + result.stderr.fnmatch_lines(["sTdeRr"]) # tee'd out # -rA and --capture=sys means we'll read them on stdout. result = pytester.runpytest(p, "--quiet", "--quiet", "-rA", "--capture=sys") assert result.ret == ExitCode.OK - result.stdout.fnmatch_lines(["*sTdoUt*", "*sTdeRr*"]) + result.stdout.fnmatch_lines(["sTdoUt", "sTdeRr"]) # no tee, just reported assert not result.stderr.lines def test_capsyscapfd(self, pytester: Pytester) -> None: