From 5c562feb4802a8fc8dc48013bd568cb33c6d21d4 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 6 Oct 2026 18:21:45 +0200 Subject: [PATCH 1/2] feat: add clear_queued function to discard queued callbacks --- src/psygnal/__init__.py | 3 ++- src/psygnal/_queue.py | 29 ++++++++++++++++++++++++++++- tests/test_psygnal.py | 19 ++++++++++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/psygnal/__init__.py b/src/psygnal/__init__.py index f8c16ec0..77d00794 100644 --- a/src/psygnal/__init__.py +++ b/src/psygnal/__init__.py @@ -30,6 +30,7 @@ "SignalInstance", "__version__", "_compiled", + "clear_queued", "debounced", "emit_queued", "evented", @@ -57,7 +58,7 @@ from ._exceptions import EmitLoopError from ._group import EmissionInfo, PathStep, SignalGroup from ._group_descriptor import SignalGroupDescriptor, get_evented_namespace, is_evented -from ._queue import emit_queued +from ._queue import clear_queued, emit_queued from ._signal import Signal, SignalInstance, _compiled from ._throttler import debounced, throttled diff --git a/src/psygnal/_queue.py b/src/psygnal/_queue.py index 4cdb4b95..5deb3eef 100644 --- a/src/psygnal/_queue.py +++ b/src/psygnal/_queue.py @@ -2,7 +2,7 @@ from collections import defaultdict from collections.abc import Callable -from queue import Queue +from queue import Empty, Queue from threading import Thread, current_thread, main_thread from typing import Any, ClassVar, Literal @@ -108,3 +108,30 @@ def emit_queued(thread: Thread | None = None) -> None: cb(args) except Exception as e: # pragma: no cover raise EmitLoopError(exc=e) from e + + +def clear_queued(thread: Thread | None = None) -> int: + """Discard all callbacks queued for `thread`, without invoking them. + + Parameters + ---------- + thread : Thread, optional + The thread whose queue should be cleared. If not provided, the current + thread will be used. + + Returns + ------- + int + The number of queued callbacks that were discarded. + """ + _thread = current_thread() if thread is None else thread + if (queue := QueuedCallback._GLOBAL_QUEUE.get(_thread)) is None: + return 0 + + count = 0 + while True: + try: + queue.get_nowait() + except Empty: + return count + count += 1 diff --git a/tests/test_psygnal.py b/tests/test_psygnal.py index cfc1813e..99cc8137 100644 --- a/tests/test_psygnal.py +++ b/tests/test_psygnal.py @@ -3,13 +3,14 @@ from contextlib import suppress from functools import partial, wraps from inspect import Signature +from threading import Thread from typing import Literal from unittest.mock import MagicMock, Mock, call import pytest import psygnal -from psygnal import EmitLoopError, Signal, SignalInstance +from psygnal import EmitLoopError, Signal, SignalInstance, clear_queued, emit_queued from psygnal._signal import ReemissionMode, ReemissionVal from psygnal._weak_callback import WeakCallback @@ -975,6 +976,22 @@ def cb3(arg): this_thread_mock.assert_called_once_with(2, this_thread) +def test_clear_queued(): + + emitter = Emitter() + mock = Mock() + emitter.one_int.connect(mock, thread="main") + + thread = Thread(target=lambda: [emitter.one_int.emit(i) for i in range(3)]) + thread.start() + thread.join() + + assert clear_queued() == 3 + emit_queued() + mock.assert_not_called() + assert clear_queued() == 0 + + def test_deepcopy(): from copy import deepcopy From 276e8a89e9a866ba38bfb27096d59ccc07dd4b8e Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Tue, 6 Oct 2026 19:19:44 +0200 Subject: [PATCH 2/2] test: cover clear_queued on a thread with no queue Co-Authored-By: Claude Opus 5.5 --- tests/test_psygnal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_psygnal.py b/tests/test_psygnal.py index 99cc8137..caeb05f4 100644 --- a/tests/test_psygnal.py +++ b/tests/test_psygnal.py @@ -990,6 +990,8 @@ def test_clear_queued(): emit_queued() mock.assert_not_called() assert clear_queued() == 0 + # a thread that never had anything queued + assert clear_queued(Thread()) == 0 def test_deepcopy():