Skip to content

Commit 069a2d3

Browse files
committed
fix(beamline-states): better pydantic model handling
1 parent 4af048e commit 069a2d3

3 files changed

Lines changed: 101 additions & 303 deletions

File tree

bec_widgets/widgets/services/beamline_states/beamline_state_manager.py

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import sys
44
from typing import Any
55

6-
from bec_lib import bl_states
6+
from bec_lib import bl_states, messages
77
from bec_lib.endpoints import MessageEndpoints
88
from bec_qthemes import material_icon
9-
from pydantic import BaseModel
109
from qtpy.QtCore import QAbstractListModel, QModelIndex, QSize, Qt
1110
from qtpy.QtWidgets import (
1211
QAbstractItemView,
@@ -48,7 +47,7 @@ def __init__(self, parent: QWidget | None = None) -> None:
4847
super().__init__(parent)
4948
self._state_order: list[str] = []
5049
self._state_rows: dict[str, int] = {}
51-
self._state_configs: dict[str, dict[str, Any]] = {}
50+
self._state_configs: dict[str, messages.BeamlineStateConfig] = {}
5251

5352
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: # noqa: N802
5453
return 0 if parent.isValid() else len(self._state_order)
@@ -60,17 +59,17 @@ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> A
6059
if role in (Qt.ItemDataRole.DisplayRole, self.NameRole):
6160
return name
6261
if role == self.ConfigRole:
63-
return self._state_configs.get(name, {})
62+
return self._state_configs.get(name)
6463
return None
6564

6665
def flags(self, index: QModelIndex) -> Qt.ItemFlag:
6766
if not index.isValid():
6867
return Qt.ItemFlag.NoItemFlags
6968
return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
7069

71-
def set_states(self, state_configs: list[dict[str, Any]]) -> None:
72-
new_order = [str(state["name"]) for state in state_configs if state.get("name")]
73-
new_configs = {str(state["name"]): state for state in state_configs if state.get("name")}
70+
def set_states(self, state_configs: list[messages.BeamlineStateConfig]) -> None:
71+
new_order = [state.name for state in state_configs]
72+
new_configs = {state.name: state for state in state_configs}
7473

7574
for row in reversed(
7675
[row for row, name in enumerate(self._state_order) if name not in new_configs]
@@ -129,11 +128,8 @@ def createEditor( # noqa: N802
129128
self, parent: QWidget, _option: QStyleOptionViewItem, index: QModelIndex
130129
) -> QWidget:
131130
name = index.data(_BeamlineStateListModel.NameRole)
132-
state_config = index.data(_BeamlineStateListModel.ConfigRole) or {}
133-
title = state_config.get("title") or name
134-
pill = BeamlineStatePill(
135-
parent=parent, state_name=name, title=title, client=self._manager.client
136-
)
131+
state_config = index.data(_BeamlineStateListModel.ConfigRole)
132+
pill = BeamlineStatePill(parent=parent, state_name=name, client=self._manager.client)
137133
pill.idle_card_background = self._manager.idle_card_background
138134
pill.set_state_config(state_config)
139135
pill.state_changed.connect(self._manager._on_pill_state_changed)
@@ -147,9 +143,8 @@ def setEditorData(self, editor: QWidget, index: QModelIndex) -> None: # noqa: N
147143
if not isinstance(editor, BeamlineStatePill):
148144
return
149145
name = index.data(_BeamlineStateListModel.NameRole)
150-
state_config = index.data(_BeamlineStateListModel.ConfigRole) or {}
151-
title = state_config.get("title") or name
152-
editor.set_state_name(str(name), title=str(title))
146+
state_config = index.data(_BeamlineStateListModel.ConfigRole)
147+
editor.set_state_name(str(name))
153148
editor.idle_card_background = self._manager.idle_card_background
154149
editor.set_state_config(state_config)
155150

@@ -229,7 +224,7 @@ def __init__(
229224
)
230225
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
231226
self._state_pills: dict[str, BeamlineStatePill] = {}
232-
self._state_configs: dict[str, dict[str, Any]] = {}
227+
self._state_configs: dict[str, messages.BeamlineStateConfig] = {}
233228
self._state_order: list[str] = []
234229
self._selected_statuses: set[str] | None = None
235230
self._selected_devices: set[str] | None = None
@@ -414,14 +409,12 @@ def update_available_states(
414409
) -> None:
415410
"""Update the displayed pills from ``AvailableBeamlineStatesMessage`` content."""
416411
expanded_names = {name for name, pill in self._state_pills.items() if pill.is_expanded()}
417-
states = content.get("states", [])
418-
state_configs = [self._state_config_to_dict(state) for state in states]
419-
state_configs = [state for state in state_configs if state.get("name")]
412+
state_configs: list[messages.BeamlineStateConfig] = content.get("states", [])
420413
if state_configs == list(self._state_configs.values()):
421414
self._apply_filters()
422415
return
423-
self._state_configs = {str(state["name"]): state for state in state_configs}
424-
self._state_order = [str(state["name"]) for state in state_configs]
416+
self._state_configs = {state.name: state for state in state_configs}
417+
self._state_order = [state.name for state in state_configs]
425418
self._model.set_states(state_configs)
426419
self._open_persistent_editors(expanded_names)
427420
self._apply_filters()
@@ -472,7 +465,7 @@ def _is_state_visible(self, name: str) -> bool:
472465
):
473466
return False
474467

475-
device = self._state_device(self._state_configs.get(name, {}))
468+
device = self._state_device(self._state_configs.get(name))
476469
if self._selected_devices is not None and device not in self._selected_devices:
477470
return False
478471

@@ -505,14 +498,13 @@ def _update_state_parameters(
505498
)
506499
return
507500
try:
508-
parameters = config.model_dump(exclude={"name"})
509-
state_client.update_parameters(**parameters)
501+
state_client.update_parameters(**config.model_dump(exclude={"name"}))
510502
except Exception as exc:
511503
QMessageBox.warning(self, "Cannot Update State", str(exc))
512504
return
513505
pill = self._state_pills.get(state_name)
514506
if pill is not None:
515-
pill.mark_current_settings_clean(config)
507+
pill.mark_current_settings_clean()
516508

517509
@SafeSlot(str)
518510
def _remove_state_requested(self, state_name: str) -> None:
@@ -551,27 +543,10 @@ def _refresh_hidden_summary(self, hidden_count: int | None = None) -> None:
551543
)
552544

553545
@staticmethod
554-
def _state_device(state: dict[str, Any]) -> str | None:
555-
parameters = state.get("parameters")
556-
if isinstance(parameters, dict):
557-
device = parameters.get("device")
558-
else:
559-
device = state.get("device")
546+
def _state_device(state: messages.BeamlineStateConfig | None) -> str | None:
547+
device = state.parameters.get("device") if state is not None else None
560548
return str(device) if device else None
561549

562-
@staticmethod
563-
def _state_config_to_dict(state: dict[str, Any] | BaseModel) -> dict[str, Any]:
564-
if isinstance(state, dict):
565-
return state
566-
state_dict = state.model_dump()
567-
state_type = getattr(state, "state_type", None)
568-
if state_type is not None:
569-
state_dict.setdefault("state_type", state_type)
570-
title = getattr(state, "title", None)
571-
if title is not None and not state_dict.get("title"):
572-
state_dict["title"] = title
573-
return state_dict
574-
575550
def cleanup(self) -> None:
576551
self.bec_dispatcher.disconnect_slot(
577552
self.update_available_states, MessageEndpoints.available_beamline_states()

bec_widgets/widgets/services/beamline_states/beamline_state_pill.py

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import Any
44

5-
from bec_lib import bl_states
5+
from bec_lib import bl_states, messages
66
from bec_lib.endpoints import MessageEndpoints
77
from bec_qthemes import material_icon
88
from qtpy.QtCore import Qt, Signal
@@ -76,7 +76,6 @@ def __init__(
7676
self,
7777
parent: QWidget | None = None,
7878
state_name: str | None = None,
79-
title: str | None = None,
8079
client=None,
8180
config: ConnectionConfig | None = None,
8281
gui_id: str | None = None,
@@ -89,8 +88,7 @@ def __init__(
8988
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
9089
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
9190
self._state_name: str | None = None
92-
self._title: str | None = None
93-
self._state_config: dict[str, Any] = {}
91+
self._state_config: messages.BeamlineStateConfig | None = None
9492
self._status = "unknown"
9593
self._label = "No state information available."
9694
self._expanded = False
@@ -100,9 +98,9 @@ def __init__(
10098
self._settings_dirty_fields: set[str] = set()
10199
self._settings_form_stale = True
102100

103-
self._init_ui(state_name, title)
101+
self._init_ui(state_name)
104102

105-
def _init_ui(self, state_name: str | None = None, title: str | None = None) -> None:
103+
def _init_ui(self, state_name: str | None = None) -> None:
106104
self._shadow = QGraphicsDropShadowEffect(self)
107105
self._shadow.setBlurRadius(18)
108106
self._shadow.setOffset(0, 2)
@@ -212,7 +210,7 @@ def _init_ui(self, state_name: str | None = None, title: str | None = None) -> N
212210
self.setLayout(layout)
213211

214212
self._settings.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
215-
self.set_state_name(state_name, title=title)
213+
self.set_state_name(state_name)
216214
self._update_button.setEnabled(False)
217215
self._revert_button.setEnabled(False)
218216

@@ -225,15 +223,14 @@ def state_name(self) -> str | None:
225223
def state_name(self, state_name: str | None) -> None:
226224
self.set_state_name(state_name)
227225

228-
def set_state_name(self, state_name: str | None, title: str | None = None) -> None:
226+
def set_state_name(self, state_name: str | None) -> None:
229227
"""
230228
Set the BEC beamline state this pill displays.
231229
232230
Args:
233231
state_name: State name as published by ``AvailableBeamlineStatesMessage``.
234-
title: Optional human-readable title for the state.
235232
"""
236-
if state_name == self._state_name and title == self._title:
233+
if state_name == self._state_name:
237234
return
238235

239236
if self._state_name is not None:
@@ -242,8 +239,7 @@ def set_state_name(self, state_name: str | None, title: str | None = None) -> No
242239
)
243240

244241
self._state_name = state_name
245-
self._title = title
246-
self._name_label.setText(title or state_name or "Beamline state")
242+
self._name_label.setText(state_name or "Beamline state")
247243

248244
if self._state_name is None:
249245
self._set_visual_state("unknown", "No beamline state selected.")
@@ -255,13 +251,13 @@ def set_state_name(self, state_name: str | None, title: str | None = None) -> No
255251
self.update_state, MessageEndpoints.beamline_state(self._state_name)
256252
)
257253

258-
def set_state_config(self, state_config: dict[str, Any]) -> None:
254+
def set_state_config(self, state_config: messages.BeamlineStateConfig | None) -> None:
259255
"""Set the editable BEC state configuration displayed by the expanded panel."""
260256
self._state_config = state_config
261257
self._settings_form_stale = True
262258
if self._config_form is not None:
263259
self._populate_settings()
264-
self._mark_settings_clean_from_current()
260+
self.mark_current_settings_clean()
265261

266262
@SafeProperty(bool, default=False)
267263
def idle_card_background(self) -> bool:
@@ -439,13 +435,13 @@ def _ensure_config_form(
439435
def _ensure_settings_form_current(self) -> PydanticWidgetForm:
440436
if self._settings_form_stale:
441437
self._populate_settings()
442-
self._mark_settings_clean_from_current()
438+
self.mark_current_settings_clean()
443439
return self._ensure_config_form()
444440

445441
def _populate_settings(self) -> None:
446442
self._populating_settings = True
447443
try:
448-
state_type = str(self._state_config.get("state_type") or "")
444+
state_type = self._state_config.state_type if self._state_config is not None else ""
449445
config_class = None
450446
for state_class in SUPPORTED_BEAMLINE_STATES:
451447
if state_type in {state_class.__name__, state_class.CONFIG_CLASS.state_type}:
@@ -468,25 +464,8 @@ def edited_config(self) -> bl_states.BeamlineStateConfig:
468464
config = self._ensure_settings_form_current().model_instance()
469465
return config # type: ignore[return-value]
470466

471-
def mark_current_settings_clean(
472-
self, config: bl_states.BeamlineStateConfig | None = None
473-
) -> None:
467+
def mark_current_settings_clean(self) -> None:
474468
"""Mark the current editor values as saved."""
475-
if config is None:
476-
parameters = self._ensure_config_form().raw_editable_data()
477-
else:
478-
parameters = config.model_dump(exclude={"name"})
479-
if self._state_config:
480-
state_parameters = self._state_config.get("parameters")
481-
if isinstance(state_parameters, dict):
482-
state_parameters.update(parameters)
483-
else:
484-
self._state_config.update(parameters)
485-
if "title" in parameters:
486-
self._state_config["title"] = parameters["title"]
487-
self._mark_settings_clean_from_current()
488-
489-
def _mark_settings_clean_from_current(self) -> None:
490469
config_form = self._ensure_config_form()
491470
self._settings_baseline = config_form.raw_editable_data()
492471
config_form.mark_clean()
@@ -561,15 +540,12 @@ def _state_data_for_form(
561540
self, config_class: type[bl_states.BeamlineStateConfig]
562541
) -> dict[str, Any]:
563542
data: dict[str, Any] = {}
564-
parameters = self._state_config.get("parameters")
565-
parameter_values = parameters if isinstance(parameters, dict) else {}
543+
parameters = self._state_config.parameters if self._state_config is not None else {}
566544
for name in config_class.model_fields:
567545
if name == "name":
568546
data[name] = self._state_name
569-
elif name in parameter_values:
570-
data[name] = parameter_values[name]
571-
elif name in self._state_config:
572-
data[name] = self._state_config[name]
547+
elif name in parameters:
548+
data[name] = parameters[name]
573549
return data
574550

575551
@staticmethod

0 commit comments

Comments
 (0)