0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 03:01:50 +02:00

fix: parse config on submission and reload after

This commit is contained in:
2025-06-17 08:52:08 +02:00
committed by David Perl
parent 4d8c07cdd1
commit 5e4c129af6
7 changed files with 51 additions and 29 deletions

View File

@ -72,7 +72,7 @@ class DictBackedTableModel(QAbstractTableModel):
def replaceData(self, data: dict): def replaceData(self, data: dict):
self.delete_rows(list(range(len(self._data)))) self.delete_rows(list(range(len(self._data))))
self.resetInternalData() self.resetInternalData()
self._data = [[k, v] for k, v in data.items()] self._data = [[str(k), str(v)] for k, v in data.items()]
self.dataChanged.emit(self.index(0, 0), self.index(len(self._data), 1)) self.dataChanged.emit(self.index(0, 0), self.index(len(self._data), 1))
def update_disallowed_keys(self, keys: list[str]): def update_disallowed_keys(self, keys: list[str]):

View File

@ -87,14 +87,13 @@ class DeviceBrowser(BECWidget, QWidget):
for device, device_obj in self.dev.items(): for device, device_obj in self.dev.items():
item = QListWidgetItem(self.dev_list) item = QListWidgetItem(self.dev_list)
device_item = DeviceItem( device_item = DeviceItem(
parent=self, device=device, icon=map_device_type_to_icon(device_obj) parent=self,
device=device,
devices=self.dev,
icon=map_device_type_to_icon(device_obj),
) )
device_item.expansion_state_changed.connect(partial(_updatesize, item, device_item)) device_item.expansion_state_changed.connect(partial(_updatesize, item, device_item))
tooltip = self.dev[device]._config.get("description", "")
device_config = self.dev[device]._config # pylint: disable=protected-access
device_item.set_display_config(device_config)
tooltip = device_config.get("description", "")
device_item.setToolTip(tooltip) device_item.setToolTip(tooltip)
device_item.broadcast_size_hint.connect(item.setSizeHint) device_item.broadcast_size_hint.connect(item.setSizeHint)
item.setSizeHint(device_item.sizeHint()) item.setSizeHint(device_item.sizeHint())

View File

@ -1,5 +1,4 @@
import traceback from ast import literal_eval
from threading import Thread
from bec_lib.atlas_models import Device as DeviceConfigModel from bec_lib.atlas_models import Device as DeviceConfigModel
from bec_lib.config_helper import CONF as DEVICE_CONF_KEYS from bec_lib.config_helper import CONF as DEVICE_CONF_KEYS
@ -10,6 +9,7 @@ from qtpy.QtWidgets import (
QApplication, QApplication,
QDialog, QDialog,
QDialogButtonBox, QDialogButtonBox,
QLabel,
QStackedLayout, QStackedLayout,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
@ -26,7 +26,7 @@ logger = bec_logger.logger
class _CommSignals(QObject): class _CommSignals(QObject):
error = Signal(str) error = Signal(Exception)
done = Signal() done = Signal()
@ -48,18 +48,17 @@ class _CommunicateUpdate(QRunnable):
) )
logger.info("Waiting for config reply") logger.info("Waiting for config reply")
reply = self.config_helper.wait_for_config_reply(RID, timeout=timeout) reply = self.config_helper.wait_for_config_reply(RID, timeout=timeout)
logger.info("Handling config reply")
self.config_helper.handle_update_reply(reply, RID, timeout) self.config_helper.handle_update_reply(reply, RID, timeout)
logger.info("Done updating config!")
except Exception as e: except Exception as e:
self.signals.error.emit( self.signals.error.emit(e)
f"Error updating config: \n {''.join(traceback.format_exception(e))}"
)
finally: finally:
self.signals.done.emit() self.signals.done.emit()
class DeviceConfigDialog(BECWidget, QDialog): class DeviceConfigDialog(BECWidget, QDialog):
RPC = False RPC = False
applied = Signal()
def __init__( def __init__(
self, self,
@ -78,6 +77,14 @@ class DeviceConfigDialog(BECWidget, QDialog):
self._container = QStackedLayout() self._container = QStackedLayout()
self._container.setStackingMode(QStackedLayout.StackAll) self._container.setStackingMode(QStackedLayout.StackAll)
self._layout = QVBoxLayout()
user_warning = QLabel(
"Warning: edit items here at your own risk - minimal validation is applied to the entered values.\n"
"Items in the deviceConfig dictionary should correspond to python literals, e.g. numbers, lists, strings (including quotes), etc."
)
user_warning.setWordWrap(True)
user_warning.setStyleSheet("QLabel { color: red; }")
self._layout.addWidget(user_warning)
self._add_form() self._add_form()
self._add_overlay() self._add_overlay()
self._add_buttons() self._add_buttons()
@ -87,7 +94,6 @@ class DeviceConfigDialog(BECWidget, QDialog):
def _add_form(self): def _add_form(self):
self._form_widget = QWidget() self._form_widget = QWidget()
self._layout = QVBoxLayout()
self._form_widget.setLayout(self._layout) self._form_widget.setLayout(self._layout)
self._form = DeviceConfigForm() self._form = DeviceConfigForm()
self._layout.addWidget(self._form) self._layout.addWidget(self._form)
@ -137,13 +143,20 @@ class DeviceConfigDialog(BECWidget, QDialog):
def updated_config(self): def updated_config(self):
new_config = self._form.get_form_data() new_config = self._form.get_form_data()
return { diff = {
k: v for k, v in new_config.items() if self._initial_config.get(k) != new_config.get(k) k: v for k, v in new_config.items() if self._initial_config.get(k) != new_config.get(k)
} }
# TODO: replace when https://github.com/bec-project/bec/issues/528 is resolved
if diff.get("deviceConfig") is not None:
diff["deviceConfig"] = {
k: literal_eval(str(v)) for k, v in diff["deviceConfig"].items()
}
return diff
@SafeSlot() @SafeSlot()
def apply(self): def apply(self):
self._process_update_action() self._process_update_action()
self.applied.emit()
@SafeSlot() @SafeSlot()
def accept(self): def accept(self):
@ -181,9 +194,9 @@ class DeviceConfigDialog(BECWidget, QDialog):
self._fetch_config() self._fetch_config()
self._fill_form() self._fill_form()
@SafeSlot(str) @SafeSlot(Exception, popup_error=True)
def update_error(self, e: str): def update_error(self, e: Exception):
logger.error(e) raise RuntimeError("Failed to update device configuration") from e
def _start_waiting_display(self): def _start_waiting_display(self):
self._overlay_widget.setVisible(True) self._overlay_widget.setVisible(True)

View File

@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
from functools import partial
from bec_lib.atlas_models import Device as DeviceConfigModel from bec_lib.atlas_models import Device as DeviceConfigModel
from pydantic import BaseModel from pydantic import BaseModel
from qtpy.QtWidgets import QApplication from qtpy.QtWidgets import QApplication
@ -13,7 +11,6 @@ from bec_widgets.utils.forms_from_types.items import (
DEFAULT_WIDGET_TYPES, DEFAULT_WIDGET_TYPES,
BoolFormItem, BoolFormItem,
BoolToggleFormItem, BoolToggleFormItem,
widget_from_type,
) )
@ -46,6 +43,10 @@ class DeviceConfigForm(PydanticModelForm):
theme = get_theme_name() theme = get_theme_name()
self.setStyleSheet(styles.pretty_display_theme(theme)) self.setStyleSheet(styles.pretty_display_theme(theme))
def get_form_data(self):
"""Get the entered metadata as a dict."""
return self._md_schema.model_validate(super().get_form_data()).model_dump()
def _connect_to_theme_change(self): def _connect_to_theme_change(self):
"""Connect to the theme change signal.""" """Connect to the theme change signal."""
qapp = QApplication.instance() qapp = QApplication.instance()

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from bec_lib.atlas_models import Device as DeviceConfigModel from bec_lib.atlas_models import Device as DeviceConfigModel
from bec_lib.devicemanager import DeviceContainer
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from bec_qthemes import material_icon from bec_qthemes import material_icon
from qtpy.QtCore import QMimeData, QSize, Qt, Signal from qtpy.QtCore import QMimeData, QSize, Qt, Signal
@ -30,9 +31,9 @@ class DeviceItem(ExpandableGroupFrame):
RPC = False RPC = False
def __init__(self, parent, device: str, icon: str = "") -> None: def __init__(self, parent, device: str, devices: DeviceContainer, icon: str = "") -> None:
super().__init__(parent, title=device, expanded=False, icon=icon) super().__init__(parent, title=device, expanded=False, icon=icon)
self.dev = devices
self._drag_pos = None self._drag_pos = None
self._expanded_first_time = False self._expanded_first_time = False
self._data = None self._data = None
@ -54,6 +55,8 @@ class DeviceItem(ExpandableGroupFrame):
def _create_edit_dialog(self): def _create_edit_dialog(self):
dialog = DeviceConfigDialog(parent=self, device=self.device) dialog = DeviceConfigDialog(parent=self, device=self.device)
dialog.accepted.connect(self._reload_config)
dialog.applied.connect(self._reload_config)
dialog.open() dialog.open()
@SafeSlot() @SafeSlot()
@ -62,8 +65,7 @@ class DeviceItem(ExpandableGroupFrame):
self._expanded_first_time = True self._expanded_first_time = True
self.form = DeviceConfigForm(parent=self, pretty_display=True) self.form = DeviceConfigForm(parent=self, pretty_display=True)
self._contents.layout().addWidget(self.form) self._contents.layout().addWidget(self.form)
if self._data: self._reload_config()
self.form.set_data(self._data)
self.broadcast_size_hint.emit(self.sizeHint()) self.broadcast_size_hint.emit(self.sizeHint())
super().switch_expanded_state() super().switch_expanded_state()
if self._expanded_first_time: if self._expanded_first_time:
@ -74,6 +76,11 @@ class DeviceItem(ExpandableGroupFrame):
self.adjustSize() self.adjustSize()
self.broadcast_size_hint.emit(self.sizeHint()) self.broadcast_size_hint.emit(self.sizeHint())
@SafeSlot(popup_error=True)
def _reload_config(self, *_):
self.dev[self.device].read_configuration(cached=False)
self.set_display_config(self.dev[self.device]._config)
def set_display_config(self, config_dict: dict): def set_display_config(self, config_dict: dict):
"""Set the displayed information from a device config dict, which must conform to the """Set the displayed information from a device config dict, which must conform to the
bec_lib.atlas_models.Device config model.""" bec_lib.atlas_models.Device config model."""

View File

@ -26,6 +26,7 @@ if TYPE_CHECKING: # pragma: no cover
@pytest.fixture @pytest.fixture
def device_browser(qtbot, mocked_client): def device_browser(qtbot, mocked_client):
dev_browser = DeviceBrowser(client=mocked_client) dev_browser = DeviceBrowser(client=mocked_client)
dev_browser.dev["samx"].read_configuration = mock.MagicMock()
qtbot.addWidget(dev_browser) qtbot.addWidget(dev_browser)
qtbot.waitExposed(dev_browser) qtbot.waitExposed(dev_browser)
yield dev_browser yield dev_browser
@ -88,8 +89,9 @@ def test_device_item_expansion(device_browser, qtbot):
form = widget._contents.layout().itemAt(0).widget() form = widget._contents.layout().itemAt(0).widget()
qtbot.waitUntil(lambda: isinstance(form, DeviceConfigForm), timeout=500) qtbot.waitUntil(lambda: isinstance(form, DeviceConfigForm), timeout=500)
assert widget.expanded assert widget.expanded
device_browser.dev["samx"].read_configuration.assert_called()
assert (name_field := form.widget_dict.get("name")) is not None assert (name_field := form.widget_dict.get("name")) is not None
assert name_field.getValue() == "samx" qtbot.waitUntil(lambda: name_field.getValue() == "samx", timeout=500)
qtbot.mouseClick(widget._expansion_button, Qt.MouseButton.LeftButton) qtbot.mouseClick(widget._expansion_button, Qt.MouseButton.LeftButton)
assert not widget.expanded assert not widget.expanded

View File

@ -81,8 +81,8 @@ def test_waiting_display(dialog, qtbot):
def test_update_cycle(dialog, qtbot): def test_update_cycle(dialog, qtbot):
update = {"enabled": False, "readoutPriority": "baseline", "deviceTags": {"tag"}} update = {"enabled": False, "readoutPriority": "baseline", "deviceTags": {"tag"}}
def _mock_send(a, c, w): def _mock_send(action="update", config=None, wait_for_response=True, timeout_s=None):
dialog.client.device_manager.devices["test_device"]._config = c["test_device"] dialog.client.device_manager.devices["test_device"]._config = config["test_device"] # type: ignore
dialog._config_helper.send_config_request = MagicMock(side_effect=_mock_send) dialog._config_helper.send_config_request = MagicMock(side_effect=_mock_send)
for item in dialog._form.enumerate_form_widgets(): for item in dialog._form.enumerate_form_widgets():