From 78a2e8a5ce9c109746d3a3d5a8ca710cacdab256 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Thu, 6 Nov 2025 14:37:07 +0000 Subject: [PATCH 1/8] extended ability for generate to handle dict_of_dicts in the ibek_mapping.yaml Merge conflict resolution --- src/techui_builder/generate.py | 60 +++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index ec030766..5e40f37f 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -1,6 +1,7 @@ import logging import os from collections import defaultdict +from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import Path @@ -164,13 +165,7 @@ def _get_group_dimensions(self, widget_list: list[EmbeddedDisplay | ActionButton max(x_list) + max(width_list) + self.group_padding, ) - def _create_widget( - self, component: Entity - ) -> EmbeddedDisplay | ActionButton | None: - # if statement below is check if the suffix is - # missing from the component description. If - # not missing, use as name of widget, if missing, - # use type as name. + def _initialise_name_suffix(self, component: Entity) -> tuple[str, str, str | None]: if component.M is not None: name: str = component.M suffix: str = component.M @@ -184,15 +179,17 @@ def _create_widget( suffix = "" suffix_label = None - try: - scrn_mapping = self.techui_support[component.type] - except KeyError: - LOGGER.warning( - f"No available widget for {component.type} in screen \ -{self.screen_name}. Skipping..." - ) - return None + return (name, suffix, suffix_label) + def _is_dict_of_dicts(self, scrn_mapping: Mapping) -> bool: + return isinstance(scrn_mapping, Mapping) and all( + isinstance(scrn, Mapping) for scrn in scrn_mapping.values() + ) + + def _allocate_widget( + self, support_path: Path, scrn_mapping: Mapping, component: Entity + ) -> EmbeddedDisplay | ActionButton | None | list[EmbeddedDisplay | ActionButton]: + name, suffix, suffix_label = self._initialise_name_suffix(component) # Get relative path to screen scrn_path = self.support_path.joinpath(f"bob/{scrn_mapping['file']}") LOGGER.debug(f"Screen path: {scrn_path}") @@ -200,7 +197,6 @@ def _create_widget( # Path of screen relative to data/ so it knows where to open the file from data_scrn_path = scrn_path.relative_to(self.synoptic_dir, walk_up=True) - # Get dimensions of screen from TechUI repository if scrn_mapping["type"] == "embedded": height, width = self._get_screen_dimensions(str(scrn_path)) new_widget = Widget.EmbeddedDisplay( @@ -253,6 +249,35 @@ def _create_widget( new_widget.version("2.0.0") return new_widget + def _create_widget( + self, component: Entity + ) -> EmbeddedDisplay | ActionButton | None | list[EmbeddedDisplay | ActionButton]: + # if statement below is check if the suffix is + # missing from the component description. If + # not missing, use as name of widget, if missing, + # use type as name. + new_widget = [] + base_dir = self.services_dir.parent.parent.parent + + # Get the relative path to techui-support + support_path = base_dir.joinpath("src/techui_support") + try: + scrn_mapping = self.techui_support[component.type] + except KeyError: + LOGGER.warning( + f"No available widget for {component.type} in screen \ +{self.screen_name}. Skipping..." + ) + return None + + if self._is_dict_of_dicts(scrn_mapping): + for _, value in scrn_mapping.items(): + new_widget.append(self._allocate_widget(support_path, value, component)) + else: + new_widget = self._allocate_widget(support_path, scrn_mapping, component) + + return new_widget + def layout_widgets(self, widgets: list[EmbeddedDisplay | ActionButton]): group_spacing: int = 30 max_group_height: int = 800 @@ -334,6 +359,9 @@ def build_groups(self): new_widget = self._create_widget(component=component) if new_widget is None: continue + if isinstance(new_widget, list): + self.widgets.extend(new_widget) + continue self.widgets.append(new_widget) if self.widgets == []: From 7fa1c7a161f43f3415709762b2ecb2789c3646b0 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Fri, 14 Nov 2025 14:37:22 +0000 Subject: [PATCH 2/8] Changed RootModel for GuiComponents --- src/techui_builder/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index 2a353192..87cb2393 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -130,7 +130,7 @@ class TechUi(BaseModel): """ -Ibek mapping models +techui_support mapping models """ BobPath = Annotated[ @@ -151,7 +151,10 @@ class GuiComponentEntry(BaseModel): model_config = ConfigDict(extra="forbid") -class GuiComponents(RootModel[dict[str, GuiComponentEntry]]): +GuiComponentUnion = list[GuiComponentEntry] | GuiComponentEntry + + +class GuiComponents(RootModel[dict[str, GuiComponentUnion]]): pass From 2e6e68b971c06fc26b8e4f1cd813e9a4b532f605 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Fri, 14 Nov 2025 14:38:41 +0000 Subject: [PATCH 3/8] Change from _dicts_of_dicts to list_of_dicts --- src/techui_builder/generate.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index 5e40f37f..732beeb8 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -1,7 +1,7 @@ import logging import os from collections import defaultdict -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from dataclasses import dataclass, field from pathlib import Path @@ -181,9 +181,9 @@ def _initialise_name_suffix(self, component: Entity) -> tuple[str, str, str | No return (name, suffix, suffix_label) - def _is_dict_of_dicts(self, scrn_mapping: Mapping) -> bool: - return isinstance(scrn_mapping, Mapping) and all( - isinstance(scrn, Mapping) for scrn in scrn_mapping.values() + def _is_list_of_dicts(self, scrn_mapping: Mapping) -> bool: + return isinstance(scrn_mapping, Sequence) and all( + isinstance(scrn, Mapping) for scrn in scrn_mapping ) def _allocate_widget( @@ -270,8 +270,8 @@ def _create_widget( ) return None - if self._is_dict_of_dicts(scrn_mapping): - for _, value in scrn_mapping.items(): + if self._is_list_of_dicts(scrn_mapping): + for value in scrn_mapping: new_widget.append(self._allocate_widget(support_path, value, component)) else: new_widget = self._allocate_widget(support_path, scrn_mapping, component) From 81c3f6d5a277549e7e9bf3525ecc955e2b75dbda Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Mon, 10 Nov 2025 15:43:06 +0000 Subject: [PATCH 4/8] Merge conflicts --- src/techui_builder/generate.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index 732beeb8..f3b35fd7 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -187,7 +187,7 @@ def _is_list_of_dicts(self, scrn_mapping: Mapping) -> bool: ) def _allocate_widget( - self, support_path: Path, scrn_mapping: Mapping, component: Entity + self, scrn_mapping: Mapping, component: Entity ) -> EmbeddedDisplay | ActionButton | None | list[EmbeddedDisplay | ActionButton]: name, suffix, suffix_label = self._initialise_name_suffix(component) # Get relative path to screen @@ -257,10 +257,7 @@ def _create_widget( # not missing, use as name of widget, if missing, # use type as name. new_widget = [] - base_dir = self.services_dir.parent.parent.parent - # Get the relative path to techui-support - support_path = base_dir.joinpath("src/techui_support") try: scrn_mapping = self.techui_support[component.type] except KeyError: @@ -272,9 +269,9 @@ def _create_widget( if self._is_list_of_dicts(scrn_mapping): for value in scrn_mapping: - new_widget.append(self._allocate_widget(support_path, value, component)) + new_widget.append(self._allocate_widget(value, component)) else: - new_widget = self._allocate_widget(support_path, scrn_mapping, component) + new_widget = self._allocate_widget(scrn_mapping, component) return new_widget From 72f896b9aedf3e7f26b6731c31b0e853c6fb36c1 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Tue, 11 Nov 2025 11:14:48 +0000 Subject: [PATCH 5/8] Added tests --- tests/test_generate.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_generate.py b/tests/test_generate.py index 79d5d7db..c1646506 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -140,6 +140,59 @@ def test_generator_create_widget_embedded(generator): component=component, ) + +def test_generator_initialise_name_suffix_m(generator): + component = Entity(type="test", P="TEST", desc=None, M="T1", R=None) + + name, suffix, suffix_label = generator._initialise_name_suffix(component) + + assert name == "T1" + assert suffix == "T1" + assert suffix_label == "M" + + +def test_generator_initialise_name_suffix_r(generator): + component = Entity(type="test", P="TEST", desc=None, M=None, R="T1") + + name, suffix, suffix_label = generator._initialise_name_suffix(component) + + assert name == "T1" + assert suffix == "T1" + assert suffix_label == "R" + + +def test_generator_initialise_name_suffix_none(generator): + component = Entity(type="test", P="TEST", desc=None, M=None, R=None) + + name, suffix, suffix_label = generator._initialise_name_suffix(component) + + assert name == "test" + assert suffix == "" + assert suffix_label is None + + +def test_generator_is_list_of_dicts(generator): + list_of_dicts = [{"a": 1}, {"b": 2}] + assert generator._is_list_of_dicts(list_of_dicts) is True + + +def test_generator_is_list_of_dicts_not(generator): + not_list_of_dicts = {"a": 1} + assert generator._is_list_of_dicts(not_list_of_dicts) is False + + +def test_generator_allocate_widget(generator): + generator._initilise_name_suffix = Mock(return_value=("CAM:", "CAM:", "R")) + + scrn_mapping = { + "file": "ADAravis/ADAravis_summary.bob", + "prefix": "$(P)$(R)", + "type": "embedded", + } + component = Entity( + type="ADAravis.aravisCamera", P="BL23B-DI-MOD-02", desc=None, M=None, R="CAM:" + ) + widget = generator._allocate_widget(scrn_mapping, component) control_widget = Path("tests/test_files/widget.xml") with open(control_widget) as f: From aefe2481195cf81f255726c627bebd41aa0d0623 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Fri, 14 Nov 2025 08:55:09 +0000 Subject: [PATCH 6/8] Added tests for changes in models --- tests/test_models.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/test_models.py b/tests/test_models.py index 6c60d7fb..40144764 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,6 +1,11 @@ import pytest -from techui_builder.models import Beamline, Component +from techui_builder.models import ( + Beamline, + Component, + GuiComponentEntry, + GuiComponents, +) @pytest.fixture @@ -13,6 +18,13 @@ def component() -> Component: return Component(prefix="BL01T-EA-TEST-02", desc="Test Device") +@pytest.fixture +def gui_components() -> GuiComponentEntry: + return GuiComponentEntry( + file="digitelMpc/digitelMpcIonp.bob", prefix="$(P)", type="embedded" + ) + + # @pytest.mark.parametrize("beamline,expected",[]) def test_beamline_object(beamline: Beamline): assert beamline.short_dom == "t01" @@ -39,3 +51,18 @@ def test_component_repr(component: Component): def test_component_bad_prefix(): with pytest.raises(ValueError): Component(prefix="Test 2", desc="BAD_PREFIX") + + +def test_gui_component_entry(gui_components: GuiComponentEntry): + assert gui_components.file == "digitelMpc/digitelMpcIonp.bob" + assert gui_components.prefix == "$(P)" + assert gui_components.type == "embedded" + + +def test_gui_components_object(gui_components: GuiComponentEntry): + gc = GuiComponents({"digitelMpc.digitelMpcIonp": [gui_components]}) + entry = gc.root["digitelMpc.digitelMpcIonp"][0] + assert entry.file == "digitelMpc/digitelMpcIonp.bob" + + assert entry.prefix == "$(P)" + assert entry.type == "embedded" From dc9c41cba1c538385643097fe0b8144eca9fbf8f Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Fri, 14 Nov 2025 14:47:46 +0000 Subject: [PATCH 7/8] ignored warnings --- tests/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_models.py b/tests/test_models.py index 40144764..67e4652e 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -61,7 +61,7 @@ def test_gui_component_entry(gui_components: GuiComponentEntry): def test_gui_components_object(gui_components: GuiComponentEntry): gc = GuiComponents({"digitelMpc.digitelMpcIonp": [gui_components]}) - entry = gc.root["digitelMpc.digitelMpcIonp"][0] + entry = gc.root["digitelMpc.digitelMpcIonp"][0] # type: ignore assert entry.file == "digitelMpc/digitelMpcIonp.bob" assert entry.prefix == "$(P)" From 10439bad8ef1fb35ed4b23188af05dd2b296dfbe Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Mon, 17 Nov 2025 14:51:37 +0000 Subject: [PATCH 8/8] Increased coverage by adding test for generate.create_widget rearranged variables --- tests/test_generate.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_generate.py b/tests/test_generate.py index c1646506..99bf1f00 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -130,6 +130,25 @@ def test_generator_create_widget_keyerror(generator, caplog): ) +def test_generator_create_widget_is_list_of_dicts(generator): + generator._get_screen_dimensions = Mock(return_value=(800, 1280)) + generator._is_list_of_dicts = Mock(return_value=True) + generator._allocate_widget = Mock( + return_value=Widget.EmbeddedDisplay( + name="X", file="", x=0, y=0, width=205, height=120 + ) + ) + generator.screen_name = "test" + component = Entity( + type="ADAravis.aravisCamera", P="BL23B-DI-MOD-02", desc=None, M=None, R="CAM:" + ) + widget = generator._create_widget(component=component) + for value in widget: + assert str(value) == str( + Widget.EmbeddedDisplay(name="X", file="", x=0, y=0, width=205, height=120) + ) + + def test_generator_create_widget_embedded(generator): generator._get_screen_dimensions = Mock(return_value=(800, 1280)) component = Entity( @@ -139,6 +158,11 @@ def test_generator_create_widget_embedded(generator): widget = generator._create_widget( component=component, ) + control_widget = Path("tests/test_files/widget.xml") + with open(control_widget) as f: + xml_content = f.read() + + assert str(widget) == xml_content def test_generator_initialise_name_suffix_m(generator):