Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/4871.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Function-scoped fixtures no longer register their finalizer with the higher-scoped fixtures they request, so finalizers no longer accumulate in fixtures such as ``tmp_path_factory`` for every test that uses ``tmp_path``.
9 changes: 7 additions & 2 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1273,8 +1273,13 @@ def execute(self, request: SubRequest) -> FixtureValue:
# We make sure to do this after checking for cached value to avoid
# adding our finalizer multiple times. (#12135)
finalizer = functools.partial(self.finish, request=request)
for parent_fixture in requested_fixtures_that_should_finalize_us:
parent_fixture.addfinalizer(finalizer)
# Function-scoped fixtures are always torn down by SetupState at the end
# of each test, before any higher-scoped fixture can be torn down or
# invalidated, so registering with requested fixtures is not needed and
# would only accumulate finalizers on them (#4871).
if self._scope is not Scope.Function:
for parent_fixture in requested_fixtures_that_should_finalize_us:
parent_fixture.addfinalizer(finalizer)

# Register the pytest_fixture_post_finalizer as the first finalizer,
# which is executed last.
Expand Down
65 changes: 65 additions & 0 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -5688,6 +5688,71 @@ def test_result():
assert result.ret == 0


def test_function_scoped_fixture_does_not_accumulate_finalizers(
pytester: Pytester,
) -> None:
"""
Function-scoped fixtures should not register their finalizer with the
higher-scoped fixtures they request, otherwise one finalizer accumulates
there for every test (#4871).
"""
pytester.makepyfile(
"""
import pytest

@pytest.fixture(scope="session")
def session_fixture():
...

@pytest.fixture
def function_fixture(session_fixture):
...

@pytest.mark.parametrize("i", range(5))
def test_it(i, function_fixture, request):
fixturedef = request._get_active_fixturedef("session_fixture")
# Only the pytest_fixture_post_finalizer registered by the
# session fixture itself.
assert len(fixturedef._finalizers) == 1
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=5)


def test_fixture_invalidated_by_wider_scoped_param(pytester: Pytester) -> None:
"""
A class-scoped fixture requesting a parametrized module-scoped fixture must
be torn down when the parameter changes, even though the class node is
still active (#4871).
"""
pytester.makepyfile(
"""
import pytest

values = []

@pytest.fixture(scope="module", params=["a", "b"])
def module_param(request):
return request.param

@pytest.fixture(scope="class")
def class_fixture(module_param):
values.append(module_param)
return module_param

class TestIt:
def test_it(self, module_param, class_fixture):
assert class_fixture == module_param

def test_values():
assert values == ["a", "b"]
"""
)
result = pytester.runpytest()
result.assert_outcomes(passed=3)


def test_parametrized_fixture_scope_allowed(pytester: Pytester) -> None:
"""
Make sure scope from parametrize does not affect fixture's ability to be
Expand Down
Loading