Skip to content

Commit 70c1158

Browse files
authored
Don't add fixture finalizer if the value is cached (#11833)
Fixes #1489
1 parent c203f16 commit 70c1158

File tree

3 files changed

+75
-31
lines changed

3 files changed

+75
-31
lines changed

changelog/1489.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix some instances where teardown of higher-scoped fixtures was not happening in the reverse order they were initialized in.

src/_pytest/fixtures.py

+19-31
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,11 @@ def getfixturevalue(self, argname: str) -> Any:
543543
:raises pytest.FixtureLookupError:
544544
If the given fixture could not be found.
545545
"""
546+
# Note that in addition to the use case described in the docstring,
547+
# getfixturevalue() is also called by pytest itself during item and fixture
548+
# setup to evaluate the fixtures that are requested statically
549+
# (using function parameters, autouse, etc).
550+
546551
fixturedef = self._get_active_fixturedef(argname)
547552
assert fixturedef.cached_result is not None, (
548553
f'The fixture value for "{argname}" is not available. '
@@ -587,9 +592,8 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
587592
"""Create a SubRequest based on "self" and call the execute method
588593
of the given FixtureDef object.
589594
590-
This will force the FixtureDef object to throw away any previous
591-
results and compute a new fixture value, which will be stored into
592-
the FixtureDef object itself.
595+
If the FixtureDef has cached the result it will do nothing, otherwise it will
596+
setup and run the fixture, cache the value, and schedule a finalizer for it.
593597
"""
594598
# prepare a subrequest object before calling fixture function
595599
# (latter managed by fixturedef)
@@ -646,18 +650,9 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
646650
subrequest = SubRequest(
647651
self, scope, param, param_index, fixturedef, _ispytest=True
648652
)
649-
try:
650-
# Call the fixture function.
651-
fixturedef.execute(request=subrequest)
652-
finally:
653-
self._schedule_finalizers(fixturedef, subrequest)
654653

655-
def _schedule_finalizers(
656-
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
657-
) -> None:
658-
# If fixture function failed it might have registered finalizers.
659-
finalizer = functools.partial(fixturedef.finish, request=subrequest)
660-
subrequest.node.addfinalizer(finalizer)
654+
# Make sure the fixture value is cached, running it if it isn't
655+
fixturedef.execute(request=subrequest)
661656

662657

663658
@final
@@ -788,21 +783,6 @@ def _format_fixturedef_line(self, fixturedef: "FixtureDef[object]") -> str:
788783
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
789784
self._fixturedef.addfinalizer(finalizer)
790785

791-
def _schedule_finalizers(
792-
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
793-
) -> None:
794-
# If the executing fixturedef was not explicitly requested in the argument list (via
795-
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
796-
# first.
797-
if (
798-
fixturedef.argname not in self._fixture_defs
799-
and fixturedef.argname not in self._pyfuncitem.fixturenames
800-
):
801-
fixturedef.addfinalizer(
802-
functools.partial(self._fixturedef.finish, request=self)
803-
)
804-
super()._schedule_finalizers(fixturedef, subrequest)
805-
806786

807787
@final
808788
class FixtureLookupError(LookupError):
@@ -1055,12 +1035,13 @@ def finish(self, request: SubRequest) -> None:
10551035
raise BaseExceptionGroup(msg, exceptions[::-1])
10561036

10571037
def execute(self, request: SubRequest) -> FixtureValue:
1038+
finalizer = functools.partial(self.finish, request=request)
10581039
# Get required arguments and register our own finish()
10591040
# with their finalization.
10601041
for argname in self.argnames:
10611042
fixturedef = request._get_active_fixturedef(argname)
10621043
if not isinstance(fixturedef, PseudoFixtureDef):
1063-
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
1044+
fixturedef.addfinalizer(finalizer)
10641045

10651046
my_cache_key = self.cache_key(request)
10661047
if self.cached_result is not None:
@@ -1080,7 +1061,14 @@ def execute(self, request: SubRequest) -> FixtureValue:
10801061
assert self.cached_result is None
10811062

10821063
ihook = request.node.ihook
1083-
result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
1064+
try:
1065+
# Setup the fixture, run the code in it, and cache the value
1066+
# in self.cached_result
1067+
result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
1068+
finally:
1069+
# schedule our finalizer, even if the setup failed
1070+
request.node.addfinalizer(finalizer)
1071+
10841072
return result
10851073

10861074
def cache_key(self, request: SubRequest) -> object:

testing/python/fixtures.py

+55
Original file line numberDiff line numberDiff line change
@@ -4696,3 +4696,58 @@ def test_crash_expected_setup_and_teardown() -> None:
46964696
)
46974697
result = pytester.runpytest()
46984698
assert result.ret == 0
4699+
4700+
4701+
def test_scoped_fixture_teardown_order(pytester: Pytester) -> None:
4702+
"""
4703+
Make sure teardowns happen in reverse order of setup with scoped fixtures, when
4704+
a later test only depends on a subset of scoped fixtures.
4705+
4706+
Regression test for https://github.com/pytest-dev/pytest/issues/1489
4707+
"""
4708+
pytester.makepyfile(
4709+
"""
4710+
from typing import Generator
4711+
4712+
import pytest
4713+
4714+
4715+
last_executed = ""
4716+
4717+
4718+
@pytest.fixture(scope="module")
4719+
def fixture_1() -> Generator[None, None, None]:
4720+
global last_executed
4721+
assert last_executed == ""
4722+
last_executed = "fixture_1_setup"
4723+
yield
4724+
assert last_executed == "fixture_2_teardown"
4725+
last_executed = "fixture_1_teardown"
4726+
4727+
4728+
@pytest.fixture(scope="module")
4729+
def fixture_2() -> Generator[None, None, None]:
4730+
global last_executed
4731+
assert last_executed == "fixture_1_setup"
4732+
last_executed = "fixture_2_setup"
4733+
yield
4734+
assert last_executed == "run_test"
4735+
last_executed = "fixture_2_teardown"
4736+
4737+
4738+
def test_fixture_teardown_order(fixture_1: None, fixture_2: None) -> None:
4739+
global last_executed
4740+
assert last_executed == "fixture_2_setup"
4741+
last_executed = "run_test"
4742+
4743+
4744+
def test_2(fixture_1: None) -> None:
4745+
# This would previously queue an additional teardown of fixture_1,
4746+
# despite fixture_1's value being cached, which caused fixture_1 to be
4747+
# torn down before fixture_2 - violating the rule that teardowns should
4748+
# happen in reverse order of setup.
4749+
pass
4750+
"""
4751+
)
4752+
result = pytester.runpytest()
4753+
assert result.ret == 0

0 commit comments

Comments
 (0)