from decimal import Decimal import pytest from bec_lib.metadata_schema import BasicScanMetadata from pydantic import Field from pydantic.types import Json from qtpy.QtCore import QItemSelectionModel, QPoint, Qt from bec_widgets.utils.forms_from_types.items import ( BoolMetadataField, DynamicFormItem, FloatDecimalMetadataField, IntMetadataField, StrMetadataField, ) from bec_widgets.widgets.editors.dict_backed_table import DictBackedTable 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): str_optional: str | None = Field( None, title="Optional string", description="an optional string", max_length=23 ) str_required: str bool_optional: bool | None = Field(None) bool_required_default: bool = Field(True) bool_required_nodefault: bool = Field() int_default: int = Field(123) int_nodefault_optional: int | None = Field(lt=-1, ge=-44) float_nodefault: float decimal_dp_limits_nodefault: Decimal = Field(Decimal(1.23), decimal_places=2, gt=1, le=34.5) unsupported_class: Json = Field(default_factory=dict) TEST_DICT = { "sample_name": "test name", "str_optional": None, "str_required": "something", "bool_optional": None, "bool_required_default": True, "bool_required_nodefault": False, "int_default": 21, "int_nodefault_optional": -10, "float_nodefault": pytest.approx(0.1), "decimal_dp_limits_nodefault": pytest.approx(34), "unsupported_class": '{"key": "value"}', } @pytest.fixture def example_md(): return ExampleSchema.model_validate(TEST_DICT) @pytest.fixture def empty_metadata_widget(): widget = ScanMetadata() widget._additional_metadata._table_model._data = [["extra_field", "extra_data"]] yield widget widget._clear_grid() widget.deleteLater() @pytest.fixture def metadata_widget(empty_metadata_widget: ScanMetadata): widget = empty_metadata_widget widget._md_schema = ExampleSchema widget.populate() sample_name = widget._form_grid.layout().itemAtPosition(0, 1).widget() str_optional = widget._form_grid.layout().itemAtPosition(1, 1).widget() str_required = widget._form_grid.layout().itemAtPosition(2, 1).widget() bool_optional = widget._form_grid.layout().itemAtPosition(3, 1).widget() bool_required_default = widget._form_grid.layout().itemAtPosition(4, 1).widget() bool_required_nodefault = widget._form_grid.layout().itemAtPosition(5, 1).widget() int_default = widget._form_grid.layout().itemAtPosition(6, 1).widget() int_nodefault_optional = widget._form_grid.layout().itemAtPosition(7, 1).widget() float_nodefault = widget._form_grid.layout().itemAtPosition(8, 1).widget() decimal_dp_limits_nodefault = widget._form_grid.layout().itemAtPosition(9, 1).widget() unsupported_class = widget._form_grid.layout().itemAtPosition(10, 1).widget() yield ( widget, { "sample_name": sample_name, "str_optional": str_optional, "str_required": str_required, "bool_optional": bool_optional, "bool_required_default": bool_required_default, "bool_required_nodefault": bool_required_nodefault, "int_default": int_default, "int_nodefault_optional": int_nodefault_optional, "float_nodefault": float_nodefault, "decimal_dp_limits_nodefault": decimal_dp_limits_nodefault, "unsupported_class": unsupported_class, }, ) def fill_commponents(components: dict[str, DynamicFormItem]): components["sample_name"].setValue("test name") components["str_optional"].setValue(None) components["str_required"].setValue("something") components["bool_optional"].setValue(None) components["bool_required_nodefault"].setValue(False) components["int_default"].setValue(21) components["int_nodefault_optional"].setValue(-10) components["float_nodefault"].setValue(0.1) components["decimal_dp_limits_nodefault"].setValue(456.789) components["unsupported_class"].setValue(r'{"key": "value"}') def test_griditems_are_correct_class( metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]], ): _, components = metadata_widget assert isinstance(components["sample_name"], StrMetadataField) assert isinstance(components["str_optional"], StrMetadataField) assert isinstance(components["str_required"], StrMetadataField) assert isinstance(components["bool_optional"], BoolMetadataField) assert isinstance(components["bool_required_default"], BoolMetadataField) assert isinstance(components["bool_required_nodefault"], BoolMetadataField) assert isinstance(components["int_default"], IntMetadataField) assert isinstance(components["int_nodefault_optional"], IntMetadataField) assert isinstance(components["float_nodefault"], FloatDecimalMetadataField) assert isinstance(components["decimal_dp_limits_nodefault"], FloatDecimalMetadataField) assert isinstance(components["unsupported_class"], StrMetadataField) def test_grid_to_dict(metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]]): widget, components = metadata_widget = metadata_widget fill_commponents(components) assert widget._dict_from_grid() == TEST_DICT assert widget.get_form_data() == TEST_DICT | {"extra_field": "extra_data"} def test_validation(metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]]): widget, components = metadata_widget = metadata_widget assert widget._validity.compact_status.styleSheet().startswith( widget._validity.compact_status.default_led[:114] ) fill_commponents(components) widget.validate_form() assert widget._validity_message.text() == "No errors!" components["bool_required_nodefault"]._main_widget.clear() widget.validate_form() assert "Input should be a valid boolean" in widget._validity_message.text() components["bool_required_nodefault"].setValue(True) components["float_nodefault"]._main_widget.clear() widget.validate_form() assert "Input should be a valid number" in widget._validity_message.text() components["float_nodefault"].setValue(True) def test_numbers_clipped_to_limits( metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]], ): widget, components = metadata_widget = metadata_widget fill_commponents(components) components["decimal_dp_limits_nodefault"].setValue(-56) widget.validate_form() assert components["decimal_dp_limits_nodefault"].getValue() == pytest.approx(2) assert widget._validity_message.text() == "No errors!" @pytest.fixture def table(): table = DictBackedTable([["key1", "value1"], ["key2", "value2"], ["key3", "value3"]]) yield table table._table_model.deleteLater() table._table_view.deleteLater() table.deleteLater() def test_additional_metadata_table_add_row(table: DictBackedTable): assert table._table_model.rowCount() == 3 table._add_button.click() assert table._table_model.rowCount() == 4 def test_additional_metadata_table_delete_row(table: DictBackedTable): assert table._table_model.rowCount() == 3 m = table._table_view.selectionModel() item = table._table_view.indexAt(QPoint(0, 0)).siblingAtRow(1) m.select(item, QItemSelectionModel.SelectionFlag.Select) table.delete_selected_rows() assert table._table_model.rowCount() == 2 assert list(table.dump_dict().keys()) == ["key1", "key3"] def test_additional_metadata_allows_changes(table: DictBackedTable): assert table._table_model.rowCount() == 3 assert list(table.dump_dict().keys()) == ["key1", "key2", "key3"] table._table_model.setData(table._table_model.index(1, 0), "key4", Qt.ItemDataRole.EditRole) assert list(table.dump_dict().keys()) == ["key1", "key4", "key3"] def test_additional_metadata_doesnt_allow_dupes(table: DictBackedTable): assert table._table_model.rowCount() == 3 assert list(table.dump_dict().keys()) == ["key1", "key2", "key3"] table._table_model.setData(table._table_model.index(1, 0), "key1", Qt.ItemDataRole.EditRole) assert list(table.dump_dict().keys()) == ["key1", "key2", "key3"]