diff --git a/bec_widgets/utils/bec_connector.py b/bec_widgets/utils/bec_connector.py index 0c720a09..81032e27 100644 --- a/bec_widgets/utils/bec_connector.py +++ b/bec_widgets/utils/bec_connector.py @@ -12,7 +12,7 @@ import shiboken6 as shb from bec_lib.logger import bec_logger from bec_lib.utils.import_utils import lazy_import_from from pydantic import BaseModel, Field, field_validator -from qtpy.QtCore import QObject, QRunnable, QThreadPool, QTimer, Signal +from qtpy.QtCore import Property, QObject, QRunnable, QThreadPool, QTimer, Signal from qtpy.QtWidgets import QApplication from bec_widgets.cli.rpc.rpc_register import RPCRegister @@ -480,6 +480,62 @@ class BECConnector: else: return self.config + def export_settings(self) -> dict: + """ + Export the settings of the widget as dict. + + Returns: + dict: The exported settings of the widget. + """ + + # We first get all qproperties that were defined in a bec_widgets class + objs = self._get_bec_meta_objects() + settings = {} + for prop_name in objs.keys(): + try: + prop_value = getattr(self, prop_name) + settings[prop_name] = prop_value + except Exception as e: + logger.warning( + f"Could not export property '{prop_name}' from '{self.__class__.__name__}': {e}" + ) + return settings + + def load_settings(self, settings: dict) -> None: + """ + Load the settings of the widget from dict. + + Args: + settings (dict): The settings to load into the widget. + """ + objs = self._get_bec_meta_objects() + for prop_name, prop_value in settings.items(): + if prop_name in objs: + try: + setattr(self, prop_name, prop_value) + except Exception as e: + logger.warning( + f"Could not load property '{prop_name}' into '{self.__class__.__name__}': {e}" + ) + + def _get_bec_meta_objects(self) -> dict: + """ + Get BEC meta objects for the widget. + + Returns: + dict: BEC meta objects. + """ + if not isinstance(self, QObject): + return {} + objects = {} + for name, attr in vars(self.__class__).items(): + if isinstance(attr, Property): + # Check if the property is a SafeProperty + is_safe_property = getattr(attr.fget, "__is_safe_getter__", False) + if is_safe_property: + objects[name] = attr + return objects + # --- Example usage of BECConnector: running a simple task --- if __name__ == "__main__": # pragma: no cover diff --git a/bec_widgets/utils/error_popups.py b/bec_widgets/utils/error_popups.py index 2e586407..7d52af4b 100644 --- a/bec_widgets/utils/error_popups.py +++ b/bec_widgets/utils/error_popups.py @@ -53,6 +53,8 @@ def SafeProperty(prop_type, *prop_args, popup_error: bool = False, default=None, logger.error(f"SafeProperty error in GETTER of '{prop_name}':\n{error_msg}") return default + safe_getter.__is_safe_getter__ = True # type: ignore[attr-defined] + class PropertyWrapper: """ Intermediate wrapper used so that the user can optionally chain .setter(...). diff --git a/tests/unit_tests/test_bec_connector.py b/tests/unit_tests/test_bec_connector.py index 62b88ca7..4610fbc3 100644 --- a/tests/unit_tests/test_bec_connector.py +++ b/tests/unit_tests/test_bec_connector.py @@ -3,9 +3,10 @@ import time import pytest from qtpy.QtCore import QObject -from qtpy.QtWidgets import QApplication +from qtpy.QtWidgets import QApplication, QWidget from bec_widgets.utils import BECConnector +from bec_widgets.utils.error_popups import SafeProperty from bec_widgets.utils.error_popups import SafeSlot as Slot from .client_mocks import mocked_client @@ -131,3 +132,33 @@ def test_bec_connector_change_object_name(bec_connector): # Verify that the object with the previous name is no longer registered all_objects = bec_connector.rpc_register.list_all_connections().values() assert not any(obj.objectName() == previous_name for obj in all_objects) + + +def test_bec_connector_export_settings(): + + class MyWidget(BECConnector, QWidget): + def __init__(self, parent=None, client=None, **kwargs): + super().__init__(parent=parent, client=client, **kwargs) + self.setWindowTitle("My Widget") + self._my_str_property = "default" + + @SafeProperty(str) + def my_str_property(self) -> str: + return self._my_str_property + + @my_str_property.setter + def my_str_property(self, value: str): + self._my_str_property = value + + @property + def my_int_property(self) -> int: + return 42 + + widget = MyWidget(client=mocked_client) + out = widget.export_settings() + assert len(out) == 1 + assert out["my_str_property"] == "default" + + config = {"my_str_property": "new_value"} + widget.load_settings(config) + assert widget.my_str_property == "new_value"