Add capteesys capture fixture to bubble up output to --capture handler#12854
Merged
nicoddemus merged 12 commits intopytest-dev:mainfrom Mar 1, 2025
geopozo:andrew/add_capteesys_fixture
Merged
Add capteesys capture fixture to bubble up output to --capture handler#12854nicoddemus merged 12 commits intopytest-dev:mainfrom geopozo:andrew/add_capteesys_fixture
--capture handler#12854nicoddemus merged 12 commits intopytest-dev:mainfrom
geopozo:andrew/add_capteesys_fixture
Conversation
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.
Contributor
Author
|
It may be better to use "impl_kwargs" instead of "config" or something, in the future maybe have a "impl_args" that expands with * to allow positional arguments and a "impl_kwargs" that expands with **. |
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!
ayjayt
commented
Oct 5, 2024
|
|
||
|
|
||
| @fixture | ||
| def capteesys(request: SubRequest) -> Generator[CaptureFixture[str]]: |
Contributor
Author
There was a problem hiding this comment.
this fixture is almost 100% a copy of the capsys one above it, since a tee-sys is just a sys w/ an extra argument (tree=True)
ayjayt
commented
Oct 5, 2024
ayjayt
commented
Oct 5, 2024
Contributor
Author
|
I've created a polyfill to add to @pytest.fixture(scope="function")
def capteesys(request):
from _pytest import capture
import warnings
if hasattr(capture, "capteesys"):
warnings.warn(( "You are using a polyfill for capteesys, but this"
" version of pytest supports it natively- you may"
f" remove the polyfill from your {__file__}"),
DeprecationWarning)
# Remove next two lines if you don't want to ever switch to native version
yield request.getfixturevalue("capteesys")
return
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = capture.CaptureFixture(capture.SysCapture, request, _ispytest=True)
def _inject_start():
self = capture_fixture # closure seems easier than importing Type or Partial
if self._capture is None:
self._capture = capture.MultiCapture(
in_ = None,
out = self.captureclass(1, tee=True),
err = self.captureclass(2, tee=True)
)
self._capture.start_capturing()
capture_fixture._start = _inject_start
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
capture_fixture.close()
capman.unset_fixture() |
|
👍 this would be very useful (albeit in rather esoteric contexts :-) ) |
nicoddemus
approved these changes
Mar 1, 2025
Member
nicoddemus
left a comment
There was a problem hiding this comment.
Looks good, thanks @geopozo!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
potentially closes #12081(and more!)
closes #XYZWto the PR description and/or commits (whereXYZWis the issue number). See the github docs for more information.changelogfolder, with a name like<ISSUE NUMBER>.<TYPE>.rst. See changelog/README.rst for details.AUTHORSin alphabetical order.Explanation
To set global capture behavior, we set
--capture=tee-sysso that pytest can a) capture output for printing reports and b) dump output to our terminals. Both of these things are useful.Passing capture-type fixtures in our tests will block global capture behavior, but allow us to analyze captured output inside the test.
Unfortunately, capture fixtures don't support anything like
--capture=tee-sys- until now. This PR adds a new fixture:capteesyswhich captures output for analysis in tests but also pushes the output to whatever is set by--capture=. It allows the output to "bubble up" to the next capture handler.Docs build, tests pass.
My Approach (Engineering)
How the system works right now:
Right now, when you include a fixture (
syscap,fdcap, etc), that fixture initializes theCaptureFixturewrapper and passes it the class of the implementation it wants to use, one of:which the wrapper will instantiate as an object later (depending on fixture scope) as such:
There is no current way to pass other options to the constructor, which would enable other features, such as
tee.Solution:
Discarded Solutions
Make that each Capture class accept a
teeargument- most will discard it (FDCaptureprobably could accept it). Most of the time it would be nonsensical and maybe misleading to developers or users.Create a conditional switch for the
CaptureFixturewrapper which changes the initialization process based on the actual constructor- But it seems bad to create individual branches in base/containing classes for all possible children/attributes.Chosen Solution
Since the
CaptureFixtureclass is already initialized by passing the class of the implementation, you just add a new configure dictionary:Which is then expanded when initializing the implementation:
It requires very few code changes. This explanation was longer.
To create the
capteesysfixture, I just copied thecapsysfixture but added the config dictionary as part of its setup.