0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-12 18:51: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):
self.delete_rows(list(range(len(self._data))))
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))
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():
item = QListWidgetItem(self.dev_list)
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_config = self.dev[device]._config # pylint: disable=protected-access
device_item.set_display_config(device_config)
tooltip = device_config.get("description", "")
tooltip = self.dev[device]._config.get("description", "")
device_item.setToolTip(tooltip)
device_item.broadcast_size_hint.connect(item.setSizeHint)
item.setSizeHint(device_item.sizeHint())

View File

@ -1,5 +1,4 @@
import traceback
from threading import Thread
from ast import literal_eval
from bec_lib.atlas_models import Device as DeviceConfigModel
from bec_lib.config_helper import CONF as DEVICE_CONF_KEYS
@ -10,6 +9,7 @@ from qtpy.QtWidgets import (
QApplication,
QDialog,
QDialogButtonBox,
QLabel,
QStackedLayout,
QVBoxLayout,
QWidget,
@ -26,7 +26,7 @@ logger = bec_logger.logger
class _CommSignals(QObject):
error = Signal(str)
error = Signal(Exception)
done = Signal()
@ -48,18 +48,17 @@ class _CommunicateUpdate(QRunnable):
)
logger.info("Waiting for config reply")
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)
logger.info("Done updating config!")
except Exception as e:
self.signals.error.emit(
f"Error updating config: \n {''.join(traceback.format_exception(e))}"
)
self.signals.error.emit(e)
finally:
self.signals.done.emit()
class DeviceConfigDialog(BECWidget, QDialog):
RPC = False
applied = Signal()
def __init__(
self,
@ -78,6 +77,14 @@ class DeviceConfigDialog(BECWidget, QDialog):
self._container = QStackedLayout()
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_overlay()
self._add_buttons()
@ -87,7 +94,6 @@ class DeviceConfigDialog(BECWidget, QDialog):
def _add_form(self):
self._form_widget = QWidget()
self._layout = QVBoxLayout()
self._form_widget.setLayout(self._layout)
self._form = DeviceConfigForm()
self._layout.addWidget(self._form)
@ -137,13 +143,20 @@ class DeviceConfigDialog(BECWidget, QDialog):
def updated_config(self):
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)
}
# 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()
def apply(self):
self._process_update_action()
self.applied.emit()
@SafeSlot()
def accept(self):
@ -181,9 +194,9 @@ class DeviceConfigDialog(BECWidget, QDialog):
self._fetch_config()
self._fill_form()
@SafeSlot(str)
def update_error(self, e: str):
logger.error(e)
@SafeSlot(Exception, popup_error=True)
def update_error(self, e: Exception):
raise RuntimeError("Failed to update device configuration") from e
def _start_waiting_display(self):
self._overlay_widget.setVisible(True)

View File

@ -1,7 +1,5 @@
from __future__ import annotations
from functools import partial
from bec_lib.atlas_models import Device as DeviceConfigModel
from pydantic import BaseModel
from qtpy.QtWidgets import QApplication
@ -13,7 +11,6 @@ from bec_widgets.utils.forms_from_types.items import (
DEFAULT_WIDGET_TYPES,
BoolFormItem,
BoolToggleFormItem,
widget_from_type,
)
@ -46,6 +43,10 @@ class DeviceConfigForm(PydanticModelForm):
theme = get_theme_name()
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):
"""Connect to the theme change signal."""
qapp = QApplication.instance()

View File

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

View File

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

View File

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