Skip to content

Commit 434282e

Browse files
committed
fixtures: use exception group when multiple finalizers raise in fixture teardown
Previously, if more than one fixture finalizer raised, only the first was reported, and the other errors were lost. Use an exception group to report them all. This is similar to the change we made in node teardowns (in `SetupState`).
1 parent 6ed0051 commit 434282e

File tree

3 files changed

+38
-24
lines changed

3 files changed

+38
-24
lines changed

Diff for: changelog/12047.improvement.rst

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group.
2+
Previously, only the first exception was reported.

Diff for: src/_pytest/fixtures.py

+24-21
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import inspect
88
import os
99
from pathlib import Path
10+
import sys
1011
from typing import AbstractSet
1112
from typing import Any
1213
from typing import Callable
@@ -67,6 +68,10 @@
6768
from _pytest.scope import Scope
6869

6970

71+
if sys.version_info[:2] < (3, 11):
72+
from exceptiongroup import BaseExceptionGroup
73+
74+
7075
if TYPE_CHECKING:
7176
from typing import Deque
7277

@@ -1017,27 +1022,25 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None:
10171022
self._finalizers.append(finalizer)
10181023

10191024
def finish(self, request: SubRequest) -> None:
1020-
exc = None
1021-
try:
1022-
while self._finalizers:
1023-
try:
1024-
func = self._finalizers.pop()
1025-
func()
1026-
except BaseException as e:
1027-
# XXX Only first exception will be seen by user,
1028-
# ideally all should be reported.
1029-
if exc is None:
1030-
exc = e
1031-
if exc:
1032-
raise exc
1033-
finally:
1034-
ihook = request.node.ihook
1035-
ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
1036-
# Even if finalization fails, we invalidate the cached fixture
1037-
# value and remove all finalizers because they may be bound methods
1038-
# which will keep instances alive.
1039-
self.cached_result = None
1040-
self._finalizers.clear()
1025+
exceptions: List[BaseException] = []
1026+
while self._finalizers:
1027+
fin = self._finalizers.pop()
1028+
try:
1029+
fin()
1030+
except BaseException as e:
1031+
exceptions.append(e)
1032+
node = request.node
1033+
node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
1034+
# Even if finalization fails, we invalidate the cached fixture
1035+
# value and remove all finalizers because they may be bound methods
1036+
# which will keep instances alive.
1037+
self.cached_result = None
1038+
self._finalizers.clear()
1039+
if len(exceptions) == 1:
1040+
raise exceptions[0]
1041+
elif len(exceptions) > 1:
1042+
msg = f'errors while tearing down fixture "{self.argname}" of {node}'
1043+
raise BaseExceptionGroup(msg, exceptions[::-1])
10411044

10421045
def execute(self, request: SubRequest) -> FixtureValue:
10431046
# Get required arguments and register our own finish()

Diff for: testing/python/fixtures.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -932,8 +932,9 @@ def test_request_subrequest_addfinalizer_exceptions(
932932
self, pytester: Pytester
933933
) -> None:
934934
"""
935-
Ensure exceptions raised during teardown by a finalizer are suppressed
936-
until all finalizers are called, re-raising the first exception (#2440)
935+
Ensure exceptions raised during teardown by finalizers are suppressed
936+
until all finalizers are called, then re-reaised together in an
937+
exception group (#2440)
937938
"""
938939
pytester.makepyfile(
939940
"""
@@ -960,8 +961,16 @@ def test_second():
960961
"""
961962
)
962963
result = pytester.runpytest()
964+
result.assert_outcomes(passed=2, errors=1)
963965
result.stdout.fnmatch_lines(
964-
["*Exception: Error in excepts fixture", "* 2 passed, 1 error in *"]
966+
[
967+
' | *ExceptionGroup: errors while tearing down fixture "subrequest" of <Function test_first> (2 sub-exceptions)', # noqa: E501
968+
" +-+---------------- 1 ----------------",
969+
" | Exception: Error in something fixture",
970+
" +---------------- 2 ----------------",
971+
" | Exception: Error in excepts fixture",
972+
" +------------------------------------",
973+
],
965974
)
966975

967976
def test_request_getmodulepath(self, pytester: Pytester) -> None:

0 commit comments

Comments
 (0)