add exception spec to pytest_fixture_post_finalizer hook

This commit is contained in:
Aleksandr Brodin 2024-03-26 11:17:59 +07:00
parent 1f001cd105
commit 5df7641107
3 changed files with 74 additions and 7 deletions

View File

@ -1023,17 +1023,23 @@ class FixtureDef(Generic[FixtureValue]):
except BaseException as e:
exceptions.append(e)
node = request.node
node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
if len(exceptions) == 1:
final_exception = exceptions[0]
elif len(exceptions) > 1:
msg = f'errors while tearing down fixture "{self.argname}" of {node}'
final_exception = BaseExceptionGroup(msg, exceptions[::-1])
else:
final_exception = None
node.ihook.pytest_fixture_post_finalizer(
fixturedef=self, request=request, exception=final_exception
)
# 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])
if final_exception:
raise final_exception
def execute(self, request: SubRequest) -> FixtureValue:
"""Return the value of this fixture, executing it if not cached."""

View File

@ -861,7 +861,9 @@ def pytest_fixture_setup(
def pytest_fixture_post_finalizer(
fixturedef: "FixtureDef[Any]", request: "SubRequest"
fixturedef: "FixtureDef[Any]",
request: "SubRequest",
exception: "BaseException | None",
) -> None:
"""Called after fixture teardown, but before the cache is cleared, so
the fixture result ``fixturedef.cached_result`` is still available (not
@ -871,6 +873,8 @@ def pytest_fixture_post_finalizer(
The fixture definition object.
:param request:
The fixture request object.
:param exception:
The list of exceptions received at the end of the fixtures.
Use in conftest plugins
=======================

View File

@ -4014,6 +4014,63 @@ def test_pytest_fixture_setup_and_post_finalizer_hook(pytester: Pytester) -> Non
)
def test_exceptions_in_pytest_fixture_setup_and_post_finalizer_hook(
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))
def pytest_fixture_post_finalizer(fixturedef, exception):
print('TEARDOWN EXCEPTION in {0}: {1}'.format(fixturedef.argname, exception))
"""
)
pytester.makepyfile(
**{
"tests/test_fixture_exceptions.py": """
import pytest
@pytest.fixture(scope='module')
def module_teardown_exeption():
yield
raise ValueError('exeption in module_teardown_exeption')
@pytest.fixture()
def func_teardown_exeption():
yield
raise ValueError('exeption in func_teardown_exeption')
@pytest.fixture()
def func_setup_exeption():
raise ValueError('exeption in func_setup_exeption')
@pytest.mark.usefixtures(
'module_teardown_exeption',
'func_teardown_exeption',
'func_setup_exeption',
)
def test_func():
pass
""",
}
)
result = pytester.runpytest("-s")
assert result.ret == 1
result.stdout.fnmatch_lines(
[
"*SETUP EXCEPTION in module_teardown_exeption: None*",
"*SETUP EXCEPTION in func_teardown_exeption: None*",
"*SETUP EXCEPTION in func_setup_exeption: exeption in func_setup_exeption*",
"*TEARDOWN EXCEPTION in func_setup_exeption: None*",
"*TEARDOWN EXCEPTION in func_teardown_exeption: exeption in func_teardown_exeption*",
"*TEARDOWN EXCEPTION in module_teardown_exeption: exeption in module_teardown_exeption*",
]
)
class TestScopeOrdering:
"""Class of tests that ensure fixtures are ordered based on their scopes (#2405)"""