Skip to content

Commit aa1e3b3

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

File tree

2 files changed

+51
-9
lines changed

2 files changed

+51
-9
lines changed

src/_pytest/config/__init__.py

Lines changed: 20 additions & 9 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.
@@ -1085,6 +1089,7 @@ def __init__(
10851089
)
10861090
self.args_source = Config.ArgsSource.ARGS
10871091
self.args: list[str] = []
1092+
self._exit_stack = contextlib.ExitStack()
10881093

10891094
@property
10901095
def rootpath(self) -> pathlib.Path:
@@ -1104,10 +1109,11 @@ def inipath(self) -> pathlib.Path | None:
11041109
"""
11051110
return self._inipath
11061111

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

11121118
def _do_configure(self) -> None:
11131119
assert not self._configured
@@ -1117,13 +1123,18 @@ def _do_configure(self) -> None:
11171123
self.hook.pytest_configure.call_historic(kwargs=dict(config=self))
11181124

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

11281139
def get_terminal_writer(self) -> TerminalWriter:
11291140
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)