fix(beamline-states): better pydantic model handling

This commit is contained in:
2026-06-11 15:56:22 +02:00
committed by Jan Wyzula
parent 4bb7e811dd
commit 64cbf93d64
3 changed files with 101 additions and 303 deletions
@@ -3,10 +3,9 @@ from __future__ import annotations
import sys
from typing import Any
from bec_lib import bl_states
from bec_lib import bl_states, messages
from bec_lib.endpoints import MessageEndpoints
from bec_qthemes import material_icon
from pydantic import BaseModel
from qtpy.QtCore import QAbstractListModel, QModelIndex, QSize, Qt
from qtpy.QtWidgets import (
QAbstractItemView,
@@ -48,7 +47,7 @@ class _BeamlineStateListModel(QAbstractListModel):
super().__init__(parent)
self._state_order: list[str] = []
self._state_rows: dict[str, int] = {}
self._state_configs: dict[str, dict[str, Any]] = {}
self._state_configs: dict[str, messages.BeamlineStateConfig] = {}
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: # noqa: N802
return 0 if parent.isValid() else len(self._state_order)
@@ -60,7 +59,7 @@ class _BeamlineStateListModel(QAbstractListModel):
if role in (Qt.ItemDataRole.DisplayRole, self.NameRole):
return name
if role == self.ConfigRole:
return self._state_configs.get(name, {})
return self._state_configs.get(name)
return None
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
@@ -68,9 +67,9 @@ class _BeamlineStateListModel(QAbstractListModel):
return Qt.ItemFlag.NoItemFlags
return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
def set_states(self, state_configs: list[dict[str, Any]]) -> None:
new_order = [str(state["name"]) for state in state_configs if state.get("name")]
new_configs = {str(state["name"]): state for state in state_configs if state.get("name")}
def set_states(self, state_configs: list[messages.BeamlineStateConfig]) -> None:
new_order = [state.name for state in state_configs]
new_configs = {state.name: state for state in state_configs}
for row in reversed(
[row for row, name in enumerate(self._state_order) if name not in new_configs]
@@ -129,11 +128,8 @@ class _BeamlineStatePillDelegate(QStyledItemDelegate):
self, parent: QWidget, _option: QStyleOptionViewItem, index: QModelIndex
) -> QWidget:
name = index.data(_BeamlineStateListModel.NameRole)
state_config = index.data(_BeamlineStateListModel.ConfigRole) or {}
title = state_config.get("title") or name
pill = BeamlineStatePill(
parent=parent, state_name=name, title=title, client=self._manager.client
)
state_config = index.data(_BeamlineStateListModel.ConfigRole)
pill = BeamlineStatePill(parent=parent, state_name=name, client=self._manager.client)
pill.idle_card_background = self._manager.idle_card_background
pill.set_state_config(state_config)
pill.state_changed.connect(self._manager._on_pill_state_changed)
@@ -147,9 +143,8 @@ class _BeamlineStatePillDelegate(QStyledItemDelegate):
if not isinstance(editor, BeamlineStatePill):
return
name = index.data(_BeamlineStateListModel.NameRole)
state_config = index.data(_BeamlineStateListModel.ConfigRole) or {}
title = state_config.get("title") or name
editor.set_state_name(str(name), title=str(title))
state_config = index.data(_BeamlineStateListModel.ConfigRole)
editor.set_state_name(str(name))
editor.idle_card_background = self._manager.idle_card_background
editor.set_state_config(state_config)
@@ -229,7 +224,7 @@ class BeamlineStateManager(BECWidget, QWidget):
)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self._state_pills: dict[str, BeamlineStatePill] = {}
self._state_configs: dict[str, dict[str, Any]] = {}
self._state_configs: dict[str, messages.BeamlineStateConfig] = {}
self._state_order: list[str] = []
self._selected_statuses: set[str] | None = None
self._selected_devices: set[str] | None = None
@@ -414,14 +409,12 @@ class BeamlineStateManager(BECWidget, QWidget):
) -> None:
"""Update the displayed pills from ``AvailableBeamlineStatesMessage`` content."""
expanded_names = {name for name, pill in self._state_pills.items() if pill.is_expanded()}
states = content.get("states", [])
state_configs = [self._state_config_to_dict(state) for state in states]
state_configs = [state for state in state_configs if state.get("name")]
state_configs: list[messages.BeamlineStateConfig] = content.get("states", [])
if state_configs == list(self._state_configs.values()):
self._apply_filters()
return
self._state_configs = {str(state["name"]): state for state in state_configs}
self._state_order = [str(state["name"]) for state in state_configs]
self._state_configs = {state.name: state for state in state_configs}
self._state_order = [state.name for state in state_configs]
self._model.set_states(state_configs)
self._open_persistent_editors(expanded_names)
self._apply_filters()
@@ -472,7 +465,7 @@ class BeamlineStateManager(BECWidget, QWidget):
):
return False
device = self._state_device(self._state_configs.get(name, {}))
device = self._state_device(self._state_configs.get(name))
if self._selected_devices is not None and device not in self._selected_devices:
return False
@@ -505,14 +498,13 @@ class BeamlineStateManager(BECWidget, QWidget):
)
return
try:
parameters = config.model_dump(exclude={"name"})
state_client.update_parameters(**parameters)
state_client.update_parameters(**config.model_dump(exclude={"name"}))
except Exception as exc:
QMessageBox.warning(self, "Cannot Update State", str(exc))
return
pill = self._state_pills.get(state_name)
if pill is not None:
pill.mark_current_settings_clean(config)
pill.mark_current_settings_clean()
@SafeSlot(str)
def _remove_state_requested(self, state_name: str) -> None:
@@ -551,27 +543,10 @@ class BeamlineStateManager(BECWidget, QWidget):
)
@staticmethod
def _state_device(state: dict[str, Any]) -> str | None:
parameters = state.get("parameters")
if isinstance(parameters, dict):
device = parameters.get("device")
else:
device = state.get("device")
def _state_device(state: messages.BeamlineStateConfig | None) -> str | None:
device = state.parameters.get("device") if state is not None else None
return str(device) if device else None
@staticmethod
def _state_config_to_dict(state: dict[str, Any] | BaseModel) -> dict[str, Any]:
if isinstance(state, dict):
return state
state_dict = state.model_dump()
state_type = getattr(state, "state_type", None)
if state_type is not None:
state_dict.setdefault("state_type", state_type)
title = getattr(state, "title", None)
if title is not None and not state_dict.get("title"):
state_dict["title"] = title
return state_dict
def cleanup(self) -> None:
self.bec_dispatcher.disconnect_slot(
self.update_available_states, MessageEndpoints.available_beamline_states()
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Any
from bec_lib import bl_states
from bec_lib import bl_states, messages
from bec_lib.endpoints import MessageEndpoints
from bec_qthemes import material_icon
from qtpy.QtCore import Qt, Signal
@@ -76,7 +76,6 @@ class BeamlineStatePill(BECWidget, QWidget):
self,
parent: QWidget | None = None,
state_name: str | None = None,
title: str | None = None,
client=None,
config: ConnectionConfig | None = None,
gui_id: str | None = None,
@@ -89,8 +88,7 @@ class BeamlineStatePill(BECWidget, QWidget):
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self._state_name: str | None = None
self._title: str | None = None
self._state_config: dict[str, Any] = {}
self._state_config: messages.BeamlineStateConfig | None = None
self._status = "unknown"
self._label = "No state information available."
self._expanded = False
@@ -100,9 +98,9 @@ class BeamlineStatePill(BECWidget, QWidget):
self._settings_dirty_fields: set[str] = set()
self._settings_form_stale = True
self._init_ui(state_name, title)
self._init_ui(state_name)
def _init_ui(self, state_name: str | None = None, title: str | None = None) -> None:
def _init_ui(self, state_name: str | None = None) -> None:
self._shadow = QGraphicsDropShadowEffect(self)
self._shadow.setBlurRadius(18)
self._shadow.setOffset(0, 2)
@@ -212,7 +210,7 @@ class BeamlineStatePill(BECWidget, QWidget):
self.setLayout(layout)
self._settings.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.set_state_name(state_name, title=title)
self.set_state_name(state_name)
self._update_button.setEnabled(False)
self._revert_button.setEnabled(False)
@@ -225,15 +223,14 @@ class BeamlineStatePill(BECWidget, QWidget):
def state_name(self, state_name: str | None) -> None:
self.set_state_name(state_name)
def set_state_name(self, state_name: str | None, title: str | None = None) -> None:
def set_state_name(self, state_name: str | None) -> None:
"""
Set the BEC beamline state this pill displays.
Args:
state_name: State name as published by ``AvailableBeamlineStatesMessage``.
title: Optional human-readable title for the state.
"""
if state_name == self._state_name and title == self._title:
if state_name == self._state_name:
return
if self._state_name is not None:
@@ -242,8 +239,7 @@ class BeamlineStatePill(BECWidget, QWidget):
)
self._state_name = state_name
self._title = title
self._name_label.setText(title or state_name or "Beamline state")
self._name_label.setText(state_name or "Beamline state")
if self._state_name is None:
self._set_visual_state("unknown", "No beamline state selected.")
@@ -255,13 +251,13 @@ class BeamlineStatePill(BECWidget, QWidget):
self.update_state, MessageEndpoints.beamline_state(self._state_name)
)
def set_state_config(self, state_config: dict[str, Any]) -> None:
def set_state_config(self, state_config: messages.BeamlineStateConfig | None) -> None:
"""Set the editable BEC state configuration displayed by the expanded panel."""
self._state_config = state_config
self._settings_form_stale = True
if self._config_form is not None:
self._populate_settings()
self._mark_settings_clean_from_current()
self.mark_current_settings_clean()
@SafeProperty(bool, default=False)
def idle_card_background(self) -> bool:
@@ -439,13 +435,13 @@ class BeamlineStatePill(BECWidget, QWidget):
def _ensure_settings_form_current(self) -> PydanticWidgetForm:
if self._settings_form_stale:
self._populate_settings()
self._mark_settings_clean_from_current()
self.mark_current_settings_clean()
return self._ensure_config_form()
def _populate_settings(self) -> None:
self._populating_settings = True
try:
state_type = str(self._state_config.get("state_type") or "")
state_type = self._state_config.state_type if self._state_config is not None else ""
config_class = None
for state_class in SUPPORTED_BEAMLINE_STATES:
if state_type in {state_class.__name__, state_class.CONFIG_CLASS.state_type}:
@@ -468,25 +464,8 @@ class BeamlineStatePill(BECWidget, QWidget):
config = self._ensure_settings_form_current().model_instance()
return config # type: ignore[return-value]
def mark_current_settings_clean(
self, config: bl_states.BeamlineStateConfig | None = None
) -> None:
def mark_current_settings_clean(self) -> None:
"""Mark the current editor values as saved."""
if config is None:
parameters = self._ensure_config_form().raw_editable_data()
else:
parameters = config.model_dump(exclude={"name"})
if self._state_config:
state_parameters = self._state_config.get("parameters")
if isinstance(state_parameters, dict):
state_parameters.update(parameters)
else:
self._state_config.update(parameters)
if "title" in parameters:
self._state_config["title"] = parameters["title"]
self._mark_settings_clean_from_current()
def _mark_settings_clean_from_current(self) -> None:
config_form = self._ensure_config_form()
self._settings_baseline = config_form.raw_editable_data()
config_form.mark_clean()
@@ -561,15 +540,12 @@ class BeamlineStatePill(BECWidget, QWidget):
self, config_class: type[bl_states.BeamlineStateConfig]
) -> dict[str, Any]:
data: dict[str, Any] = {}
parameters = self._state_config.get("parameters")
parameter_values = parameters if isinstance(parameters, dict) else {}
parameters = self._state_config.parameters if self._state_config is not None else {}
for name in config_class.model_fields:
if name == "name":
data[name] = self._state_name
elif name in parameter_values:
data[name] = parameter_values[name]
elif name in self._state_config:
data[name] = self._state_config[name]
elif name in parameters:
data[name] = parameters[name]
return data
@staticmethod
+66 -219
View File
@@ -1,7 +1,7 @@
import shiboken6
from bec_lib import bl_states
from bec_lib import bl_states, messages
from qtpy.QtCore import QCoreApplication, QEvent, Qt
from qtpy.QtWidgets import QMessageBox, QStyleOptionViewItem
from qtpy.QtWidgets import QMessageBox
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.utils.widget_io import WidgetIO
@@ -15,14 +15,30 @@ from .client_mocks import mocked_client
from .conftest import create_widget
def test_beamline_state_pill_updates_from_message(qtbot, mocked_client):
pill = create_widget(
qtbot, BeamlineStatePill, state_name="shutter_open", title="Shutter", client=mocked_client
def _state(name: str, state_type: str, parameters: dict | None = None):
return messages.BeamlineStateConfig(
name=name, state_type=state_type, parameters=parameters or {}
)
def _limits_state(name: str = "limits", **overrides):
parameters = {
"device": "samx",
"signal": "samx",
"low_limit": 0.0,
"high_limit": 10.0,
"tolerance": 0.1,
}
parameters.update(overrides)
return _state(name, "DeviceWithinLimitsState", parameters)
def test_beamline_state_pill_updates_from_message(qtbot, mocked_client):
pill = create_widget(qtbot, BeamlineStatePill, state_name="shutter_open", client=mocked_client)
pill.update_state({"name": "shutter_open", "status": "valid", "label": "Shutter is open."}, {})
assert pill._state_name == "shutter_open"
assert pill._name_label.text() == "Shutter"
assert pill._name_label.text() == "shutter_open"
assert pill._status_label.text() == "VALID"
assert pill._detail_label.text() == "Shutter is open."
assert not pill._icon_label.pixmap().isNull()
@@ -30,9 +46,7 @@ def test_beamline_state_pill_updates_from_message(qtbot, mocked_client):
def test_beamline_state_pill_ignores_other_states(qtbot, mocked_client):
pill = create_widget(
qtbot, BeamlineStatePill, state_name="shutter_open", title="Shutter", client=mocked_client
)
pill = create_widget(qtbot, BeamlineStatePill, state_name="shutter_open", client=mocked_client)
pill.update_state(
{"name": "other_state", "status": "invalid", "label": "Should be ignored."}, {}
)
@@ -42,25 +56,8 @@ def test_beamline_state_pill_ignores_other_states(qtbot, mocked_client):
def test_beamline_state_pill_expands_and_emits_updated_limits(qtbot, mocked_client):
limits_pill = create_widget(
qtbot, BeamlineStatePill, state_name="limits", title="Limits", client=mocked_client
)
limits_pill.set_state_config(
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {
"name": "limits",
"title": "Limits",
"device": "samx",
"signal": "samx",
"low_limit": 0.0,
"high_limit": 10.0,
"tolerance": 0.1,
},
}
)
limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client)
limits_pill.set_state_config(_limits_state())
assert limits_pill._settings.isHidden()
assert limits_pill._config_form is None
@@ -97,9 +94,7 @@ def test_beamline_state_pill_expands_and_emits_updated_limits(qtbot, mocked_clie
def test_beamline_state_pill_first_expand_uses_config_class_without_rebuild(
qtbot, mocked_client, monkeypatch
):
limits_pill = create_widget(
qtbot, BeamlineStatePill, state_name="limits", title="Limits", client=mocked_client
)
limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client)
set_model_calls = []
original_set_model = pill_module.PydanticWidgetForm.set_model
@@ -108,20 +103,7 @@ def test_beamline_state_pill_first_expand_uses_config_class_without_rebuild(
return original_set_model(self, model, data=data)
monkeypatch.setattr(pill_module.PydanticWidgetForm, "set_model", set_model_spy)
limits_pill.set_state_config(
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {
"device": "samx",
"signal": "samx",
"low_limit": 0.0,
"high_limit": 10.0,
"tolerance": 0.1,
},
}
)
limits_pill.set_state_config(_limits_state())
limits_pill.set_expanded(True)
assert limits_pill._config_form is not None
@@ -129,23 +111,8 @@ def test_beamline_state_pill_first_expand_uses_config_class_without_rebuild(
def test_beamline_state_pill_reverts_changed_settings(qtbot, mocked_client):
limits_pill = create_widget(
qtbot, BeamlineStatePill, state_name="limits", title="Limits", client=mocked_client
)
limits_pill.set_state_config(
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {
"device": "samx",
"signal": "samx",
"low_limit": 0.0,
"high_limit": 10.0,
"tolerance": 0.1,
},
}
)
limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client)
limits_pill.set_state_config(_limits_state())
limits_pill.set_expanded(True)
assert limits_pill._config_form is not None
@@ -166,23 +133,8 @@ def test_beamline_state_pill_reverts_changed_settings(qtbot, mocked_client):
def test_beamline_state_pill_does_not_override_themed_input_controls(qtbot, mocked_client):
limits_pill = create_widget(
qtbot, BeamlineStatePill, state_name="limits", title="Limits", client=mocked_client
)
limits_pill.set_state_config(
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {
"device": "samx",
"signal": "samx",
"low_limit": 0.0,
"high_limit": 10.0,
"tolerance": 0.1,
},
}
)
limits_pill = create_widget(qtbot, BeamlineStatePill, state_name="limits", client=mocked_client)
limits_pill.set_state_config(_limits_state())
limits_pill.set_expanded(True)
stylesheet = limits_pill.styleSheet()
@@ -197,18 +149,8 @@ def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client):
beamline_state_manager.update_available_states(
{
"states": [
{
"name": "shutter_open",
"title": "Shutter",
"state_type": "ShutterState",
"parameters": {},
},
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {},
},
_state("shutter_open", "ShutterState"),
_state("limits", "DeviceWithinLimitsState"),
]
},
{},
@@ -216,7 +158,7 @@ def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client):
assert sorted(beamline_state_manager._state_pills) == ["limits", "shutter_open"]
assert beamline_state_manager._model.rowCount() == 2
assert beamline_state_manager._state_pills["shutter_open"]._name_label.text() == "Shutter"
assert beamline_state_manager._state_pills["shutter_open"]._name_label.text() == "shutter_open"
assert not beamline_state_manager._empty_label.isVisible()
beamline_state_manager._state_pills["limits"].update_state(
@@ -227,17 +169,7 @@ def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client):
assert summary["shutter_open"]["status"] == "unknown"
beamline_state_manager.update_available_states(
{
"states": [
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {},
}
]
},
{},
{"states": [_state("limits", "DeviceWithinLimitsState")]}, {}
)
assert sorted(beamline_state_manager._state_pills) == ["limits"]
@@ -246,22 +178,7 @@ def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client):
def test_beamline_state_manager_ignores_unchanged_available_states(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
content = {
"states": [
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {
"device": "samx",
"signal": "samx",
"low_limit": 0.0,
"high_limit": 10.0,
"tolerance": 0.1,
},
}
]
}
content = {"states": [_limits_state()]}
beamline_state_manager.update_available_states(content, {})
pill = beamline_state_manager._state_pills["limits"]
@@ -274,24 +191,8 @@ def test_beamline_state_manager_ignores_unchanged_available_states(qtbot, mocked
def test_beamline_state_manager_adds_state_without_recreating_existing_pills(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
limits_state = {
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {
"device": "samx",
"signal": "samx",
"low_limit": 0.0,
"high_limit": 10.0,
"tolerance": 0.1,
},
}
shutter_state = {
"name": "shutter_open",
"title": "Shutter",
"state_type": "ShutterState",
"parameters": {},
}
limits_state = _limits_state()
shutter_state = _state("shutter_open", "ShutterState")
beamline_state_manager.update_available_states({"states": [limits_state]}, {})
pill = beamline_state_manager._state_pills["limits"]
@@ -309,17 +210,7 @@ def test_beamline_state_manager_adds_state_without_recreating_existing_pills(qtb
def test_beamline_state_manager_header_click_expands_pill_once(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
beamline_state_manager.update_available_states(
{
"states": [
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samx"},
}
]
},
{},
{"states": [_state("limits", "DeviceWithinLimitsState", {"device": "samx"})]}, {}
)
pill = beamline_state_manager._state_pills["limits"]
@@ -332,12 +223,7 @@ def test_beamline_state_manager_header_click_expands_pill_once(qtbot, mocked_cli
def test_beamline_state_manager_preserves_expanded_pill_on_refresh(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
state = {
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samx", "high_limit": 10.0},
}
state = _state("limits", "DeviceWithinLimitsState", {"device": "samx", "high_limit": 10.0})
beamline_state_manager.update_available_states({"states": [state]}, {})
beamline_state_manager._state_pills["limits"].set_expanded(True)
@@ -352,17 +238,7 @@ def test_beamline_state_manager_propagates_idle_card_background(qtbot, mocked_cl
qtbot, BeamlineStateManager, client=mocked_client, idle_card_background=True
)
idle_card_manager.update_available_states(
{
"states": [
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samx"},
}
]
},
{},
{"states": [_state("limits", "DeviceWithinLimitsState", {"device": "samx"})]}, {}
)
assert idle_card_manager._state_pills["limits"]._idle_card_background is True
@@ -377,18 +253,8 @@ def test_beamline_state_manager_filters_status(qtbot, mocked_client):
beamline_state_manager.update_available_states(
{
"states": [
{
"name": "shutter_open",
"title": "Shutter",
"state_type": "ShutterState",
"parameters": {"device": "samy"},
},
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samx"},
},
_state("shutter_open", "ShutterState", {"device": "samy"}),
_state("limits", "DeviceWithinLimitsState", {"device": "samx"}),
]
},
{},
@@ -433,17 +299,7 @@ def test_beamline_state_manager_filters_status(qtbot, mocked_client):
def test_beamline_state_manager_status_filter_reacts_to_state_changes(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
beamline_state_manager.update_available_states(
{
"states": [
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samx"},
}
]
},
{},
{"states": [_state("limits", "DeviceWithinLimitsState", {"device": "samx"})]}, {}
)
beamline_state_manager._selected_statuses = {"valid"}
@@ -468,18 +324,8 @@ def test_beamline_state_manager_filters_devices(qtbot, mocked_client, monkeypatc
beamline_state_manager.update_available_states(
{
"states": [
{
"name": "samx_limits",
"title": "samx",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samx"},
},
{
"name": "samy_limits",
"title": "samy",
"state_type": "DeviceWithinLimitsState",
"parameters": {"device": "samy"},
},
_state("samx_limits", "DeviceWithinLimitsState", {"device": "samx"}),
_state("samy_limits", "DeviceWithinLimitsState", {"device": "samy"}),
]
},
{},
@@ -512,27 +358,28 @@ def test_beamline_state_manager_filters_devices(qtbot, mocked_client, monkeypatc
assert captured["parent"] is beamline_state_manager
def test_beamline_state_manager_backend_echo_repopulates_expanded_pill(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
beamline_state_manager.update_available_states({"states": [_limits_state()]}, {})
pill = beamline_state_manager._state_pills["limits"]
pill.set_expanded(True)
high_limit = pill._config_form.input_widget("high_limit")
high_limit.setValue(20.0)
assert pill._update_button.isEnabled()
beamline_state_manager.update_available_states({"states": [_limits_state(high_limit=20.0)]}, {})
assert pill.is_expanded()
assert high_limit.value() == 20.0
assert not pill._update_button.isEnabled()
assert pill._config_form.dirty_fields() == set()
def test_beamline_state_manager_updates_state_parameters(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
beamline_state_manager.update_available_states(
{
"states": [
{
"name": "limits",
"title": "Limits",
"state_type": "DeviceWithinLimitsState",
"parameters": {
"device": "samx",
"signal": "samx",
"low_limit": 0.0,
"high_limit": 10.0,
"tolerance": 0.1,
},
}
]
},
{},
)
beamline_state_manager.update_available_states({"states": [_limits_state()]}, {})
class StateClient:
def __init__(self):