Skip to content

Commit 4f777ff

Browse files
Prevent Config.add_cleanup callbacks preventing other cleanups running (#12982)
Ensure all callbacks registered via Config.add_cleanup will be called, regardless if any of them raises an error. --------- Co-authored-by: Bruno Oliveira <[email protected]>
1 parent 72f17d1 commit 4f777ff

File tree

3 files changed

+49
-11
lines changed

3 files changed

+49
-11
lines changed

changelog/12981.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Prevent exceptions in :func:`pytest.Config.add_cleanup` callbacks preventing further cleanups.

src/_pytest/config/__init__.py

+17-11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import argparse
77
import collections.abc
8+
import contextlib
89
import copy
910
import dataclasses
1011
import enum
@@ -73,7 +74,6 @@
7374
from _pytest.cacheprovider import Cache
7475
from _pytest.terminal import TerminalReporter
7576

76-
7777
_PluggyPlugin = object
7878
"""A type to represent plugin objects.
7979
@@ -1077,7 +1077,7 @@ def __init__(
10771077
self._inicache: dict[str, Any] = {}
10781078
self._override_ini: Sequence[str] = ()
10791079
self._opt2dest: dict[str, str] = {}
1080-
self._cleanup: list[Callable[[], None]] = []
1080+
self._cleanup_stack = contextlib.ExitStack()
10811081
self.pluginmanager.register(self, "pytestconfig")
10821082
self._configured = False
10831083
self.hook.pytest_addoption.call_historic(
@@ -1106,8 +1106,9 @@ def inipath(self) -> pathlib.Path | None:
11061106

11071107
def add_cleanup(self, func: Callable[[], None]) -> None:
11081108
"""Add a function to be called when the config object gets out of
1109-
use (usually coinciding with pytest_unconfigure)."""
1110-
self._cleanup.append(func)
1109+
use (usually coinciding with pytest_unconfigure).
1110+
"""
1111+
self._cleanup_stack.callback(func)
11111112

11121113
def _do_configure(self) -> None:
11131114
assert not self._configured
@@ -1117,13 +1118,18 @@ def _do_configure(self) -> None:
11171118
self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
11181119

11191120
def _ensure_unconfigure(self) -> None:
1120-
if self._configured:
1121-
self._configured = False
1122-
self.hook.pytest_unconfigure(config=self)
1123-
self.hook.pytest_configure._call_history = []
1124-
while self._cleanup:
1125-
fin = self._cleanup.pop()
1126-
fin()
1121+
try:
1122+
if self._configured:
1123+
self._configured = False
1124+
try:
1125+
self.hook.pytest_unconfigure(config=self)
1126+
finally:
1127+
self.hook.pytest_configure._call_history = []
1128+
finally:
1129+
try:
1130+
self._cleanup_stack.close()
1131+
finally:
1132+
self._cleanup_stack = contextlib.ExitStack()
11271133

11281134
def get_terminal_writer(self) -> TerminalWriter:
11291135
terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin(

testing/test_config.py

+31
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,37 @@ def test_confcutdir_check_isdir(self, pytester: Pytester) -> None:
983983
def test_iter_rewritable_modules(self, names, expected) -> None:
984984
assert list(_iter_rewritable_modules(names)) == expected
985985

986+
def test_add_cleanup(self, pytester: Pytester) -> None:
987+
config = Config.fromdictargs({}, [])
988+
config._do_configure()
989+
report = []
990+
991+
class MyError(BaseException):
992+
pass
993+
994+
@config.add_cleanup
995+
def cleanup_last():
996+
report.append("cleanup_last")
997+
998+
@config.add_cleanup
999+
def raise_2():
1000+
report.append("raise_2")
1001+
raise MyError("raise_2")
1002+
1003+
@config.add_cleanup
1004+
def raise_1():
1005+
report.append("raise_1")
1006+
raise MyError("raise_1")
1007+
1008+
@config.add_cleanup
1009+
def cleanup_first():
1010+
report.append("cleanup_first")
1011+
1012+
with pytest.raises(MyError, match=r"raise_2"):
1013+
config._ensure_unconfigure()
1014+
1015+
assert report == ["cleanup_first", "raise_1", "raise_2", "cleanup_last"]
1016+
9861017

9871018
class TestConfigFromdictargs:
9881019
def test_basic_behavior(self, _sys_snapshot) -> None:

0 commit comments

Comments
 (0)