Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 125 additions & 27 deletions src/techui_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand Down Expand Up @@ -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:
Expand All @@ -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 "")
Expand All @@ -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]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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]"
)
55 changes: 51 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
49 changes: 42 additions & 7 deletions tests/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)

Expand Down Expand Up @@ -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"]
)
Expand All @@ -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 <name> tag is present"""
display_name = builder._parse_display_name(
Expand Down Expand Up @@ -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
46 changes: 46 additions & 0 deletions tests/test_files/test_bob_navtabs.bob
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Saved on 2026-05-20 13:34:00 by ryi58813-->
<display version="2.0.0">
<name>Display</name>
<widget type="navtabs" version="2.0.0">
<name>Navigation Tabs</name>
<tabs>
<tab>
<name>Tab1</name>
<file>tab1.bob</file>
<macros>
</macros>
<group_name/>
<selected_color>
<color red="236" green="236" blue="236">
</color>
</selected_color>
<deselected_color>
<color red="200" green="200" blue="200">
</color>
</deselected_color>
</tab>
<tab>
<name>Tab2</name>
<file>tab2.bob</file>
<macros>
</macros>
<group_name/>
<selected_color>
<color red="236" green="236" blue="236">
</color>
</selected_color>
<deselected_color>
<color red="200" green="200" blue="200">
</color>
</deselected_color>
</tab>
</tabs>
<x>150</x>
<y>120</y>
<width>820</width>
<height>530</height>
<direction>0</direction>
<tab_height>20</tab_height>
</widget>
</display>