Skip to content

Commit 33234ef

Browse files
committed
prevent Config.add_cleanup callbacks preventing other cleanups running
1 parent 72f17d1 commit 33234ef

File tree

2 files changed

+51
-10
lines changed

2 files changed

+51
-10
lines changed

src/_pytest/config/__init__.py

Lines changed: 20 additions & 10 deletions
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
@@ -33,6 +34,7 @@
3334
from typing import TextIO
3435
from typing import Type
3536
from typing import TYPE_CHECKING
37+
from typing import TypeVar
3638
import warnings
3739

3840
import pluggy
@@ -73,6 +75,8 @@
7375
from _pytest.cacheprovider import Cache
7476
from _pytest.terminal import TerminalReporter
7577

78+
_T_callback = TypeVar("_T_callback", bound=Callable[[], None])
79+
7680

7781
_PluggyPlugin = object
7882
"""A type to represent plugin objects.
@@ -1077,7 +1081,7 @@ def __init__(
10771081
self._inicache: dict[str, Any] = {}
10781082
self._override_ini: Sequence[str] = ()
10791083
self._opt2dest: dict[str, str] = {}
1080-
self._cleanup: list[Callable[[], None]] = []
1084+
self._exit_stack = contextlib.ExitStack()
10811085
self.pluginmanager.register(self, "pytestconfig")
10821086
self._configured = False
10831087
self.hook.pytest_addoption.call_historic(
@@ -1104,10 +1108,11 @@ def inipath(self) -> pathlib.Path | None:
11041108
"""
11051109
return self._inipath
11061110

1107-
def add_cleanup(self, func: Callable[[], None]) -> None:
1111+
def add_cleanup(self, func: _T_callback) -> _T_callback:
11081112
"""Add a function to be called when the config object gets out of
11091113
use (usually coinciding with pytest_unconfigure)."""
1110-
self._cleanup.append(func)
1114+
self._exit_stack.callback(func)
1115+
return func
11111116

11121117
def _do_configure(self) -> None:
11131118
assert not self._configured
@@ -1117,13 +1122,18 @@ def _do_configure(self) -> None:
11171122
self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
11181123

11191124
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()
1125+
try:
1126+
if self._configured:
1127+
self._configured = False
1128+
try:
1129+
self.hook.pytest_unconfigure(config=self)
1130+
finally:
1131+
self.hook.pytest_configure._call_history = []
1132+
finally:
1133+
try:
1134+
self._exit_stack.close()
1135+
finally:
1136+
self._exit_stack = contextlib.ExitStack()
11271137

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

testing/test_config.py

Lines changed: 31 additions & 0 deletions
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)