From baed905c0b1a8381bd97167a691096e206f1a168 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Wed, 17 Jan 2024 15:44:38 +0100
Subject: [PATCH] Move scheduling of fixture finalization so it isn't
rescheduled if the fixture value is cached.
---
src/_pytest/fixtures.py | 29 ++++++++++------
.../test_scope_fixture_teardown_order.py | 33 +++++++++++++++++++
2 files changed, 52 insertions(+), 10 deletions(-)
create mode 100644 testing/python/test_scope_fixture_teardown_order.py
diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py
index c294ec586..5edb7c4a3 100644
--- a/src/_pytest/fixtures.py
+++ b/src/_pytest/fixtures.py
@@ -538,6 +538,8 @@ class FixtureRequest(abc.ABC):
: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 @@ class FixtureRequest(abc.ABC):
"""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)
@@ -641,11 +642,8 @@ class FixtureRequest(abc.ABC):
# Check if a higher-level scoped fixture accesses a lower level one.
subrequest._check_scope(argname, self._scope, scope)
- try:
- # Call the fixture function.
- fixturedef.execute(request=subrequest)
- finally:
- self._schedule_finalizers(fixturedef, subrequest)
+ # 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,7 +1053,9 @@ class FixtureDef(Generic[FixtureValue]):
self.cached_result = None
self._finalizers.clear()
+ # Note: the return value is entirely unused, no tests depend on it
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:
@@ -1063,7 +1063,7 @@ class FixtureDef(Generic[FixtureValue]):
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 @@ class FixtureDef(Generic[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 @@ class FixtureDef(Generic[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
return result
def cache_key(self, request: SubRequest) -> object:
diff --git a/testing/python/test_scope_fixture_teardown_order.py b/testing/python/test_scope_fixture_teardown_order.py
new file mode 100644
index 000000000..fdff19af2
--- /dev/null
+++ b/testing/python/test_scope_fixture_teardown_order.py
@@ -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