Skip to content

Commit f4e1025

Browse files
authored
Merge pull request #12048 from bluetech/fixture-teardown-excgroup
fixtures: use exception group when multiple finalizers raise in fixture teardown
2 parents 43492f5 + 434282e commit f4e1025

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)