From a391f3018c50fee6a4a06884491b957df80c3cd3 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Sun, 30 Jun 2024 18:18:36 +0200 Subject: [PATCH] feat(bec_connector): export config to yaml --- bec_widgets/cli/client.py | 6 -- .../jupyter_console/jupyter_console_window.py | 5 +- bec_widgets/utils/bec_connector.py | 58 +++++++++++++++++++ bec_widgets/utils/yaml_dialog.py | 30 +++++++++- bec_widgets/widgets/figure/figure.py | 6 +- tests/unit_tests/test_yaml_dialog.py | 14 ++--- 6 files changed, 98 insertions(+), 21 deletions(-) diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index d32dcc63..8fbba771 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -622,12 +622,6 @@ class BECFigure(RPCBase): list[BECPlotBase]: List of all widgets in the figure. """ - @rpc_call - def apply_config(self, config: "dict | FigureConfig"): - """ - None - """ - class BECImageItem(RPCBase): @property diff --git a/bec_widgets/examples/jupyter_console/jupyter_console_window.py b/bec_widgets/examples/jupyter_console/jupyter_console_window.py index 08fa0f3b..376ae984 100644 --- a/bec_widgets/examples/jupyter_console/jupyter_console_window.py +++ b/bec_widgets/examples/jupyter_console/jupyter_console_window.py @@ -47,9 +47,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover: "d0": self.d0, "d1": self.d1, "d2": self.d2, - "fig0": self.fig0, - "fig1": self.fig1, - "fig2": self.fig2, "plt": self.plt, "bar": self.bar, } @@ -108,6 +105,8 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover: # curves for w1 self.c1 = self.w1.get_config() + self.fig_c = self.figure.config_dict + def _init_dock(self): self.d0 = self.dock.add_dock(name="dock_0") diff --git a/bec_widgets/utils/bec_connector.py b/bec_widgets/utils/bec_connector.py index 7362d077..b4b8e1f1 100644 --- a/bec_widgets/utils/bec_connector.py +++ b/bec_widgets/utils/bec_connector.py @@ -1,15 +1,19 @@ # pylint: disable = no-name-in-module,missing-module-docstring from __future__ import annotations +import os import time +import uuid from typing import Optional +import yaml from bec_lib.utils.import_utils import lazy_import_from from pydantic import BaseModel, Field, field_validator from qtpy.QtCore import QObject, QRunnable, QThreadPool, Signal from qtpy.QtCore import Slot as pyqtSlot from bec_widgets.cli.rpc_register import RPCRegister +from bec_widgets.utils.yaml_dialog import load_yaml, load_yaml_gui, save_yaml, save_yaml_gui BECDispatcher = lazy_import_from("bec_widgets.utils.bec_dispatcher", ("BECDispatcher",)) @@ -161,6 +165,60 @@ class BECConnector: """ self.config = config + def apply_config(self, config: dict, generate_new_id: bool = True) -> None: + """ + Apply the configuration to the widget. + + Args: + config(dict): Configuration settings. + generate_new_id(bool): If True, generate a new GUI ID for the widget. + """ + self.config = ConnectionConfig(**config) + if generate_new_id is True: + gui_id = str(uuid.uuid4()) + self.rpc_register.remove_rpc(self) + self.set_gui_id(gui_id) + self.rpc_register.add_rpc(self) + else: + self.gui_id = self.config.gui_id + + def load_config(self, path: str | None = None, gui: bool = False): + """ + Load the configuration of the widget from YAML. + + Args: + path(str): Path to the configuration file for non-GUI dialog mode. + gui(bool): If True, use the GUI dialog to load the configuration file. + """ + if gui is True: + config = load_yaml_gui(self) + else: + config = load_yaml(path) + + if config is not None: + if config.get("widget_class") != self.__class__.__name__: + raise ValueError( + f"Configuration file is not for {self.__class__.__name__}. Got configuration for {config.get('widget_class')}." + ) + self.apply_config(config) + + def save_config(self, path: str | None = None, gui: bool = False): + """ + Save the configuration of the widget to YAML. + + Args: + path(str): Path to save the configuration file for non-GUI dialog mode. + gui(bool): If True, use the GUI dialog to save the configuration file. + """ + if gui is True: + save_yaml_gui(self, self.config_dict) + else: + if path is None: + path = os.getcwd() + file_path = os.path.join(path, f"{self.__class__.__name__}_config.yaml") + + save_yaml(file_path, self.config_dict) + @pyqtSlot(str) def set_gui_id(self, gui_id: str) -> None: """ diff --git a/bec_widgets/utils/yaml_dialog.py b/bec_widgets/utils/yaml_dialog.py index 1f051813..660a0715 100644 --- a/bec_widgets/utils/yaml_dialog.py +++ b/bec_widgets/utils/yaml_dialog.py @@ -6,7 +6,7 @@ import yaml from qtpy.QtWidgets import QFileDialog -def load_yaml(instance) -> Union[dict, None]: +def load_yaml_gui(instance) -> Union[dict, None]: """ Load YAML file from disk. @@ -20,12 +20,25 @@ def load_yaml(instance) -> Union[dict, None]: file_path, _ = QFileDialog.getOpenFileName( instance, "Load Settings", "", "YAML Files (*.yaml *.yml);;All Files (*)", options=options ) + config = load_yaml(file_path) + return config + +def load_yaml(file_path: str) -> Union[dict, None]: + """ + Load YAML file from disk. + + Args: + file_path(str): Path to the YAML file. + + Returns: + dict: Configuration data loaded from the YAML file. + """ if not file_path: return None try: with open(file_path, "r") as file: - config = yaml.safe_load(file) + config = yaml.load(file, Loader=yaml.FullLoader) return config except FileNotFoundError: @@ -38,7 +51,7 @@ def load_yaml(instance) -> Union[dict, None]: print(f"An error occurred while loading the settings from {file_path}: {e}") -def save_yaml(instance, config: dict) -> None: +def save_yaml_gui(instance, config: dict) -> None: """ Save YAML file to disk. @@ -51,6 +64,17 @@ def save_yaml(instance, config: dict) -> None: instance, "Save Settings", "", "YAML Files (*.yaml *.yml);;All Files (*)", options=options ) + save_yaml(file_path, config) + + +def save_yaml(file_path: str, config: dict) -> None: + """ + Save YAML file to disk. + + Args: + file_path(str): Path to the YAML file. + config(dict): Configuration data to be saved. + """ if not file_path: return try: diff --git a/bec_widgets/widgets/figure/figure.py b/bec_widgets/widgets/figure/figure.py index 98a13c82..9ac9b8b7 100644 --- a/bec_widgets/widgets/figure/figure.py +++ b/bec_widgets/widgets/figure/figure.py @@ -123,7 +123,9 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget): "clear_all", "get_all_rpc", "widget_list", - "apply_config", + # "apply_config", + # "save_config", + # "load_config", ] subplot_map = { "PlotBase": BECPlotBase, @@ -173,7 +175,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget): "Key must be a string (widget id) or a tuple of two integers (grid coordinates)" ) - def apply_config(self, config: dict | FigureConfig): + def apply_config(self, config: dict | FigureConfig): # ,generate_new_id: bool = False): if isinstance(config, dict): try: config = FigureConfig(**config) diff --git a/tests/unit_tests/test_yaml_dialog.py b/tests/unit_tests/test_yaml_dialog.py index 568d21f1..647f818b 100644 --- a/tests/unit_tests/test_yaml_dialog.py +++ b/tests/unit_tests/test_yaml_dialog.py @@ -7,7 +7,7 @@ import pytest import yaml from qtpy.QtWidgets import QPushButton, QVBoxLayout, QWidget -from bec_widgets.utils.yaml_dialog import load_yaml, save_yaml +from bec_widgets.utils.yaml_dialog import load_yaml_gui, save_yaml_gui @pytest.fixture(scope="function") @@ -33,7 +33,7 @@ def test_load_yaml(qtbot, example_widget): temp_file.write(b"name: test\nvalue: 42") def load_yaml_wrapper(): - config = load_yaml(example_widget) + config = load_yaml_gui(example_widget) if config: example_widget.config.update(config) @@ -49,7 +49,7 @@ def test_load_yaml(qtbot, example_widget): def test_load_yaml_file_not_found(qtbot, example_widget, capsys): def load_yaml_wrapper(): - config = load_yaml(example_widget) + config = load_yaml_gui(example_widget) if config: example_widget.config.update(config) @@ -76,7 +76,7 @@ def test_load_yaml_general_exception(qtbot, example_widget, capsys, monkeypatch) monkeypatch.setattr("builtins.open", mock_open) def load_yaml_wrapper(): - config = load_yaml(example_widget) + config = load_yaml_gui(example_widget) if config: example_widget.config.update(config) @@ -96,7 +96,7 @@ def test_load_yaml_permission_error(qtbot, example_widget, monkeypatch, capsys): os.chmod(temp_file_path, 0o000) # Remove permissions def load_yaml_wrapper(): - config = load_yaml(example_widget) + config = load_yaml_gui(example_widget) if config: example_widget.config.update(config) @@ -120,7 +120,7 @@ def test_load_yaml_invalid_yaml(qtbot, example_widget, capsys): temp_file.write(b"\tinvalid_yaml: [unbalanced_brackets: ]") def load_yaml_wrapper(): - config = load_yaml(example_widget) + config = load_yaml_gui(example_widget) if config: example_widget.config.update(config) @@ -147,7 +147,7 @@ def test_save_yaml(qtbot, example_widget): example_widget.saved_config = {"name": "test", "value": 42} def save_yaml_wrapper(): - save_yaml(example_widget, example_widget.saved_config) + save_yaml_gui(example_widget, example_widget.saved_config) example_widget.export_button.clicked.connect(save_yaml_wrapper)