From a823dd243e2bb9d8eaded78f102e985be1ae4722 Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Fri, 29 Aug 2025 15:39:58 +0200 Subject: [PATCH] feat: add SafeConnect --- bec_widgets/utils/bec_widget.py | 9 +++--- bec_widgets/utils/error_popups.py | 48 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/bec_widgets/utils/bec_widget.py b/bec_widgets/utils/bec_widget.py index 67a4c5e9..34a80ee2 100644 --- a/bec_widgets/utils/bec_widget.py +++ b/bec_widgets/utils/bec_widget.py @@ -11,7 +11,7 @@ from qtpy.QtWidgets import QApplication, QFileDialog, QWidget from bec_widgets.cli.rpc.rpc_register import RPCRegister from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig -from bec_widgets.utils.error_popups import SafeSlot +from bec_widgets.utils.error_popups import SafeConnect, SafeSlot from bec_widgets.utils.rpc_decorator import rpc_timeout from bec_widgets.utils.widget_io import WidgetHierarchy @@ -61,7 +61,6 @@ class BECWidget(BECConnector): ) if not isinstance(self, QObject): raise RuntimeError(f"{repr(self)} is not a subclass of QWidget") - if theme_update: logger.debug(f"Subscribing to theme updates for {self.__class__.__name__}") self._connect_to_theme_change() @@ -70,10 +69,10 @@ class BECWidget(BECConnector): """Connect to the theme change signal.""" qapp = QApplication.instance() if hasattr(qapp, "theme"): - qapp.theme.theme_changed.connect(self._update_theme) + SafeConnect(self, qapp.theme.theme_changed, self._update_theme) - @SafeSlot(str, verify_sender=True) - @SafeSlot(verify_sender=True) + @SafeSlot(str) + @SafeSlot() def _update_theme(self, theme: str | None = None): """Update the theme.""" if theme is None: diff --git a/bec_widgets/utils/error_popups.py b/bec_widgets/utils/error_popups.py index d2ead3dd..730fcdce 100644 --- a/bec_widgets/utils/error_popups.py +++ b/bec_widgets/utils/error_popups.py @@ -2,7 +2,9 @@ import functools import sys import traceback +import shiboken6 from bec_lib.logger import bec_logger +from louie.saferef import safe_ref from qtpy.QtCore import Property, QObject, Qt, Signal, Slot from qtpy.QtWidgets import QApplication, QMessageBox, QPushButton, QVBoxLayout, QWidget @@ -90,6 +92,52 @@ def SafeProperty(prop_type, *prop_args, popup_error: bool = False, default=None, return decorator +def _safe_connect_slot(weak_instance, weak_slot, *connect_args): + """Internal function used by SafeConnect to handle weak references to slots.""" + instance = weak_instance() + slot_func = weak_slot() + + # Check if the python object has already been garbage collected + if instance is None or slot_func is None: + return + + # Check if the python object has already been marked for deletion + if getattr(instance, "_destroyed", False): + return + + # Check if the C++ object is still valid + if not shiboken6.isValid(instance): + return + + if connect_args: + slot_func(*connect_args) + slot_func() + + +def SafeConnect(instance, signal, slot): # pylint: disable=invalid-name + """ + Method to safely handle Qt signal-slot connections. The python object is only forwarded + as a weak reference to avoid stale objects. + + Args: + instance: The instance to connect. + signal: The signal to connect to. + slot: The slot to connect. + + Example: + >>> SafeConnect(self, qapp.theme.theme_changed, self._update_theme) + + """ + weak_instance = safe_ref(instance) + weak_slot = safe_ref(slot) + + # Create a partial function that will check weak references before calling the actual slot + safe_slot = functools.partial(_safe_connect_slot, weak_instance, weak_slot) + + # Connect the signal to the safe connect slot wrapper + return signal.connect(safe_slot) + + def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name """Function with args, acting like a decorator, applying "error_managed" decorator + Qt Slot to the passed function, to display errors instead of potentially raising an exception