diff --git a/bec_widgets/utils/error_popups.py b/bec_widgets/utils/error_popups.py index 8d35f1a2..36500c84 100644 --- a/bec_widgets/utils/error_popups.py +++ b/bec_widgets/utils/error_popups.py @@ -96,15 +96,33 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name 'popup_error' keyword argument can be passed with boolean value if a dialog should pop up, otherwise error display is left to the original exception hook + 'verify_sender' keyword argument can be passed with boolean value if the sender should be verified + before executing the slot. If True, the slot will only execute if the sender is a QObject. This is + useful to prevent function calls from already deleted objects. """ popup_error = bool(slot_kwargs.pop("popup_error", False)) + verify_sender = bool(slot_kwargs.pop("verify_sender", False)) def error_managed(method): @Slot(*slot_args, **slot_kwargs) @functools.wraps(method) def wrapper(*args, **kwargs): try: + if not verify_sender or len(args) == 0: + return method(*args, **kwargs) + + _instance = args[0] + if not isinstance(_instance, QObject): + return method(*args, **kwargs) + sender = _instance.sender() + if sender is None: + logger.info( + f"Sender is None for {method.__module__}.{method.__qualname__}, " + "skipping method call." + ) + return return method(*args, **kwargs) + except Exception: slot_name = f"{method.__module__}.{method.__qualname__}" error_msg = traceback.format_exc() diff --git a/tests/unit_tests/test_error_utils.py b/tests/unit_tests/test_error_utils.py index 205a18da..184853c6 100644 --- a/tests/unit_tests/test_error_utils.py +++ b/tests/unit_tests/test_error_utils.py @@ -1,13 +1,11 @@ -import sys from unittest.mock import patch import pytest -import pytestqt from bec_lib.logger import bec_logger -from qtpy.QtCore import QObject +from qtpy.QtCore import QObject, Signal from qtpy.QtWidgets import QMessageBox -from bec_widgets.utils.error_popups import ErrorPopupUtility, ExampleWidget, SafeProperty +from bec_widgets.utils.error_popups import ErrorPopupUtility, ExampleWidget, SafeProperty, SafeSlot class TestSafePropertyClass(QObject): @@ -30,6 +28,32 @@ class TestSafePropertyClass(QObject): self._my_value = val +class TestSafeSlotEmitter(QObject): + test_signal = Signal() + + +class TestSafeSlotClass(QObject): + """ + Test class to demonstrate the use of SafeSlot decorator. + """ + + def __init__(self, parent=None, signal_obj: TestSafeSlotEmitter | None = None): + super().__init__(parent) + assert signal_obj is not None, "Signal object must be provided" + signal_obj.test_signal.connect(self.method_without_sender_verification) + signal_obj.test_signal.connect(self.method_with_sender_verification) + self._method_without_verification_called = False + self._method_with_verification_called = False + + @SafeSlot() + def method_without_sender_verification(self): + self._method_without_verification_called = True + + @SafeSlot(verify_sender=True) + def method_with_sender_verification(self): + self._method_with_verification_called = True + + @pytest.fixture def widget(qtbot): test_widget = ExampleWidget() @@ -147,3 +171,28 @@ def test_safe_property_setter_error(mock_exec, mock_log_error, qtbot, global_pop logged_msg = mock_log_error.call_args[0][0] assert "SafeProperty error in SETTER" in logged_msg assert "ValueError" in logged_msg + + +@pytest.mark.timeout(100) +def test_safe_slot_emit(qtbot): + """ + Test that the signal is emitted correctly. + """ + signal_obj = TestSafeSlotEmitter() + test_obj = TestSafeSlotClass(signal_obj=signal_obj) + signal_obj.test_signal.emit() + + qtbot.waitUntil(lambda: test_obj._method_without_verification_called, timeout=1000) + qtbot.waitUntil(lambda: test_obj._method_with_verification_called, timeout=1000) + + test_obj.deleteLater() + + test_obj = TestSafeSlotClass(signal_obj=signal_obj) + test_obj.method_without_sender_verification() + test_obj.method_with_sender_verification() + + assert test_obj._method_without_verification_called is True + assert test_obj._method_with_verification_called is False + + test_obj.deleteLater() + signal_obj.deleteLater()