-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Don't add fixture finalizer if the value is cached #11833
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
baed905
3356fa7
6e5a8fe
fa9607e
5eddb50
f76a77b
537a831
d168ea4
fbe15ca
b3928bf
3fc5c55
007d24a
fa853df
73ae964
43f394f
74e0941
13d1049
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Fix some instances where teardown of higher-scoped fixtures was not happening in the reverse order they were initialized in. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -543,6 +543,11 @@ def getfixturevalue(self, argname: str) -> Any: | |
:raises pytest.FixtureLookupError: | ||
If the given fixture could not be found. | ||
""" | ||
# Note that in addition to the use case described in the docstring, | ||
# getfixturevalue() is also called by pytest itself during item and fixture | ||
# setup to evaluate the fixtures that are requested statically | ||
# (using function parameters, autouse, etc). | ||
|
||
fixturedef = self._get_active_fixturedef(argname) | ||
assert fixturedef.cached_result is not None, ( | ||
f'The fixture value for "{argname}" is not available. ' | ||
|
@@ -587,9 +592,8 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: | |
"""Create a SubRequest based on "self" and call the execute method | ||
of the given FixtureDef object. | ||
|
||
This will force the FixtureDef object to throw away any previous | ||
results and compute a new fixture value, which will be stored into | ||
the FixtureDef object itself. | ||
If the FixtureDef has cached the result it will do nothing, otherwise it will | ||
setup and run the fixture, cache the value, and schedule a finalizer for it. | ||
""" | ||
# prepare a subrequest object before calling fixture function | ||
# (latter managed by fixturedef) | ||
|
@@ -646,18 +650,9 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: | |
subrequest = SubRequest( | ||
self, scope, param, param_index, fixturedef, _ispytest=True | ||
) | ||
try: | ||
# Call the fixture function. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like it originally ensured teardown even when setup failed I suspect that we are missing a edge case tests there There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the fixture need finalizing in case we never run But I could change it to try:
fixturedef.execute(...)
except:
self._schedule_finalizers(...)
raise or specifically schedule a finalizer where I made a comment on line 1076 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The specific schedule is slightly better, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On further consideration, changing that seems... bad to me? It means we'll be running the finalizer multiple times for a fixture with a cached exception, even if its setup was only run once. That is how it worked in the past though There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apologies, what I meant is that a single schedule is better than the current mechanism Ideally this logic would move to a data structure instead of the code where it is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right - no yeah I agree that the current implementation is quite messy and would benefit from an overhaul, but that seems out of scope for this PR. The only real change should ™️ be that the finalizer isn't scheduled if the value is cached (regardless of if it's an exception or not). If the setup code for the fixture fails, it's catched by the new There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So is there a problem I should address, or was your comment just a musing on how this should be generally overhauled? Or do you consider that a requirement before modifying any logic? |
||
fixturedef.execute(request=subrequest) | ||
finally: | ||
self._schedule_finalizers(fixturedef, subrequest) | ||
jakkdl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def _schedule_finalizers( | ||
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" | ||
) -> None: | ||
# If fixture function failed it might have registered finalizers. | ||
finalizer = functools.partial(fixturedef.finish, request=subrequest) | ||
subrequest.node.addfinalizer(finalizer) | ||
# Make sure the fixture value is cached, running it if it isn't | ||
fixturedef.execute(request=subrequest) | ||
|
||
|
||
@final | ||
|
@@ -788,21 +783,6 @@ def _format_fixturedef_line(self, fixturedef: "FixtureDef[object]") -> str: | |
def addfinalizer(self, finalizer: Callable[[], object]) -> None: | ||
self._fixturedef.addfinalizer(finalizer) | ||
|
||
def _schedule_finalizers( | ||
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" | ||
) -> None: | ||
# If the executing fixturedef was not explicitly requested in the argument list (via | ||
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished | ||
# first. | ||
if ( | ||
fixturedef.argname not in self._fixture_defs | ||
and fixturedef.argname not in self._pyfuncitem.fixturenames | ||
): | ||
fixturedef.addfinalizer( | ||
functools.partial(self._fixturedef.finish, request=self) | ||
) | ||
super()._schedule_finalizers(fixturedef, subrequest) | ||
|
||
|
||
@final | ||
class FixtureLookupError(LookupError): | ||
|
@@ -1055,12 +1035,13 @@ def finish(self, request: SubRequest) -> None: | |
raise BaseExceptionGroup(msg, exceptions[::-1]) | ||
|
||
def execute(self, request: SubRequest) -> FixtureValue: | ||
finalizer = functools.partial(self.finish, request=request) | ||
# Get required arguments and register our own finish() | ||
# with their finalization. | ||
for argname in self.argnames: | ||
fixturedef = request._get_active_fixturedef(argname) | ||
if not isinstance(fixturedef, PseudoFixtureDef): | ||
fixturedef.addfinalizer(functools.partial(self.finish, request=request)) | ||
fixturedef.addfinalizer(finalizer) | ||
|
||
my_cache_key = self.cache_key(request) | ||
if self.cached_result is not None: | ||
|
@@ -1080,7 +1061,14 @@ def execute(self, request: SubRequest) -> FixtureValue: | |
assert self.cached_result is None | ||
|
||
ihook = request.node.ihook | ||
result = ihook.pytest_fixture_setup(fixturedef=self, request=request) | ||
try: | ||
# Setup the fixture, run the code in it, and cache the value | ||
# in self.cached_result | ||
result = ihook.pytest_fixture_setup(fixturedef=self, request=request) | ||
finally: | ||
# schedule our finalizer, even if the setup failed | ||
request.node.addfinalizer(finalizer) | ||
|
||
return result | ||
|
||
def cache_key(self, request: SubRequest) -> object: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous comment is wrong, however saying that if the FixtureDef has cached the result it does nothing is not right either. It registers finalizers, and recomputes if the cache key no longer matches,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, registering finalizers regardless of if the value is cached seems dumb... if it's cached then we've already registered a finalizer when we computed the value. And this can also cause bad teardown ordering:
this prints
but if we remove
test_3
we get 2-3-3-2.But this is also a different issue+PR
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jakkdl would you mind opening a fresh issue for this case and the one you describe in #11833 (comment)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done: #12134 and #12135