Skip to content

Commit

Permalink
prevent Config.add_cleanup callbacks preventing other cleanups running
Browse files Browse the repository at this point in the history
  • Loading branch information
graingert committed Nov 21, 2024
1 parent 72f17d1 commit aa1e3b3
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 9 deletions.
29 changes: 20 additions & 9 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import argparse
import collections.abc
import contextlib
import copy
import dataclasses
import enum
Expand Down Expand Up @@ -33,6 +34,7 @@
from typing import TextIO
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
import warnings

import pluggy
Expand Down Expand Up @@ -73,6 +75,8 @@
from _pytest.cacheprovider import Cache
from _pytest.terminal import TerminalReporter

_T_callback = TypeVar("_T_callback", bound=Callable[[], None])


_PluggyPlugin = object
"""A type to represent plugin objects.
Expand Down Expand Up @@ -1085,6 +1089,7 @@ def __init__(
)
self.args_source = Config.ArgsSource.ARGS
self.args: list[str] = []
self._exit_stack = contextlib.ExitStack()

@property
def rootpath(self) -> pathlib.Path:
Expand All @@ -1104,10 +1109,11 @@ def inipath(self) -> pathlib.Path | None:
"""
return self._inipath

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

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

def _ensure_unconfigure(self) -> None:
if self._configured:
self._configured = False
self.hook.pytest_unconfigure(config=self)
self.hook.pytest_configure._call_history = []
while self._cleanup:
fin = self._cleanup.pop()
fin()
try:
if self._configured:
self._configured = False
try:
self.hook.pytest_unconfigure(config=self)
finally:
self.hook.pytest_configure._call_history = []
finally:
try:
self._exit_stack.close()
finally:
self._exit_stack = contextlib.ExitStack()

def get_terminal_writer(self) -> TerminalWriter:
terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin(
Expand Down
31 changes: 31 additions & 0 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,37 @@ def test_confcutdir_check_isdir(self, pytester: Pytester) -> None:
def test_iter_rewritable_modules(self, names, expected) -> None:
assert list(_iter_rewritable_modules(names)) == expected

def test_add_cleanup(self, pytester: Pytester) -> None:
config = Config.fromdictargs({}, [])
config._do_configure()
report = []

class MyError(BaseException):
pass

@config.add_cleanup
def cleanup_last():
report.append("cleanup_last")

@config.add_cleanup
def raise_2():
report.append("raise_2")
raise MyError("raise_2")

@config.add_cleanup
def raise_1():
report.append("raise_1")
raise MyError("raise_1")

@config.add_cleanup
def cleanup_first():
report.append("cleanup_first")

with pytest.raises(MyError, match=r"raise_2"):
config._ensure_unconfigure()

assert report == ["cleanup_first", "raise_1", "raise_2", "cleanup_last"]


class TestConfigFromdictargs:
def test_basic_behavior(self, _sys_snapshot) -> None:
Expand Down

0 comments on commit aa1e3b3

Please sign in to comment.