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
3 changes: 3 additions & 0 deletions changelog/15067.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixture finalizers registered with ``request.addfinalizer()`` now run even if
fixture setup is interrupted after registration, including by
``KeyboardInterrupt`` or an exception from a ``pytest_fixture_setup`` hook.
5 changes: 2 additions & 3 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1203,9 +1203,8 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
self._finalizers.append(finalizer)

def finish(self, request: SubRequest) -> None:
if self.cached_result is None:
# Already finished. It is assumed that finalizers cannot be added in
# this state.
# Setup may have failed before caching a result, but still added finalizers.
if self.cached_result is None and not self._finalizers:
return

exceptions: list[BaseException] = []
Expand Down
54 changes: 54 additions & 0 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,60 @@ def test_finalizer_ran():
reprec = pytester.inline_run("-s")
reprec.assertoutcome(failed=1, passed=1)

def test_request_addfinalizer_interrupted_setup(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
from pathlib import Path
import pytest

@pytest.fixture
def resource(request):
request.addfinalizer(
lambda: Path("finalizer-ran").write_text("ran", encoding="utf-8")
)
raise KeyboardInterrupt

def test_setup(resource):
pass
"""
)

result = pytester.runpytest_subprocess("-q")
assert result.ret == ExitCode.INTERRUPTED
assert (pytester.path / "finalizer-ran").read_text(encoding="utf-8") == "ran"

def test_request_addfinalizer_interrupted_setup_hook(
self, pytester: Pytester
) -> None:
pytester.makeconftest(
"""
from pathlib import Path

def pytest_fixture_setup(fixturedef, request):
if fixturedef.argname == "resource":
request.addfinalizer(
lambda: Path("finalizer-ran").write_text("ran", encoding="utf-8")
)
raise KeyboardInterrupt
"""
)
pytester.makepyfile(
"""
import pytest

@pytest.fixture
def resource():
pass

def test_setup(resource):
pass
"""
)

result = pytester.runpytest_subprocess("-q")
assert result.ret == ExitCode.INTERRUPTED
assert (pytester.path / "finalizer-ran").read_text(encoding="utf-8") == "ran"

def test_request_addfinalizer_failing_setup_module(
self, pytester: Pytester
) -> None:
Expand Down
Loading