From 6f2f2aa06ae9b50f0451029caa1d8d83890a5b30 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Sun, 26 Jan 2025 14:17:20 +0100 Subject: [PATCH] fix(bec_signal_proxy): timeout for blocking implemented --- bec_widgets/utils/bec_signal_proxy.py | 50 +++++++++++++++---- .../unit_tests/test_utils_bec_signal_proxy.py | 49 ++++++++++++++++++ 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/bec_widgets/utils/bec_signal_proxy.py b/bec_widgets/utils/bec_signal_proxy.py index 949e3def..0055c70e 100644 --- a/bec_widgets/utils/bec_signal_proxy.py +++ b/bec_widgets/utils/bec_signal_proxy.py @@ -5,28 +5,43 @@ analyse data. Requesting a new fit may lead to request piling up and an overall will allow you to decide by yourself when to unblock and execute the callback again.""" from pyqtgraph import SignalProxy -from qtpy.QtCore import Signal, Slot +from qtpy.QtCore import QTimer, Signal + +from bec_widgets.qt_utils.error_popups import SafeSlot class BECSignalProxy(SignalProxy): - """Thin wrapper around the SignalProxy class to allow signal calls to be blocked, but args still being stored + """ + Thin wrapper around the SignalProxy class to allow signal calls to be blocked, + but arguments still being stored. Args: - *args: Arguments to pass to the SignalProxy class - rateLimit (int): The rateLimit of the proxy - **kwargs: Keyword arguments to pass to the SignalProxy class + *args: Arguments to pass to the SignalProxy class. + rateLimit (int): The rateLimit of the proxy. + timeout (float): The number of seconds after which the proxy automatically + unblocks if still blocked. Default is 10.0 seconds. + **kwargs: Keyword arguments to pass to the SignalProxy class. Example: - >>> proxy = BECSignalProxy(signal, rate_limit=25, slot=callback)""" + >>> proxy = BECSignalProxy(signal, rate_limit=25, slot=callback) + """ is_blocked = Signal(bool) - def __init__(self, *args, rateLimit=25, **kwargs): + def __init__(self, *args, rateLimit=25, timeout=10.0, **kwargs): super().__init__(*args, rateLimit=rateLimit, **kwargs) self._blocking = False self.old_args = None self.new_args = None + # Store timeout value (in seconds) + self._timeout = timeout + + # Create a single-shot timer for auto-unblocking + self._timer = QTimer() + self._timer.setSingleShot(True) + self._timer.timeout.connect(self._timeout_unblock) + @property def blocked(self): """Returns if the proxy is blocked""" @@ -46,9 +61,22 @@ class BECSignalProxy(SignalProxy): self.old_args = args super().signalReceived(*args) - @Slot() + self._timer.start(int(self._timeout * 1000)) + + @SafeSlot() def unblock_proxy(self): """Unblock the proxy, and call the signalReceived method in case there was an update of the args.""" - self.blocked = False - if self.new_args != self.old_args: - self.signalReceived(*self.new_args) + if self.blocked: + self._timer.stop() + self.blocked = False + if self.new_args != self.old_args: + self.signalReceived(*self.new_args) + + @SafeSlot() + def _timeout_unblock(self): + """ + Internal method called by the QTimer upon timeout. Unblocks the proxy + automatically if it is still blocked. + """ + if self.blocked: + self.unblock_proxy() diff --git a/tests/unit_tests/test_utils_bec_signal_proxy.py b/tests/unit_tests/test_utils_bec_signal_proxy.py index 2a549c25..e8fd50fb 100644 --- a/tests/unit_tests/test_utils_bec_signal_proxy.py +++ b/tests/unit_tests/test_utils_bec_signal_proxy.py @@ -73,3 +73,52 @@ def test_bec_signal_proxy(qtbot, dap_combo_box): qtbot.wait(100) assert proxy.blocked is False assert proxy_container == [(("samx",),), (("samz",),)] + + +def test_bec_signal_proxy_timeout(qtbot, dap_combo_box): + """ + Test that BECSignalProxy auto-unblocks after the specified timeout if no manual unblock + occurs in the interim. + """ + proxy_container = [] + + def proxy_callback(*args): + proxy_container.append(args) + + # Create the proxy with a short 1-second timeout + proxy = BECSignalProxy( + dap_combo_box.x_axis_updated, rateLimit=25, slot=proxy_callback, timeout=1.0 + ) + + # Initially, ensure it's not blocked + assert proxy.blocked is False + + # Trigger the signal once (samx) -> the proxy should block + dap_combo_box.x_axis = "samx" + qtbot.waitSignal(dap_combo_box.x_axis_updated, timeout=1000) + qtbot.wait(100) + assert proxy.blocked is True + # The first signal should be passed immediately to the callback + assert proxy_container == [(("samx",),)] + + # While still blocked, set another value (samz) + dap_combo_box.x_axis = "samz" + qtbot.waitSignal(dap_combo_box.x_axis_updated, timeout=1000) + qtbot.wait(100) + # Proxy is still blocked, so the callback shouldn't see "samz" yet + assert len(proxy_container) == 1 + + # Wait just under 1 second -> should still be blocked + qtbot.wait(700) + assert proxy.blocked is True + + # Wait a bit more than 1 s + qtbot.wait(2000) + + # Wait to catch the is_blocked signal that indicates it has unblocked + qtbot.waitSignal(proxy.is_blocked, timeout=2000) + # Now it should be unblocked + assert proxy.blocked is False + + # The second value "samz" should have been forwarded after auto-unblocking + assert proxy_container == [(("samx",),), (("samz",),)]