diff --git a/src/psygnal/_signal.py b/src/psygnal/_signal.py index f6f3b5b8..503d2e50 100644 --- a/src/psygnal/_signal.py +++ b/src/psygnal/_signal.py @@ -951,6 +951,11 @@ def _run_emit_loop(self, args: tuple[Any, ...]) -> None: for caller in self._slots: try: caller.cb(args) + except RecursionError as e: + raise RecursionError( + f"RecursionError in {caller.slot_repr()} when " + f"emitting signal {self.name!r} with args {args}" + ) from e except Exception as e: raise EmitLoopError( cb=caller, args=args, exc=e, signal=self diff --git a/tests/test_psygnal.py b/tests/test_psygnal.py index d47ac22b..8a3633c5 100644 --- a/tests/test_psygnal.py +++ b/tests/test_psygnal.py @@ -1,4 +1,6 @@ import gc +import os +import sys from contextlib import suppress from functools import partial, wraps from inspect import Signature @@ -6,11 +8,15 @@ from unittest.mock import MagicMock, Mock, call import pytest -import toolz +import psygnal from psygnal import EmitLoopError, Signal, SignalInstance from psygnal._weak_callback import WeakCallback +PY39 = sys.version_info[:2] == (3, 9) +WINDOWS = os.name == "nt" +COMPILED = psygnal._compiled + def stupid_decorator(fun): def _fun(*args): @@ -268,8 +274,10 @@ def test_slot_types(type_: str) -> None: elif type_ == "partial_method": signal.connect(partial(obj.f_int_int, 2)) elif type_ == "toolz_function": + toolz = pytest.importorskip("toolz") signal.connect(toolz.curry(f_int_int, 2)) elif type_ == "toolz_method": + toolz = pytest.importorskip("toolz") signal.connect(toolz.curry(obj.f_int_int, 2)) elif type_ == "partial_method_kwarg": signal.connect(partial(obj.f_int_int, b=2)) @@ -362,6 +370,7 @@ def test_weakref(slot): if slot == "partial": emitter.one_int.connect(partial(obj.f_int_int, 1)) elif slot == "toolz_curry": + toolz = pytest.importorskip("toolz") emitter.one_int.connect(toolz.curry(obj.f_int_int, 1)) else: emitter.one_int.connect(getattr(obj, slot)) @@ -967,3 +976,15 @@ def test_pickle(): x = pickle.loads(_dump) x.sig.emit() mock.assert_called_once() + + +@pytest.mark.skipif(PY39 and WINDOWS and COMPILED, reason="fails") +def test_recursion_error() -> None: + s = SignalInstance() + + @s.connect + def callback() -> None: + s.emit() + + with pytest.raises(RecursionError): + s.emit() diff --git a/tests/test_weak_callable.py b/tests/test_weak_callable.py index 70eccf80..4dc06b98 100644 --- a/tests/test_weak_callable.py +++ b/tests/test_weak_callable.py @@ -5,7 +5,6 @@ from weakref import ref import pytest -import toolz from psygnal._weak_callback import WeakCallback, weak_callback @@ -59,6 +58,7 @@ def obj(x: int) -> None: cb = weak_callback(obj, strong_func=(type_ == "function"), finalize=final_mock) elif type_ == "toolz_function": + toolz = pytest.importorskip("toolz") @toolz.curry def obj(z: int, x: int) -> None: @@ -73,6 +73,7 @@ def obj(z: int, x: int) -> None: elif type_ == "partial_method": cb = weak_callback(partial(obj.method, 2), max_args=0, finalize=final_mock) elif type_ == "toolz_method": + toolz = pytest.importorskip("toolz") cb = weak_callback(toolz.curry(obj.method, 2), max_args=0, finalize=final_mock) elif type_ == "mock": cb = weak_callback(mock, finalize=final_mock)