Skip to content
1 change: 1 addition & 0 deletions changelog/12164.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added new hook :hook:`pytest_fixture_teardown` to fully track the completion of fixtures by analogy with :hook:`pytest_fixture_setup`.
2 changes: 2 additions & 0 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,8 @@ Session related reporting hooks:
.. autofunction:: pytest_terminal_summary
.. hook:: pytest_fixture_setup
.. autofunction:: pytest_fixture_setup
.. hook:: pytest_fixture_teardown
.. autofunction:: pytest_fixture_teardown
.. hook:: pytest_fixture_post_finalizer
.. autofunction:: pytest_fixture_post_finalizer
.. hook:: pytest_warning_recorded
Expand Down
43 changes: 25 additions & 18 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -1014,25 +1014,8 @@
self._finalizers.append(finalizer)

def finish(self, request: SubRequest) -> None:
exceptions: list[BaseException] = []
while self._finalizers:
fin = self._finalizers.pop()
try:
fin()
except BaseException as e:
exceptions.append(e)
node = request.node
node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
# Even if finalization fails, we invalidate the cached fixture
# value and remove all finalizers because they may be bound methods
# which will keep instances alive.
self.cached_result = None
self._finalizers.clear()
if len(exceptions) == 1:
raise exceptions[0]
elif len(exceptions) > 1:
msg = f'errors while tearing down fixture "{self.argname}" of {node}'
raise BaseExceptionGroup(msg, exceptions[::-1])
node.ihook.pytest_fixture_teardown(fixturedef=self, request=request)

def execute(self, request: SubRequest) -> FixtureValue:
"""Return the value of this fixture, executing it if not cached."""
Expand Down Expand Up @@ -1150,6 +1133,30 @@
return result


def pytest_fixture_teardown(
fixturedef: FixtureDef[FixtureValue], request: SubRequest
) -> None:
exceptions: list[BaseException] = []
while fixturedef._finalizers:
fin = fixturedef._finalizers.pop()
try:
fin()
except BaseException as e:
exceptions.append(e)

Check warning on line 1145 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L1144-L1145

Added lines #L1144 - L1145 were not covered by tests
node = request.node
node.ihook.pytest_fixture_post_finalizer(fixturedef=fixturedef, request=request)
# Even if finalization fails, we invalidate the cached fixture
# value and remove all finalizers because they may be bound methods
# which will keep instances alive.
fixturedef.cached_result = None
fixturedef._finalizers.clear()
if len(exceptions) == 1:
raise exceptions[0]

Check warning on line 1154 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L1154

Added line #L1154 was not covered by tests
elif len(exceptions) > 1:
msg = f'errors while tearing down fixture "{fixturedef.argname}" of {node}'
raise BaseExceptionGroup(msg, exceptions[::-1])

Check warning on line 1157 in src/_pytest/fixtures.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/fixtures.py#L1156-L1157

Added lines #L1156 - L1157 were not covered by tests


def wrap_function_to_error_out_if_called_directly(
function: FixtureFunction,
fixture_marker: FixtureFunctionMarker,
Expand Down
19 changes: 19 additions & 0 deletions src/_pytest/hookspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,23 @@ def pytest_fixture_setup(
"""


def pytest_fixture_teardown(fixturedef: FixtureDef[Any], request: SubRequest) -> None:
"""Perform fixture teardown execution.

:param fixturdef:
The fixture definition object.
:param request:
The fixture request object.

Use in conftest plugins
=======================

Any conftest file can implement this hook. For a given fixture, only
conftest files in the fixture scope's directory and its parent directories
are consulted.
"""


def pytest_fixture_post_finalizer(
fixturedef: FixtureDef[Any], request: SubRequest
) -> None:
Expand All @@ -904,6 +921,8 @@ def pytest_fixture_post_finalizer(
The fixture definition object.
:param request:
The fixture request object.
:param exception:
An exception raised in the finalisation of the fixtures.

Use in conftest plugins
=======================
Expand Down
59 changes: 59 additions & 0 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4098,6 +4098,65 @@ def test_func(my_fixture):
)


def test_exceptions_in_pytest_fixture_setup_and_pytest_fixture_teardown(
pytester: Pytester,
) -> None:
pytester.makeconftest(
"""
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_setup(fixturedef):
result = yield
print('SETUP EXCEPTION in {0}: {1}'.format(fixturedef.argname, result.exception))
@pytest.hookimpl(hookwrapper=True)
def pytest_fixture_teardown(fixturedef):
result = yield
print('TEARDOWN EXCEPTION in {0}: {1}'.format(fixturedef.argname, result.exception))
"""
)
pytester.makepyfile(
**{
"tests/test_fixture_exceptions.py": """
import pytest

@pytest.fixture(scope='module')
def module_teardown_exception():
yield
raise ValueError('exception in module_teardown_exception')

@pytest.fixture()
def func_teardown_exception():
yield
raise ValueError('exception in func_teardown_exception')

@pytest.fixture()
def func_setup_exception():
raise ValueError('exception in func_setup_exception')

@pytest.mark.usefixtures(
'module_teardown_exception',
'func_teardown_exception',
'func_setup_exception',
)
def test_func():
pass
""",
}
)
result = pytester.runpytest("-s")
assert result.ret == 1
result.stdout.fnmatch_lines(
[
"*SETUP EXCEPTION in module_teardown_exception: None*",
"*SETUP EXCEPTION in func_teardown_exception: None*",
"*SETUP EXCEPTION in func_setup_exception: exception in func_setup_exception*",
"*TEARDOWN EXCEPTION in func_setup_exception: None*",
"*TEARDOWN EXCEPTION in func_teardown_exception: exception in func_teardown_exception*",
"*TEARDOWN EXCEPTION in module_teardown_exception: exception in module_teardown_exception*",
]
)


class TestScopeOrdering:
"""Class of tests that ensure fixtures are ordered based on their scopes (#2405)"""

Expand Down
Loading