diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index fdfbdac..bf71431 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -266,6 +266,51 @@ def _generate_json_map( ) -> JsonMap: """Recursively generate JSON map from .bob file tree""" + # ------------ USEFUL FUNCTIONS ------------ + + def _get_display_name( + name_element: str | None, component_name: str | None, file_path: Path + ): + # Validated screen names don't get renegerated + name = name_element + display_name = self._get_component_label( + name_element, + component_name, + name, + ) + # Create valid displayName + display_name = self._parse_display_name(display_name, file_path) + return display_name + + def _next_file_crawl( + file_path: Path, + destination_path: Path, + name_element: str | None, + component_name: str | None, + display_name: str | None, + macro_dictionary: dict[str, Any], + ): + # TODO: misleading var name? + next_file_path = destination_path.joinpath(file_path) + + # Crawl the next file + if next_file_path.is_file(): + # TODO: investigate non-recursive approaches? + child_node = self._generate_json_map( + next_file_path, + destination_path, + current_component_name=component_name, + name_elem=name_element, + ) + else: + child_node = JsonMap( + str(file_path), display_name, exists=("IOC" in macro_dictionary) + ) + + return child_node + + # ------------------------------------------ + # Create initial node at top of .bob file current_node = JsonMap( str(screen_path.relative_to(self._write_directory)), @@ -298,7 +343,7 @@ def _generate_json_map( for w in root.findall(".//widget") if w.get("type", default=None) # in ["symbol", "embedded", "action_button"] - in ["symbol", "action_button", "embedded"] + in ["symbol", "action_button", "embedded", "navtabs"] ] for widget_elem in widgets: @@ -324,16 +369,49 @@ def _generate_json_map( name_elem = widget_elem.name.text macro_dict = self._get_macros(widget_elem) - case _: + case "navtabs": + tabs = _get_nav_tabs(widget_elem) + if tabs is None: + continue + + for tab in tabs: + name_elem = tab.name.text + file_elem = tab.file + macro_dict = self._get_macros(tab) + + # Extract file path from file_elem + file_path = Path( + file_elem.text.strip() if file_elem.text else "" + ) + + # If file is already a .bob file, skip it + if not file_path.suffix == ".bob": + continue + + display_name = _get_display_name( + name_elem, current_component_name, file_path + ) + + child_node = _next_file_crawl( + file_path, + dest_path, + name_elem, + current_component_name, + display_name, + macro_dict, + ) + + child_node.macros = macro_dict + # TODO: make this work for only list[JsonMap] + assert isinstance(current_node.children, list) + # TODO: fix typing + current_node.children.append(child_node) + + # We have already done the logic, so skip to the next widget continue - # Validated screen names don't get renegerated - display_name = name_elem - display_name = self._get_component_label( - name_elem, - current_component_name, - display_name, - ) + case _: + continue # Extract file path from file_elem file_path = Path(file_elem.text.strip() if file_elem.text else "") @@ -342,25 +420,18 @@ def _generate_json_map( if not file_path.suffix == ".bob": continue - # Create valid displayName - display_name = self._parse_display_name(display_name, file_path) - - # TODO: misleading var name? - next_file_path = dest_path.joinpath(file_path) + display_name = _get_display_name( + name_elem, current_component_name, file_path + ) - # Crawl the next file - if next_file_path.is_file(): - # TODO: investigate non-recursive approaches? - child_node = self._generate_json_map( - next_file_path, - dest_path, - current_component_name=current_component_name, - name_elem=name_elem, - ) - else: - child_node = JsonMap( - str(file_path), display_name, exists=("IOC" in macro_dict) - ) + child_node = _next_file_crawl( + file_path, + dest_path, + name_elem, + current_component_name, + display_name, + macro_dict, + ) child_node.macros = macro_dict # TODO: make this work for only list[JsonMap] @@ -515,6 +586,11 @@ def write_json_map( json.dumps(map, indent=4, default=lambda o: _serialise_json_map(o)) + "\n" ) + # f.writelines( + # self.conf.model_dump_json( + # indent=4, exclude_defaults=True, exclude_none=True + # ) + # ) # Function to convert the JsonMap objects into dictionaries, @@ -574,3 +650,25 @@ def _get_action_group(element: ObjectifiedElement) -> ObjectifiedElement | None: f"Actions group not found in component [bold]{name}[/bold] on " f"[bold]{parent_name}[/bold]" ) + + +def _get_nav_tabs(element: ObjectifiedElement) -> list[ObjectifiedElement] | None: + try: + element_tabs = element.tabs + assert element_tabs is not None + + tabs = list(element_tabs.iterchildren("tab")) + + return tabs + + except AttributeError: + # TODO: Find better way of handling there being no "tabs" group + # TODO: Do widgets always have a name attr, or _can_ it be empty?? + name = element.name + + parent_name = p.name if (p := element.getparent()) is not None else None + + logger_.error( + f"Tabs group not found in component [bold]{name}[/bold] on " + f"[bold]{parent_name}[/bold]" + ) diff --git a/tests/conftest.py b/tests/conftest.py index 60934c7..b0125ef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,13 +53,20 @@ def test_files(): @pytest.fixture -def example_json_map(): +def example_json_map_root(): + test_map_base = JsonMap("test_bob.bob", "Display") + + return test_map_base + + +@pytest.fixture +def example_json_map(example_json_map_root): # Create test json map with child json map test_map_child = JsonMap("test_child_bob.bob", "Detector", exists=False) - test_map = JsonMap("test_bob.bob", "Display") - test_map.children.append(test_map_child) - return test_map + example_json_map_root.children.append(test_map_child) + + return example_json_map_root @pytest.fixture @@ -217,3 +224,43 @@ def example_symbol_widget(): widget_element = fromstring(tostring(widget_element)) return widget_element + + +@pytest.fixture +def example_navtabs_widget(): + # You cannot set a text tag of an ObjectifiedElement, + # so we need to make an etree.Element and convert it ... + + widget_element = Element("widget") + widget_element.set("type", "navtabs") + widget_element.set("version", "2.0.0") + name_element = SubElement(widget_element, "name") + name_element.text = "navtab" + width_element = SubElement(widget_element, "width") + width_element.text = "205" + height_element = SubElement(widget_element, "height") + height_element.text = "120" + tabs_element = SubElement(widget_element, "tabs") + tab_element_1 = SubElement(tabs_element, "tab") + tab_element_2 = SubElement(tabs_element, "tab") + + name_element_1 = SubElement(tab_element_1, "name") + name_element_1.text = "tab1" + file_element_1 = SubElement(tab_element_1, "file") + file_element_1.text = "tests/test-files/motor_embed.bob" + macros_element_1 = SubElement(tab_element_1, "macros") + macro_element_1 = SubElement(macros_element_1, "macro1") + macro_element_1.text = "test_macro_1" + + name_element_2 = SubElement(tab_element_2, "name") + name_element_2.text = "tab2" + file_element_2 = SubElement(tab_element_2, "file") + file_element_2.text = "tests/test-files/motor_embed.bob" + macros_element_2 = SubElement(tab_element_2, "macros") + macro_element_2 = SubElement(macros_element_2, "macro2") + macro_element_2.text = "test_macro_2" + + # ... which requires this horror + widget_element = fromstring(tostring(widget_element)) + + return widget_element diff --git a/tests/test_builder.py b/tests/test_builder.py index 24375b0..39cb12e 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -2,7 +2,7 @@ import os from io import StringIO from pathlib import Path -from unittest.mock import Mock, mock_open, patch +from unittest.mock import MagicMock, Mock, mock_open, patch import pytest from lxml import objectify @@ -12,6 +12,7 @@ from techui_builder.builder import ( JsonMap, _get_action_group, # type: ignore + _get_nav_tabs, # type: ignore _serialise_json_map, # type: ignore ) @@ -317,9 +318,7 @@ def test_generate_json_map( # TODO: write this test -def test_generate_json_map_embedded_screen( - builder_with_test_files, example_json_map, components -): +def test_generate_json_map_embedded_screen(builder_with_test_files, example_json_map): builder_with_test_files._get_component_label = Mock( side_effect=["Display", "Detector", "Embedded Display"] ) @@ -334,13 +333,32 @@ def test_generate_json_map_embedded_screen( ) ) - test_json_map = builder_with_test_files._generate_json_map( - screen_path, dest_path, components - ) + test_json_map = builder_with_test_files._generate_json_map(screen_path, dest_path) assert test_json_map == example_json_map +def test_generate_json_map_nav_tabs(builder_with_test_files, example_json_map_root): + builder_with_test_files._get_component_label = Mock( + side_effect=["Display", "Tab1", "Tab2"] + ) + + screen_path = Path("tests/test_files/test_bob_navtabs.bob").absolute() + dest_path = Path("tests/test_files/") + + example_json_map_root.file = "test_bob_navtabs.bob" + example_json_map_root.children.extend( + [ + JsonMap(display_name="Tab1", file="tab1.bob", exists=False), + JsonMap(display_name="Tab2", file="tab2.bob", exists=False), + ] + ) + + test_json_map = builder_with_test_files._generate_json_map(screen_path, dest_path) + + assert test_json_map == example_json_map_root + + def test_parse_display_name_with_name(builder): """Test parse display name when tag is present""" display_name = builder._parse_display_name( @@ -552,3 +570,20 @@ def test_get_component_label_with_current_component_name_invalid( display_name="new_name", ) assert display_name == "new_name" + + +def test_get_nav_tabs(example_navtabs_widget): + tabs_widget = _get_nav_tabs(example_navtabs_widget) + + assert isinstance(tabs_widget, list) + + +def test_get_nav_tabs_no_tabs_group(caplog): + mock_navtabs = MagicMock(spec=objectify.ObjectifiedElement) + mock_navtabs.name = "no_tabs" + + with caplog.at_level(logging.ERROR): + _get_nav_tabs(mock_navtabs) + + for log_output in caplog.records: + assert "Tabs group not found" in log_output.message diff --git a/tests/test_files/test_bob_navtabs.bob b/tests/test_files/test_bob_navtabs.bob new file mode 100644 index 0000000..ddb9975 --- /dev/null +++ b/tests/test_files/test_bob_navtabs.bob @@ -0,0 +1,46 @@ + + + + Display + + Navigation Tabs + + + Tab1 + tab1.bob + + + + + + + + + + + + + + Tab2 + tab2.bob + + + + + + + + + + + + + + 150 + 120 + 820 + 530 + 0 + 20 + +