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:
@ -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]):
|
||||||
|
@ -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())
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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."""
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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():
|
||||||
|
Reference in New Issue
Block a user