-
-
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 4 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 |
---|---|---|
|
@@ -538,6 +538,8 @@ def getfixturevalue(self, argname: str) -> Any: | |
:raises pytest.FixtureLookupError: | ||
If the given fixture could not be found. | ||
""" | ||
# Note: This is called during setup for evaluating fixtures defined via | ||
# function arguments as well. | ||
fixturedef = self._get_active_fixturedef(argname) | ||
assert fixturedef.cached_result is not None, ( | ||
f'The fixture value for "{argname}" is not available. ' | ||
|
@@ -574,9 +576,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 | ||
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 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 commentThe 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: import pytest
@pytest.fixture(scope="module")
def fixture_1(request):
...
@pytest.fixture(scope="module")
def fixture_2(fixture_1):
print("setup 2")
yield
print("teardown 2")
@pytest.fixture(scope="module")
def fixture_3(fixture_1):
print("setup 3")
yield
print("teardown 3")
def test_1(fixture_2):
...
def test_2(fixture_3):
...
# this will reschedule fixture_2's finalizer in the parent fixture, causing it to be
# torn down before fixture 3
def test_3(fixture_2):
...
# trigger finalization of fixture_1, otherwise the cleanup would sequence 3&2 before 1 as normal
@pytest.mark.parametrize("fixture_1", [None], indirect=["fixture_1"])
def test_4(fixture_1):
... this prints
but if we remove But this is also a different issue+PR 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. @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 commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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) | ||
|
@@ -641,11 +642,8 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: | |
|
||
# Check if a higher-level scoped fixture accesses a lower level one. | ||
subrequest._check_scope(argname, self._scope, scope) | ||
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
|
||
# Make sure the fixture value is cached, running it if it isn't | ||
fixturedef.execute(request=subrequest) | ||
|
||
def _schedule_finalizers( | ||
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" | ||
|
@@ -1055,15 +1053,17 @@ def finish(self, request: SubRequest) -> None: | |
self.cached_result = None | ||
self._finalizers.clear() | ||
|
||
# Note: the return value is entirely unused, no tests depend on it | ||
jakkdl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 argname != "request": | ||
# PseudoFixtureDef is only for "request". | ||
assert isinstance(fixturedef, FixtureDef) | ||
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: | ||
|
@@ -1073,6 +1073,7 @@ def execute(self, request: SubRequest) -> FixtureValue: | |
if my_cache_key is cache_key: | ||
if self.cached_result[2] is not None: | ||
exc = self.cached_result[2] | ||
# this would previously trigger adding a finalizer. Should it? | ||
raise exc | ||
else: | ||
result = self.cached_result[0] | ||
|
@@ -1083,7 +1084,15 @@ 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) | ||
|
||
# note: unused | ||
jakkdl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return result | ||
|
||
def cache_key(self, request: SubRequest) -> object: | ||
|
jakkdl marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import pytest | ||
|
||
last_executed = "" | ||
|
||
|
||
@pytest.fixture(scope="module") | ||
def fixture_1(): | ||
global last_executed | ||
assert last_executed == "" | ||
last_executed = "autouse_setup" | ||
yield | ||
assert last_executed == "noautouse_teardown" | ||
last_executed = "autouse_teardown" | ||
|
||
|
||
@pytest.fixture(scope="module") | ||
def fixture_2(): | ||
global last_executed | ||
assert last_executed == "autouse_setup" | ||
last_executed = "noautouse_setup" | ||
yield | ||
assert last_executed == "run_test" | ||
last_executed = "noautouse_teardown" | ||
|
||
|
||
def test_autouse_fixture_teardown_order(fixture_1, fixture_2): | ||
global last_executed | ||
assert last_executed == "noautouse_setup" | ||
last_executed = "run_test" | ||
|
||
|
||
def test_2(fixture_1): | ||
pass |
Uh oh!
There was an error while loading. Please reload this page.