mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
refactor: rearrange base of metadata forms for generic use
This commit is contained in:
0
bec_widgets/utils/forms_from_types/__init__.py
Normal file
0
bec_widgets/utils/forms_from_types/__init__.py
Normal file
182
bec_widgets/utils/forms_from_types/forms.py
Normal file
182
bec_widgets/utils/forms_from_types/forms.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from types import NoneType
|
||||||
|
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_qthemes import material_icon
|
||||||
|
from pydantic import BaseModel, ValidationError
|
||||||
|
from qtpy.QtCore import Signal # type: ignore
|
||||||
|
from qtpy.QtWidgets import QGridLayout, QLabel, QLayout, QVBoxLayout, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
||||||
|
from bec_widgets.utils.forms_from_types.items import FormItemSpec, widget_from_type
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class TypedForm(BECWidget, QWidget):
|
||||||
|
PLUGIN = True
|
||||||
|
ICON_NAME = "list_alt"
|
||||||
|
|
||||||
|
value_changed = Signal()
|
||||||
|
|
||||||
|
RPC = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
items: list[tuple[str, type]] | None = None,
|
||||||
|
form_item_specs: list[FormItemSpec] | None = None,
|
||||||
|
parent=None,
|
||||||
|
client=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Widget with a list of form items based on a list of types.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
items (list[tuple[str, type]]): list of tuples of a name for the field and its type.
|
||||||
|
Should be a type supported by the logic in items.py
|
||||||
|
form_item_specs (list[FormItemSpec]): list of form item specs, equivalent to items.
|
||||||
|
only one of items or form_item_specs should be
|
||||||
|
supplied.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if (items is not None and form_item_specs is not None) or (
|
||||||
|
items is None and form_item_specs is None
|
||||||
|
):
|
||||||
|
raise ValueError("Must specify one and only one of items and form_item_specs")
|
||||||
|
super().__init__(parent=parent, client=client, **kwargs)
|
||||||
|
self._items = (
|
||||||
|
form_item_specs
|
||||||
|
if form_item_specs is not None
|
||||||
|
else [
|
||||||
|
FormItemSpec(name=name, item_type=item_type)
|
||||||
|
for name, item_type in items # type: ignore
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self._layout = QVBoxLayout()
|
||||||
|
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.setLayout(self._layout)
|
||||||
|
|
||||||
|
self._form_grid_container = QWidget(parent=self)
|
||||||
|
self._form_grid = QWidget(parent=self._form_grid_container)
|
||||||
|
self._layout.addWidget(self._form_grid_container)
|
||||||
|
self._form_grid_container.setLayout(QVBoxLayout())
|
||||||
|
self._form_grid.setLayout(self._new_grid_layout())
|
||||||
|
|
||||||
|
self.populate()
|
||||||
|
|
||||||
|
def populate(self):
|
||||||
|
self._clear_grid()
|
||||||
|
for r, item in enumerate(self._items):
|
||||||
|
self._add_griditem(item, r)
|
||||||
|
|
||||||
|
def _add_griditem(self, item: FormItemSpec, row: int):
|
||||||
|
grid = self._form_grid.layout()
|
||||||
|
label = QLabel(item.name)
|
||||||
|
label.setProperty("_model_field_name", item.name)
|
||||||
|
label.setToolTip(item.info.description or item.name)
|
||||||
|
grid.addWidget(label, row, 0)
|
||||||
|
widget = widget_from_type(item.item_type)(parent=self, spec=item)
|
||||||
|
widget.valueChanged.connect(self.value_changed)
|
||||||
|
grid.addWidget(widget, row, 1)
|
||||||
|
|
||||||
|
def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
|
||||||
|
grid: QGridLayout = self._form_grid.layout() # type: ignore
|
||||||
|
return {
|
||||||
|
grid.itemAtPosition(i, 0)
|
||||||
|
.widget()
|
||||||
|
.property("_model_field_name"): grid.itemAtPosition(i, 1)
|
||||||
|
.widget()
|
||||||
|
.getValue() # type: ignore # we only add 'DynamicFormItem's here
|
||||||
|
for i in range(grid.rowCount())
|
||||||
|
}
|
||||||
|
|
||||||
|
def _clear_grid(self):
|
||||||
|
if (old_layout := self._form_grid.layout()) is not None:
|
||||||
|
while old_layout.count():
|
||||||
|
item = old_layout.takeAt(0)
|
||||||
|
widget = item.widget()
|
||||||
|
if widget is not None:
|
||||||
|
widget.deleteLater()
|
||||||
|
old_layout.deleteLater()
|
||||||
|
self._form_grid.deleteLater()
|
||||||
|
self._form_grid = QWidget()
|
||||||
|
|
||||||
|
self._form_grid.setLayout(self._new_grid_layout())
|
||||||
|
self._form_grid_container.layout().addWidget(self._form_grid)
|
||||||
|
|
||||||
|
self._form_grid.adjustSize()
|
||||||
|
self._form_grid_container.adjustSize()
|
||||||
|
self.adjustSize()
|
||||||
|
|
||||||
|
def _new_grid_layout(self):
|
||||||
|
new_grid = QGridLayout()
|
||||||
|
new_grid.setContentsMargins(0, 0, 0, 0)
|
||||||
|
new_grid.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
|
||||||
|
return new_grid
|
||||||
|
|
||||||
|
|
||||||
|
class PydanticModelForm(TypedForm):
|
||||||
|
metadata_updated = Signal(dict)
|
||||||
|
metadata_cleared = Signal(NoneType)
|
||||||
|
|
||||||
|
def __init__(self, metadata_model: type[BaseModel], parent=None, client=None, **kwargs):
|
||||||
|
"""
|
||||||
|
A form generated from a pydantic model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metadata_model (type[BaseModel]): the model class for which to generate a form.
|
||||||
|
"""
|
||||||
|
self._md_schema = metadata_model
|
||||||
|
super().__init__(form_item_specs=self._form_item_specs(), parent=parent, client=client)
|
||||||
|
|
||||||
|
self._validity = CompactPopupWidget()
|
||||||
|
self._validity.compact_view = True # type: ignore
|
||||||
|
self._validity.label = "Metadata validity" # type: ignore
|
||||||
|
self._validity.compact_show_popup.setIcon(
|
||||||
|
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
|
||||||
|
)
|
||||||
|
self._validity_message = QLabel("Not yet validated")
|
||||||
|
self._validity.addWidget(self._validity_message)
|
||||||
|
self._layout.addWidget(self._validity)
|
||||||
|
self.value_changed.connect(self.validate_form)
|
||||||
|
|
||||||
|
def set_schema(self, schema: type[BaseModel]):
|
||||||
|
self._md_schema = schema
|
||||||
|
self.populate()
|
||||||
|
|
||||||
|
def _form_item_specs(self):
|
||||||
|
return [
|
||||||
|
FormItemSpec(name=name, info=info, item_type=info.annotation)
|
||||||
|
for name, info in self._md_schema.model_fields.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
def update_items_from_schema(self):
|
||||||
|
self._items = self._form_item_specs()
|
||||||
|
|
||||||
|
def populate(self):
|
||||||
|
self.update_items_from_schema()
|
||||||
|
super().populate()
|
||||||
|
|
||||||
|
def get_form_data(self):
|
||||||
|
"""Get the entered metadata as a dict."""
|
||||||
|
return self._dict_from_grid()
|
||||||
|
|
||||||
|
def validate_form(self, *_) -> bool:
|
||||||
|
"""validate the currently entered metadata against the pydantic schema.
|
||||||
|
If successful, returns on metadata_emitted and returns true.
|
||||||
|
Otherwise, emits on metadata_cleared and returns false."""
|
||||||
|
try:
|
||||||
|
metadata_dict = self.get_form_data()
|
||||||
|
self._md_schema.model_validate(metadata_dict)
|
||||||
|
self._validity.set_global_state("success")
|
||||||
|
self._validity_message.setText("No errors!")
|
||||||
|
self.metadata_updated.emit(metadata_dict)
|
||||||
|
return True
|
||||||
|
except ValidationError as e:
|
||||||
|
self._validity.set_global_state("emergency")
|
||||||
|
self._validity_message.setText(str(e))
|
||||||
|
self.metadata_cleared.emit(None)
|
||||||
|
return False
|
@ -2,11 +2,13 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import TYPE_CHECKING, Callable, get_args
|
from types import UnionType
|
||||||
|
from typing import Callable, Protocol
|
||||||
|
|
||||||
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 pydantic import BaseModel, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from pydantic.fields import FieldInfo
|
||||||
from qtpy.QtCore import Signal # type: ignore
|
from qtpy.QtCore import Signal # type: ignore
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
@ -33,12 +35,22 @@ from bec_widgets.widgets.editors.scan_metadata._util import (
|
|||||||
field_precision,
|
field_precision,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
|
||||||
from pydantic.fields import FieldInfo
|
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class FormItemSpec(BaseModel):
|
||||||
|
"""
|
||||||
|
The specification for an item in a dynamically generated form. Uses a pydantic FieldInfo
|
||||||
|
to store most annotation info, since one of the main purposes is to store data for
|
||||||
|
forms genrated from pydantic models, but can also be composed from other sources or by hand.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
|
item_type: type | UnionType
|
||||||
|
name: str
|
||||||
|
info: FieldInfo = FieldInfo()
|
||||||
|
|
||||||
|
|
||||||
class ClearableBoolEntry(QWidget):
|
class ClearableBoolEntry(QWidget):
|
||||||
stateChanged = Signal()
|
stateChanged = Signal()
|
||||||
|
|
||||||
@ -82,21 +94,20 @@ class ClearableBoolEntry(QWidget):
|
|||||||
self._false.setToolTip(tooltip)
|
self._false.setToolTip(tooltip)
|
||||||
|
|
||||||
|
|
||||||
class MetadataWidget(QWidget):
|
class DynamicFormItem(QWidget):
|
||||||
|
|
||||||
valueChanged = Signal()
|
valueChanged = Signal()
|
||||||
|
|
||||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._info = info
|
self._spec = spec
|
||||||
self._layout = QHBoxLayout()
|
self._layout = QHBoxLayout()
|
||||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self._layout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize)
|
self._layout.setSizeConstraint(QLayout.SizeConstraint.SetMaximumSize)
|
||||||
self._default = field_default(self._info)
|
self._default = field_default(self._spec.info)
|
||||||
self._desc = self._info.description
|
self._desc = self._spec.info.description
|
||||||
self.setLayout(self._layout)
|
self.setLayout(self._layout)
|
||||||
self._add_main_widget()
|
self._add_main_widget()
|
||||||
if clearable_required(info):
|
if clearable_required(spec.info):
|
||||||
self._add_clear_button()
|
self._add_clear_button()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -127,15 +138,15 @@ class MetadataWidget(QWidget):
|
|||||||
self.valueChanged.emit()
|
self.valueChanged.emit()
|
||||||
|
|
||||||
|
|
||||||
class StrMetadataField(MetadataWidget):
|
class StrMetadataField(DynamicFormItem):
|
||||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||||
super().__init__(info, parent)
|
super().__init__(parent=parent, spec=spec)
|
||||||
self._main_widget.textChanged.connect(self._value_changed)
|
self._main_widget.textChanged.connect(self._value_changed)
|
||||||
|
|
||||||
def _add_main_widget(self) -> None:
|
def _add_main_widget(self) -> None:
|
||||||
self._main_widget = QLineEdit()
|
self._main_widget = QLineEdit()
|
||||||
self._layout.addWidget(self._main_widget)
|
self._layout.addWidget(self._main_widget)
|
||||||
min_length, max_length = field_minlen(self._info), field_maxlen(self._info)
|
min_length, max_length = (field_minlen(self._spec.info), field_maxlen(self._spec.info))
|
||||||
if max_length:
|
if max_length:
|
||||||
self._main_widget.setMaxLength(max_length)
|
self._main_widget.setMaxLength(max_length)
|
||||||
self._main_widget.setToolTip(
|
self._main_widget.setToolTip(
|
||||||
@ -156,15 +167,15 @@ class StrMetadataField(MetadataWidget):
|
|||||||
self._main_widget.setText(value)
|
self._main_widget.setText(value)
|
||||||
|
|
||||||
|
|
||||||
class IntMetadataField(MetadataWidget):
|
class IntMetadataField(DynamicFormItem):
|
||||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||||
super().__init__(info, parent)
|
super().__init__(parent=parent, spec=spec)
|
||||||
self._main_widget.textChanged.connect(self._value_changed)
|
self._main_widget.textChanged.connect(self._value_changed)
|
||||||
|
|
||||||
def _add_main_widget(self) -> None:
|
def _add_main_widget(self) -> None:
|
||||||
self._main_widget = QSpinBox()
|
self._main_widget = QSpinBox()
|
||||||
self._layout.addWidget(self._main_widget)
|
self._layout.addWidget(self._main_widget)
|
||||||
min_, max_ = field_limits(self._info, int)
|
min_, max_ = field_limits(self._spec.info, int)
|
||||||
self._main_widget.setMinimum(min_)
|
self._main_widget.setMinimum(min_)
|
||||||
self._main_widget.setMaximum(max_)
|
self._main_widget.setMaximum(max_)
|
||||||
self._main_widget.setToolTip(f"(range {min_} to {max_}){self._describe()}")
|
self._main_widget.setToolTip(f"(range {min_} to {max_}){self._describe()}")
|
||||||
@ -185,18 +196,18 @@ class IntMetadataField(MetadataWidget):
|
|||||||
self._main_widget.setValue(value)
|
self._main_widget.setValue(value)
|
||||||
|
|
||||||
|
|
||||||
class FloatDecimalMetadataField(MetadataWidget):
|
class FloatDecimalMetadataField(DynamicFormItem):
|
||||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None:
|
||||||
super().__init__(info, parent)
|
super().__init__(parent=parent, spec=spec)
|
||||||
self._main_widget.textChanged.connect(self._value_changed)
|
self._main_widget.textChanged.connect(self._value_changed)
|
||||||
|
|
||||||
def _add_main_widget(self) -> None:
|
def _add_main_widget(self) -> None:
|
||||||
self._main_widget = QDoubleSpinBox()
|
self._main_widget = QDoubleSpinBox()
|
||||||
self._layout.addWidget(self._main_widget)
|
self._layout.addWidget(self._main_widget)
|
||||||
min_, max_ = field_limits(self._info, int)
|
min_, max_ = field_limits(self._spec.info, int)
|
||||||
self._main_widget.setMinimum(min_)
|
self._main_widget.setMinimum(min_)
|
||||||
self._main_widget.setMaximum(max_)
|
self._main_widget.setMaximum(max_)
|
||||||
precision = field_precision(self._info)
|
precision = field_precision(self._spec.info)
|
||||||
if precision:
|
if precision:
|
||||||
self._main_widget.setDecimals(precision)
|
self._main_widget.setDecimals(precision)
|
||||||
minstr = f"{float(min_):.3f}" if abs(min_) <= 1000 else f"{float(min_):.3e}"
|
minstr = f"{float(min_):.3f}" if abs(min_) <= 1000 else f"{float(min_):.3e}"
|
||||||
@ -219,13 +230,13 @@ class FloatDecimalMetadataField(MetadataWidget):
|
|||||||
self._main_widget.setValue(value)
|
self._main_widget.setValue(value)
|
||||||
|
|
||||||
|
|
||||||
class BoolMetadataField(MetadataWidget):
|
class BoolMetadataField(DynamicFormItem):
|
||||||
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
|
def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
|
||||||
super().__init__(info, parent)
|
super().__init__(parent=parent, spec=spec)
|
||||||
self._main_widget.stateChanged.connect(self._value_changed)
|
self._main_widget.stateChanged.connect(self._value_changed)
|
||||||
|
|
||||||
def _add_main_widget(self) -> None:
|
def _add_main_widget(self) -> None:
|
||||||
if clearable_required(self._info):
|
if clearable_required(self._spec.info):
|
||||||
self._main_widget = ClearableBoolEntry()
|
self._main_widget = ClearableBoolEntry()
|
||||||
else:
|
else:
|
||||||
self._main_widget = QCheckBox()
|
self._main_widget = QCheckBox()
|
||||||
@ -240,7 +251,7 @@ class BoolMetadataField(MetadataWidget):
|
|||||||
self._main_widget.setChecked(value)
|
self._main_widget.setChecked(value)
|
||||||
|
|
||||||
|
|
||||||
def widget_from_type(annotation: type | None) -> Callable[[FieldInfo], MetadataWidget]:
|
def widget_from_type(annotation: type | UnionType | None) -> type[DynamicFormItem]:
|
||||||
if annotation in [str, str | None]:
|
if annotation in [str, str | None]:
|
||||||
return StrMetadataField
|
return StrMetadataField
|
||||||
if annotation in [int, int | None]:
|
if annotation in [int, int | None]:
|
@ -64,7 +64,6 @@ class ScanControl(BECWidget, QWidget):
|
|||||||
default_scan: str | None = None,
|
default_scan: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
|
||||||
if config is None:
|
if config is None:
|
||||||
config = ScanControlConfig(
|
config = ScanControlConfig(
|
||||||
widget_class=self.__class__.__name__, allowed_scans=allowed_scans
|
widget_class=self.__class__.__name__, allowed_scans=allowed_scans
|
||||||
|
@ -16,12 +16,20 @@ from qtpy.QtWidgets import (
|
|||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
|
|
||||||
|
|
||||||
class AdditionalMetadataTableModel(QAbstractTableModel):
|
class DictBackedTableModel(QAbstractTableModel):
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
|
"""A model to go with DictBackedTable, which represents key-value pairs
|
||||||
|
to be displayed in a TreeWidget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (list[list[str]]): list of key-value pairs to initialise with"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._data: list[list[str]] = data
|
self._data: list[list[str]] = data
|
||||||
self._disallowed_keys: list[str] = []
|
self._disallowed_keys: list[str] = []
|
||||||
|
|
||||||
|
# pylint: disable=missing-function-docstring
|
||||||
|
# see QAbstractTableModel documentation for these methods
|
||||||
|
|
||||||
def headerData(
|
def headerData(
|
||||||
self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole()
|
self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole()
|
||||||
) -> Any:
|
) -> Any:
|
||||||
@ -49,6 +57,10 @@ class AdditionalMetadataTableModel(QAbstractTableModel):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def update_disallowed_keys(self, keys: list[str]):
|
def update_disallowed_keys(self, keys: list[str]):
|
||||||
|
"""Set the list of keys which may not be used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keys (list[str]): list of keys which are forbidden."""
|
||||||
self._disallowed_keys = keys
|
self._disallowed_keys = keys
|
||||||
for i, item in enumerate(self._data):
|
for i, item in enumerate(self._data):
|
||||||
if item[0] in self._disallowed_keys:
|
if item[0] in self._disallowed_keys:
|
||||||
@ -95,16 +107,21 @@ class AdditionalMetadataTableModel(QAbstractTableModel):
|
|||||||
return dict(self._data)
|
return dict(self._data)
|
||||||
|
|
||||||
|
|
||||||
class AdditionalMetadataTable(QWidget):
|
class DictBackedTable(QWidget):
|
||||||
|
|
||||||
delete_rows = Signal(list)
|
delete_rows = Signal(list)
|
||||||
|
|
||||||
def __init__(self, initial_data: list[list[str]]):
|
def __init__(self, initial_data: list[list[str]]):
|
||||||
|
"""Widget which uses a DictBackedTableModel to display an editable table
|
||||||
|
which can be extracted as a dict.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
initial_data (list[list[str]]): list of key-value pairs to initialise with
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._layout = QHBoxLayout()
|
self._layout = QHBoxLayout()
|
||||||
self.setLayout(self._layout)
|
self.setLayout(self._layout)
|
||||||
self._table_model = AdditionalMetadataTableModel(initial_data)
|
self._table_model = DictBackedTableModel(initial_data)
|
||||||
self._table_view = QTreeView()
|
self._table_view = QTreeView()
|
||||||
self._table_view.setModel(self._table_model)
|
self._table_view.setModel(self._table_model)
|
||||||
self._table_view.setSizePolicy(
|
self._table_view.setSizePolicy(
|
||||||
@ -126,15 +143,21 @@ class AdditionalMetadataTable(QWidget):
|
|||||||
self.delete_rows.connect(self._table_model.delete_rows)
|
self.delete_rows.connect(self._table_model.delete_rows)
|
||||||
|
|
||||||
def delete_selected_rows(self):
|
def delete_selected_rows(self):
|
||||||
|
"""Delete rows which are part of the selection model"""
|
||||||
cells: list[QModelIndex] = self._table_view.selectionModel().selectedIndexes()
|
cells: list[QModelIndex] = self._table_view.selectionModel().selectedIndexes()
|
||||||
row_indices = list({r.row() for r in cells})
|
row_indices = list({r.row() for r in cells})
|
||||||
if row_indices:
|
if row_indices:
|
||||||
self.delete_rows.emit(row_indices)
|
self.delete_rows.emit(row_indices)
|
||||||
|
|
||||||
def dump_dict(self):
|
def dump_dict(self):
|
||||||
|
"""Get the current content of the table as a dict"""
|
||||||
return self._table_model.dump_dict()
|
return self._table_model.dump_dict()
|
||||||
|
|
||||||
def update_disallowed_keys(self, keys: list[str]):
|
def update_disallowed_keys(self, keys: list[str]):
|
||||||
|
"""Set the list of keys which may not be used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keys (list[str]): list of keys which are forbidden."""
|
||||||
self._table_model.update_disallowed_keys(keys)
|
self._table_model.update_disallowed_keys(keys)
|
||||||
|
|
||||||
|
|
||||||
@ -144,6 +167,6 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
set_theme("dark")
|
set_theme("dark")
|
||||||
|
|
||||||
window = AdditionalMetadataTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
window = DictBackedTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
||||||
window.show()
|
window.show()
|
||||||
app.exec()
|
app.exec()
|
@ -1,7 +0,0 @@
|
|||||||
from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
|
|
||||||
AdditionalMetadataTable,
|
|
||||||
AdditionalMetadataTableModel,
|
|
||||||
)
|
|
||||||
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
|
|
||||||
|
|
||||||
__all__ = ["ScanMetadata", "AdditionalMetadataTable", "AdditionalMetadataTableModel"]
|
|
||||||
|
@ -1,52 +1,21 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from types import NoneType
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from bec_lib.metadata_schema import get_metadata_schema_for_scan
|
from bec_lib.metadata_schema import get_metadata_schema_for_scan
|
||||||
from bec_qthemes import material_icon
|
from pydantic import Field
|
||||||
from pydantic import Field, ValidationError
|
from qtpy.QtWidgets import QApplication, QComboBox, QHBoxLayout, QVBoxLayout, QWidget
|
||||||
from qtpy.QtCore import Signal # type: ignore
|
|
||||||
from qtpy.QtWidgets import (
|
|
||||||
QApplication,
|
|
||||||
QComboBox,
|
|
||||||
QGridLayout,
|
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QLayout,
|
|
||||||
QVBoxLayout,
|
|
||||||
QWidget,
|
|
||||||
)
|
|
||||||
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
|
||||||
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
|
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
|
||||||
from bec_widgets.widgets.editors.scan_metadata._metadata_widgets import widget_from_type
|
from bec_widgets.utils.forms_from_types.forms import PydanticModelForm
|
||||||
from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
|
from bec_widgets.widgets.editors.dict_backed_table import DictBackedTable
|
||||||
AdditionalMetadataTable,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
|
||||||
from pydantic.fields import FieldInfo
|
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
class ScanMetadata(BECWidget, QWidget):
|
class ScanMetadata(PydanticModelForm):
|
||||||
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the
|
|
||||||
metadata schema registry supplied in the plugin repo to find pydantic models
|
|
||||||
associated with the scan type. Sets limits for numerical values if specified."""
|
|
||||||
|
|
||||||
PLUGIN = True
|
|
||||||
ICON_NAME = "list_alt"
|
|
||||||
|
|
||||||
metadata_updated = Signal(dict)
|
|
||||||
metadata_cleared = Signal(NoneType)
|
|
||||||
RPC = False
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent=None,
|
parent=None,
|
||||||
@ -55,117 +24,35 @@ class ScanMetadata(BECWidget, QWidget):
|
|||||||
initial_extras: list[list[str]] | None = None,
|
initial_extras: list[list[str]] | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(parent=parent, client=client, **kwargs)
|
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the
|
||||||
|
metadata schema registry supplied in the plugin repo to find pydantic models
|
||||||
|
associated with the scan type. Sets limits for numerical values if specified.
|
||||||
|
|
||||||
self.set_schema(scan_name)
|
Args:
|
||||||
|
scan_name (str): The scan for which to generate a metadata form
|
||||||
self._layout = QVBoxLayout()
|
Initial_extras (list[list[str]]): Initial data with which to populate the additional
|
||||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
metadata table - inner lists should be key-value pairs
|
||||||
self.setLayout(self._layout)
|
"""
|
||||||
|
|
||||||
self._required_md_box = ExpandableGroupFrame("Scan schema metadata")
|
|
||||||
self._layout.addWidget(self._required_md_box)
|
|
||||||
self._required_md_box_layout = QHBoxLayout()
|
|
||||||
self._required_md_box.set_layout(self._required_md_box_layout)
|
|
||||||
|
|
||||||
self._md_grid = QWidget()
|
|
||||||
self._required_md_box_layout.addWidget(self._md_grid)
|
|
||||||
self._grid_container = QVBoxLayout()
|
|
||||||
self._md_grid.setLayout(self._grid_container)
|
|
||||||
self._new_grid_layout()
|
|
||||||
self._grid_container.addLayout(self._md_grid_layout)
|
|
||||||
|
|
||||||
|
# self.populate() gets called in super().__init__
|
||||||
|
# so make sure self._additional_metadata exists
|
||||||
self._additional_md_box = ExpandableGroupFrame("Additional metadata", expanded=False)
|
self._additional_md_box = ExpandableGroupFrame("Additional metadata", expanded=False)
|
||||||
self._layout.addWidget(self._additional_md_box)
|
|
||||||
self._additional_md_box_layout = QHBoxLayout()
|
self._additional_md_box_layout = QHBoxLayout()
|
||||||
self._additional_md_box.set_layout(self._additional_md_box_layout)
|
self._additional_md_box.set_layout(self._additional_md_box_layout)
|
||||||
|
|
||||||
self._additional_metadata = AdditionalMetadataTable(initial_extras or [])
|
self._additional_metadata = DictBackedTable(initial_extras or [])
|
||||||
self._additional_md_box_layout.addWidget(self._additional_metadata)
|
|
||||||
|
|
||||||
self._validity = CompactPopupWidget()
|
|
||||||
self._validity.compact_view = True # type: ignore
|
|
||||||
self._validity.label = "Metadata validity" # type: ignore
|
|
||||||
self._validity.compact_show_popup.setIcon(
|
|
||||||
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
|
|
||||||
)
|
|
||||||
self._validity_message = QLabel("Not yet validated")
|
|
||||||
self._validity.addWidget(self._validity_message)
|
|
||||||
self._layout.addWidget(self._validity)
|
|
||||||
|
|
||||||
self.populate()
|
|
||||||
|
|
||||||
@SafeSlot(str)
|
|
||||||
def update_with_new_scan(self, scan_name: str):
|
|
||||||
self.set_schema(scan_name)
|
|
||||||
self.populate()
|
|
||||||
self.validate_form()
|
|
||||||
|
|
||||||
def validate_form(self, *_) -> bool:
|
|
||||||
"""validate the currently entered metadata against the pydantic schema.
|
|
||||||
If successful, returns on metadata_emitted and returns true.
|
|
||||||
Otherwise, emits on metadata_cleared and returns false."""
|
|
||||||
try:
|
|
||||||
metadata_dict = self.get_full_model_dict()
|
|
||||||
self._md_schema.model_validate(metadata_dict)
|
|
||||||
self._validity.set_global_state("success")
|
|
||||||
self._validity_message.setText("No errors!")
|
|
||||||
self.metadata_updated.emit(metadata_dict)
|
|
||||||
except ValidationError as e:
|
|
||||||
self._validity.set_global_state("emergency")
|
|
||||||
self._validity_message.setText(str(e))
|
|
||||||
self.metadata_cleared.emit(None)
|
|
||||||
|
|
||||||
def get_full_model_dict(self):
|
|
||||||
"""Get the entered metadata as a dict"""
|
|
||||||
return self._additional_metadata.dump_dict() | self._dict_from_grid()
|
|
||||||
|
|
||||||
def set_schema(self, scan_name: str | None = None):
|
|
||||||
self._scan_name = scan_name or ""
|
self._scan_name = scan_name or ""
|
||||||
self._md_schema = get_metadata_schema_for_scan(self._scan_name)
|
self._md_schema = get_metadata_schema_for_scan(self._scan_name)
|
||||||
|
|
||||||
def populate(self):
|
super().__init__(self._md_schema, parent, client, **kwargs)
|
||||||
self._clear_grid()
|
|
||||||
self._populate()
|
|
||||||
|
|
||||||
def _populate(self):
|
self._layout.addWidget(self._additional_md_box)
|
||||||
self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys()))
|
self._additional_md_box_layout.addWidget(self._additional_metadata)
|
||||||
for i, (field_name, info) in enumerate(self._md_schema.model_fields.items()):
|
|
||||||
self._add_griditem(field_name, info, i)
|
|
||||||
|
|
||||||
def _add_griditem(self, field_name: str, info: FieldInfo, row: int):
|
@SafeSlot(str)
|
||||||
grid = self._md_grid_layout
|
def update_with_new_scan(self, scan_name: str):
|
||||||
label = QLabel(info.title or field_name)
|
self.set_schema_from_scan(scan_name)
|
||||||
label.setProperty("_model_field_name", field_name)
|
self.validate_form()
|
||||||
label.setToolTip(info.description or field_name)
|
|
||||||
grid.addWidget(label, row, 0)
|
|
||||||
widget = widget_from_type(info.annotation)(info)
|
|
||||||
widget.valueChanged.connect(self.validate_form)
|
|
||||||
grid.addWidget(widget, row, 1)
|
|
||||||
|
|
||||||
def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
|
|
||||||
grid = self._md_grid_layout
|
|
||||||
return {
|
|
||||||
grid.itemAtPosition(i, 0).widget().property("_model_field_name"): grid.itemAtPosition(i, 1).widget().getValue() # type: ignore # we only add 'MetadataWidget's here
|
|
||||||
for i in range(grid.rowCount())
|
|
||||||
}
|
|
||||||
|
|
||||||
def _clear_grid(self):
|
|
||||||
while self._md_grid_layout.count():
|
|
||||||
item = self._md_grid_layout.takeAt(0)
|
|
||||||
widget = item.widget()
|
|
||||||
if widget is not None:
|
|
||||||
widget.deleteLater()
|
|
||||||
self._md_grid_layout.deleteLater()
|
|
||||||
self._new_grid_layout()
|
|
||||||
self._grid_container.addLayout(self._md_grid_layout)
|
|
||||||
self._md_grid.adjustSize()
|
|
||||||
self.adjustSize()
|
|
||||||
|
|
||||||
def _new_grid_layout(self):
|
|
||||||
self._md_grid_layout = QGridLayout()
|
|
||||||
self._md_grid_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self._md_grid_layout.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)
|
|
||||||
|
|
||||||
@SafeProperty(bool)
|
@SafeProperty(bool)
|
||||||
def hide_optional_metadata(self): # type: ignore
|
def hide_optional_metadata(self): # type: ignore
|
||||||
@ -181,8 +68,25 @@ class ScanMetadata(BECWidget, QWidget):
|
|||||||
"""
|
"""
|
||||||
self._additional_md_box.setVisible(not hide)
|
self._additional_md_box.setVisible(not hide)
|
||||||
|
|
||||||
|
def get_form_data(self):
|
||||||
|
"""Get the entered metadata as a dict"""
|
||||||
|
return self._additional_metadata.dump_dict() | self._dict_from_grid()
|
||||||
|
|
||||||
|
def populate(self):
|
||||||
|
self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys()))
|
||||||
|
super().populate()
|
||||||
|
|
||||||
|
def set_schema_from_scan(self, scan_name: str | None):
|
||||||
|
self._scan_name = scan_name or ""
|
||||||
|
self.set_schema(get_metadata_schema_for_scan(self._scan_name))
|
||||||
|
self.populate()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
# pylint: disable=disallowed-name
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from bec_lib.metadata_schema import BasicScanMetadata
|
from bec_lib.metadata_schema import BasicScanMetadata
|
||||||
@ -213,7 +117,6 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
"bec_lib.metadata_schema._get_metadata_schema_registry",
|
"bec_lib.metadata_schema._get_metadata_schema_registry",
|
||||||
lambda: {"scan1": ExampleSchema1, "scan2": ExampleSchema2, "scan3": ExampleSchema3},
|
lambda: {"scan1": ExampleSchema1, "scan2": ExampleSchema2, "scan3": ExampleSchema3},
|
||||||
):
|
):
|
||||||
|
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
w = QWidget()
|
w = QWidget()
|
||||||
selection = QComboBox()
|
selection = QComboBox()
|
||||||
|
@ -5,12 +5,17 @@ import pytest
|
|||||||
from bec_lib.endpoints import MessageEndpoints
|
from bec_lib.endpoints import MessageEndpoints
|
||||||
from bec_lib.messages import AvailableResourceMessage, ScanQueueHistoryMessage, ScanQueueMessage
|
from bec_lib.messages import AvailableResourceMessage, ScanQueueHistoryMessage, ScanQueueMessage
|
||||||
|
|
||||||
|
from bec_widgets.utils.forms_from_types.items import StrMetadataField
|
||||||
from bec_widgets.utils.widget_io import WidgetIO
|
from bec_widgets.utils.widget_io import WidgetIO
|
||||||
from bec_widgets.widgets.control.scan_control import ScanControl
|
from bec_widgets.widgets.control.scan_control import ScanControl
|
||||||
from bec_widgets.widgets.editors.scan_metadata._metadata_widgets import StrMetadataField
|
|
||||||
|
|
||||||
from .client_mocks import mocked_client
|
from .client_mocks import mocked_client
|
||||||
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
# pylint: disable=missing-function-docstring
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
available_scans_message = AvailableResourceMessage(
|
available_scans_message = AvailableResourceMessage(
|
||||||
resource={
|
resource={
|
||||||
"line_scan": {
|
"line_scan": {
|
||||||
@ -539,9 +544,10 @@ def test_scan_metadata_is_connected(scan_control):
|
|||||||
assert scan_control._metadata_form._scan_name == "line_scan"
|
assert scan_control._metadata_form._scan_name == "line_scan"
|
||||||
scan_control.comboBox_scan_selection.setCurrentText("grid_scan")
|
scan_control.comboBox_scan_selection.setCurrentText("grid_scan")
|
||||||
assert scan_control._metadata_form._scan_name == "grid_scan"
|
assert scan_control._metadata_form._scan_name == "grid_scan"
|
||||||
sample_name = scan_control._metadata_form._md_grid_layout.itemAtPosition(0, 1).widget()
|
sample_name = scan_control._metadata_form._form_grid.layout().itemAtPosition(0, 1).widget()
|
||||||
assert isinstance(sample_name, StrMetadataField)
|
assert isinstance(sample_name, StrMetadataField)
|
||||||
sample_name._main_widget.setText("Test Sample")
|
sample_name._main_widget.setText("Test Sample")
|
||||||
|
|
||||||
scan_control._metadata_form._additional_metadata._table_model._data = [
|
scan_control._metadata_form._additional_metadata._table_model._data = [
|
||||||
["test key 1", "test value 1"],
|
["test key 1", "test value 1"],
|
||||||
["test key 2", "test value 2"],
|
["test key 2", "test value 2"],
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from bec_lib.metadata_schema import BasicScanMetadata
|
from bec_lib.metadata_schema import BasicScanMetadata
|
||||||
@ -7,17 +6,20 @@ from pydantic import Field
|
|||||||
from pydantic.types import Json
|
from pydantic.types import Json
|
||||||
from qtpy.QtCore import QItemSelectionModel, QPoint, Qt
|
from qtpy.QtCore import QItemSelectionModel, QPoint, Qt
|
||||||
|
|
||||||
from bec_widgets.widgets.editors.scan_metadata import ScanMetadata
|
from bec_widgets.utils.forms_from_types.items import (
|
||||||
from bec_widgets.widgets.editors.scan_metadata._metadata_widgets import (
|
|
||||||
BoolMetadataField,
|
BoolMetadataField,
|
||||||
|
DynamicFormItem,
|
||||||
FloatDecimalMetadataField,
|
FloatDecimalMetadataField,
|
||||||
IntMetadataField,
|
IntMetadataField,
|
||||||
MetadataWidget,
|
|
||||||
StrMetadataField,
|
StrMetadataField,
|
||||||
)
|
)
|
||||||
from bec_widgets.widgets.editors.scan_metadata.additional_metadata_table import (
|
from bec_widgets.widgets.editors.dict_backed_table import DictBackedTable
|
||||||
AdditionalMetadataTable,
|
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
|
||||||
)
|
|
||||||
|
# pylint: disable=no-member
|
||||||
|
# pylint: disable=missing-function-docstring
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
|
||||||
|
|
||||||
class ExampleSchema(BasicScanMetadata):
|
class ExampleSchema(BasicScanMetadata):
|
||||||
@ -35,8 +37,6 @@ class ExampleSchema(BasicScanMetadata):
|
|||||||
unsupported_class: Json = Field(default_factory=dict)
|
unsupported_class: Json = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
pytest.approx(0.1)
|
|
||||||
|
|
||||||
TEST_DICT = {
|
TEST_DICT = {
|
||||||
"sample_name": "test name",
|
"sample_name": "test name",
|
||||||
"str_optional": None,
|
"str_optional": None,
|
||||||
@ -72,17 +72,17 @@ def metadata_widget(empty_metadata_widget: ScanMetadata):
|
|||||||
widget._md_schema = ExampleSchema
|
widget._md_schema = ExampleSchema
|
||||||
widget.populate()
|
widget.populate()
|
||||||
|
|
||||||
sample_name = widget._md_grid_layout.itemAtPosition(0, 1).widget()
|
sample_name = widget._form_grid.layout().itemAtPosition(0, 1).widget()
|
||||||
str_optional = widget._md_grid_layout.itemAtPosition(1, 1).widget()
|
str_optional = widget._form_grid.layout().itemAtPosition(1, 1).widget()
|
||||||
str_required = widget._md_grid_layout.itemAtPosition(2, 1).widget()
|
str_required = widget._form_grid.layout().itemAtPosition(2, 1).widget()
|
||||||
bool_optional = widget._md_grid_layout.itemAtPosition(3, 1).widget()
|
bool_optional = widget._form_grid.layout().itemAtPosition(3, 1).widget()
|
||||||
bool_required_default = widget._md_grid_layout.itemAtPosition(4, 1).widget()
|
bool_required_default = widget._form_grid.layout().itemAtPosition(4, 1).widget()
|
||||||
bool_required_nodefault = widget._md_grid_layout.itemAtPosition(5, 1).widget()
|
bool_required_nodefault = widget._form_grid.layout().itemAtPosition(5, 1).widget()
|
||||||
int_default = widget._md_grid_layout.itemAtPosition(6, 1).widget()
|
int_default = widget._form_grid.layout().itemAtPosition(6, 1).widget()
|
||||||
int_nodefault_optional = widget._md_grid_layout.itemAtPosition(7, 1).widget()
|
int_nodefault_optional = widget._form_grid.layout().itemAtPosition(7, 1).widget()
|
||||||
float_nodefault = widget._md_grid_layout.itemAtPosition(8, 1).widget()
|
float_nodefault = widget._form_grid.layout().itemAtPosition(8, 1).widget()
|
||||||
decimal_dp_limits_nodefault = widget._md_grid_layout.itemAtPosition(9, 1).widget()
|
decimal_dp_limits_nodefault = widget._form_grid.layout().itemAtPosition(9, 1).widget()
|
||||||
unsupported_class = widget._md_grid_layout.itemAtPosition(10, 1).widget()
|
unsupported_class = widget._form_grid.layout().itemAtPosition(10, 1).widget()
|
||||||
|
|
||||||
yield (
|
yield (
|
||||||
widget,
|
widget,
|
||||||
@ -102,7 +102,7 @@ def metadata_widget(empty_metadata_widget: ScanMetadata):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def fill_commponents(components: dict[str, MetadataWidget]):
|
def fill_commponents(components: dict[str, DynamicFormItem]):
|
||||||
components["sample_name"].setValue("test name")
|
components["sample_name"].setValue("test name")
|
||||||
components["str_optional"].setValue(None)
|
components["str_optional"].setValue(None)
|
||||||
components["str_required"].setValue("something")
|
components["str_required"].setValue("something")
|
||||||
@ -116,7 +116,7 @@ def fill_commponents(components: dict[str, MetadataWidget]):
|
|||||||
|
|
||||||
|
|
||||||
def test_griditems_are_correct_class(
|
def test_griditems_are_correct_class(
|
||||||
metadata_widget: tuple[ScanMetadata, dict[str, MetadataWidget]]
|
metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]]
|
||||||
):
|
):
|
||||||
_, components = metadata_widget
|
_, components = metadata_widget
|
||||||
assert isinstance(components["sample_name"], StrMetadataField)
|
assert isinstance(components["sample_name"], StrMetadataField)
|
||||||
@ -132,15 +132,15 @@ def test_griditems_are_correct_class(
|
|||||||
assert isinstance(components["unsupported_class"], StrMetadataField)
|
assert isinstance(components["unsupported_class"], StrMetadataField)
|
||||||
|
|
||||||
|
|
||||||
def test_grid_to_dict(metadata_widget: tuple[ScanMetadata, dict[str, MetadataWidget]]):
|
def test_grid_to_dict(metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]]):
|
||||||
widget, components = metadata_widget = metadata_widget
|
widget, components = metadata_widget = metadata_widget
|
||||||
fill_commponents(components)
|
fill_commponents(components)
|
||||||
|
|
||||||
assert widget._dict_from_grid() == TEST_DICT
|
assert widget._dict_from_grid() == TEST_DICT
|
||||||
assert widget.get_full_model_dict() == TEST_DICT | {"extra_field": "extra_data"}
|
assert widget.get_form_data() == TEST_DICT | {"extra_field": "extra_data"}
|
||||||
|
|
||||||
|
|
||||||
def test_validation(metadata_widget: tuple[ScanMetadata, dict[str, MetadataWidget]]):
|
def test_validation(metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]]):
|
||||||
widget, components = metadata_widget = metadata_widget
|
widget, components = metadata_widget = metadata_widget
|
||||||
assert widget._validity.compact_status.styleSheet().startswith(
|
assert widget._validity.compact_status.styleSheet().startswith(
|
||||||
widget._validity.compact_status.default_led[:114]
|
widget._validity.compact_status.default_led[:114]
|
||||||
@ -161,7 +161,9 @@ def test_validation(metadata_widget: tuple[ScanMetadata, dict[str, MetadataWidge
|
|||||||
components["float_nodefault"].setValue(True)
|
components["float_nodefault"].setValue(True)
|
||||||
|
|
||||||
|
|
||||||
def test_numbers_clipped_to_limits(metadata_widget: tuple[ScanMetadata, dict[str, MetadataWidget]]):
|
def test_numbers_clipped_to_limits(
|
||||||
|
metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]]
|
||||||
|
):
|
||||||
widget, components = metadata_widget = metadata_widget
|
widget, components = metadata_widget = metadata_widget
|
||||||
fill_commponents(components)
|
fill_commponents(components)
|
||||||
|
|
||||||
@ -173,20 +175,20 @@ def test_numbers_clipped_to_limits(metadata_widget: tuple[ScanMetadata, dict[str
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def table():
|
def table():
|
||||||
table = AdditionalMetadataTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
table = DictBackedTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]])
|
||||||
yield table
|
yield table
|
||||||
table._table_model.deleteLater()
|
table._table_model.deleteLater()
|
||||||
table._table_view.deleteLater()
|
table._table_view.deleteLater()
|
||||||
table.deleteLater()
|
table.deleteLater()
|
||||||
|
|
||||||
|
|
||||||
def test_additional_metadata_table_add_row(table: AdditionalMetadataTable):
|
def test_additional_metadata_table_add_row(table: DictBackedTable):
|
||||||
assert table._table_model.rowCount() == 3
|
assert table._table_model.rowCount() == 3
|
||||||
table._add_button.click()
|
table._add_button.click()
|
||||||
assert table._table_model.rowCount() == 4
|
assert table._table_model.rowCount() == 4
|
||||||
|
|
||||||
|
|
||||||
def test_additional_metadata_table_delete_row(table: AdditionalMetadataTable):
|
def test_additional_metadata_table_delete_row(table: DictBackedTable):
|
||||||
assert table._table_model.rowCount() == 3
|
assert table._table_model.rowCount() == 3
|
||||||
m = table._table_view.selectionModel()
|
m = table._table_view.selectionModel()
|
||||||
item = table._table_view.indexAt(QPoint(0, 0)).siblingAtRow(1)
|
item = table._table_view.indexAt(QPoint(0, 0)).siblingAtRow(1)
|
||||||
@ -196,14 +198,14 @@ def test_additional_metadata_table_delete_row(table: AdditionalMetadataTable):
|
|||||||
assert list(table.dump_dict().keys()) == ["key1", "key3"]
|
assert list(table.dump_dict().keys()) == ["key1", "key3"]
|
||||||
|
|
||||||
|
|
||||||
def test_additional_metadata_allows_changes(table: AdditionalMetadataTable):
|
def test_additional_metadata_allows_changes(table: DictBackedTable):
|
||||||
assert table._table_model.rowCount() == 3
|
assert table._table_model.rowCount() == 3
|
||||||
assert list(table.dump_dict().keys()) == ["key1", "key2", "key3"]
|
assert list(table.dump_dict().keys()) == ["key1", "key2", "key3"]
|
||||||
table._table_model.setData(table._table_model.index(1, 0), "key4", Qt.ItemDataRole.EditRole)
|
table._table_model.setData(table._table_model.index(1, 0), "key4", Qt.ItemDataRole.EditRole)
|
||||||
assert list(table.dump_dict().keys()) == ["key1", "key4", "key3"]
|
assert list(table.dump_dict().keys()) == ["key1", "key4", "key3"]
|
||||||
|
|
||||||
|
|
||||||
def test_additional_metadata_doesnt_allow_dupes(table: AdditionalMetadataTable):
|
def test_additional_metadata_doesnt_allow_dupes(table: DictBackedTable):
|
||||||
assert table._table_model.rowCount() == 3
|
assert table._table_model.rowCount() == 3
|
||||||
assert list(table.dump_dict().keys()) == ["key1", "key2", "key3"]
|
assert list(table.dump_dict().keys()) == ["key1", "key2", "key3"]
|
||||||
table._table_model.setData(table._table_model.index(1, 0), "key1", Qt.ItemDataRole.EditRole)
|
table._table_model.setData(table._table_model.index(1, 0), "key1", Qt.ItemDataRole.EditRole)
|
||||||
|
Reference in New Issue
Block a user