Skip to content
Merged
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
10 changes: 7 additions & 3 deletions example/t01-services/synoptic/techui.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ beamline:

components:
fshtr:
desc: Fast Shutter
label: Fast Shutter
prefix: BL01T-EA-FSHTR-01

d1:
desc: Diode 1
label: Diode 1
prefix: BL01T-DI-PHDGN-01
file: test.bob

motor:
desc: Motor Stage
label: Motor Stage
prefix: BL01T-MO-MOTOR-01
extras:
- BL01T-MO-BRICK-01
child_labels:
X: X1
Y: Y1
Z: Z1
6 changes: 5 additions & 1 deletion src/techui_builder/autofill.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ def replace_content(
tag_name = "description"

if component_attr is None:
component_attr = component_name
component_attr = (
component_name
if component.label is None
else component.label
)

case "file":
tag_name = "file"
Expand Down
86 changes: 78 additions & 8 deletions src/techui_builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,13 @@ def create_screens(self):

# ONLY IF there is a matching component and entity, generate a screen
if component.prefix in self.entities.keys():
# Populate child labels for any entities
# with the same prefix as the component
for entity in self.entities[component.prefix]:
entity.child_labels = component.child_labels

screen_entities.extend(self.entities[component.prefix])

if component.extras is not None:
# If component has any extras, add them to the entries to generate
for extra_p in component.extras:
Expand All @@ -235,7 +241,7 @@ def create_screens(self):
# This is used by both generate and validate,
# so called beforehand for tidyness
self.generator.build_widgets(component_name, screen_entities)
self.generator.build_groups(component_name)
self.generator.build_groups(component_name, self.conf.components)

screens_to_validate = list(self.validator.validate.keys())

Expand All @@ -251,7 +257,13 @@ def create_screens(self):
" any P field in the ioc.yaml files in services"
)

def _generate_json_map(self, screen_path: Path, dest_path: Path) -> JsonMap:
def _generate_json_map(
self,
screen_path: Path,
dest_path: Path,
current_component_name: str | None = None,
name_elem: str | None = None,
) -> JsonMap:
"""Recursively generate JSON map from .bob file tree"""

# Create initial node at top of .bob file
Expand All @@ -260,6 +272,10 @@ def _generate_json_map(self, screen_path: Path, dest_path: Path) -> JsonMap:
display_name=None,
)

# Get Current Component
if current_component_name is None and screen_path.stem in self.conf.components:
current_component_name = screen_path.stem

abs_path = screen_path.absolute()

try:
Expand All @@ -271,7 +287,11 @@ def _generate_json_map(self, screen_path: Path, dest_path: Path) -> JsonMap:
current_node.display_name = self._parse_display_name(
root.name.text, screen_path
)

current_node.display_name = self._get_component_label(
name_elem,
current_component_name,
current_node.display_name,
)
# Find all <widget> elements
widgets = [
w
Expand Down Expand Up @@ -307,6 +327,14 @@ def _generate_json_map(self, screen_path: Path, dest_path: Path) -> JsonMap:
case _:
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,
)

# Extract file path from file_elem
file_path = Path(file_elem.text.strip() if file_elem.text else "")

Expand All @@ -315,15 +343,20 @@ def _generate_json_map(self, screen_path: Path, dest_path: Path) -> JsonMap:
continue

# Create valid displayName
display_name = self._parse_display_name(name_elem, file_path)
display_name = self._parse_display_name(display_name, file_path)

# TODO: misleading var name?
next_file_path = dest_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, dest_path)
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)
Expand All @@ -340,10 +373,43 @@ def _generate_json_map(self, screen_path: Path, dest_path: Path) -> JsonMap:
except Exception as e:
current_node.error = str(e)

self._fix_duplicate_names(current_node)
self._fix_names_json_map(current_node)

return current_node

def _get_component_label(
self,
name_elem: str | None,
current_component_name: str | None,
display_name: str | None,
) -> str | None:
"""
Get display name from the label or child labels if they exist, otherwise return
name_elem or existing display_name if name_elem is None.
"""
component = self.conf.components
if name_elem is not None:
if name_elem in component.keys() and component[name_elem].label is not None:
display_name = component[name_elem].label
elif (
current_component_name is not None
and (current_component_name in component.keys())
and (component[current_component_name].child_labels is not None)
):
child_labels = component[current_component_name].child_labels
if child_labels is not None:
# Because name_elem is initially grabbed from
# the .bob file, the generated .bobfile might have
# already propagated the child label from techui.yaml
if name_elem in child_labels.values():
display_name = name_elem
# In the case of screens not regenerated, such as validated screens,
# the name text will not be updated to the childlabel,so we check
# keys solely for generating the json_map from the top level .bob.
elif name_elem in child_labels:
display_name = child_labels[name_elem]
return display_name

def _extract_action_button_file_from_embedded(
self, file_elem: ObjectifiedElement, dest_path: Path
) -> ObjectifiedElement:
Expand Down Expand Up @@ -398,7 +464,10 @@ def _parse_display_name(self, name: str | None, file_path: Path) -> str | None:
# Populate displayName with null
return None

def _fix_duplicate_names(self, node: JsonMap) -> None:
def _fix_names_json_map(
self,
node: JsonMap,
) -> None:
"""Recursively fix duplicate display names in children"""
if not node.children:
return
Expand All @@ -412,6 +481,7 @@ def _fix_duplicate_names(self, node: JsonMap) -> None:
for name, children in name_groups.items():
if name and len(children) > 1:
# append pv names when present

for child in children:
if "P" in child.macros:
child.display_name = f"{name} ({child.macros['P']})"
Expand All @@ -423,7 +493,7 @@ def _fix_duplicate_names(self, node: JsonMap) -> None:

# recursively fix children
for child in node.children:
self._fix_duplicate_names(child)
self._fix_names_json_map(child)

def write_json_map(
self,
Expand Down
28 changes: 24 additions & 4 deletions src/techui_builder/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from phoebusgen import widget as pwidget
from phoebusgen.widget.widgets import ActionButton, EmbeddedDisplay, Group

from techui_builder.models import Entity
from techui_builder.models import Component, Entity

logger_ = logging.getLogger(__name__)

Expand Down Expand Up @@ -40,6 +40,7 @@ class Generator:
widget_x: int = field(default=0, init=False, repr=False)
widget_count: int = field(default=0, init=False, repr=False)
group_padding: int = field(default=50, init=False, repr=False)
label_flag: bool = field(default=False, init=False, repr=False)

def __post_init__(self):
# This needs to be before _read_map()
Expand Down Expand Up @@ -173,6 +174,14 @@ def _initialise_name_suffix(self, component: Entity) -> tuple[str, str, str | No
suffix = ""
suffix_label = ""

name = name.removeprefix(":").removesuffix(":")
# Try to get name from child labels if they exist,
# if not, just use the name as it is.
if component.child_labels is not None:
if name in component.child_labels.keys():
name = component.child_labels[name]
self.label_flag = True

return (name, suffix, suffix_label)

def _is_list_of_dicts(self, scrn_mapping: Mapping) -> bool:
Expand Down Expand Up @@ -201,7 +210,8 @@ def _allocate_widget(
)
if match:
suffix_label: str | None = match.group(2)
name: str = suffix
if self.label_flag is False:
name = suffix
except KeyError:
pass

Expand All @@ -221,6 +231,7 @@ def _allocate_widget(
new_widget.macro(
f"{suffix_label}", suffix.removeprefix(":").removesuffix(":")
)
new_widget.macro("label", name.removeprefix(":").removesuffix(":"))
# TODO: Change this to pvi_button
if True:
new_widget.macro("IOC", f"{self.beamline_url}/{component.service_name}")
Expand Down Expand Up @@ -260,6 +271,7 @@ def _allocate_widget(

# For some reason the version of action buttons is 3.0.0?
new_widget.version("2.0.0")
self.label_flag = False
return new_widget

def _create_widget(
Expand Down Expand Up @@ -367,7 +379,7 @@ def build_widgets(self, screen_name: str, screen_components: list[Entity]):
continue
self.widgets.append(new_widget)

def build_groups(self, screen_name: str):
def build_groups(self, screen_name: str, builder_components: dict[str, Component]):
"""
Create a group to fill with widgets
"""
Expand All @@ -381,8 +393,16 @@ def build_groups(self, screen_name: str):
# that will be created.
height, width = self._get_group_dimensions(self.widgets)

if (
screen_name in builder_components.keys()
and builder_components[screen_name].label is not None
):
label = builder_components[screen_name].label or screen_name
else:
label = screen_name

self.group = Group(
screen_name,
label,
0,
0,
width,
Expand Down
9 changes: 8 additions & 1 deletion src/techui_builder/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ class Component(BaseModel):
"""One UI Component from techui.yaml `components:` dictionary"""

prefix: Annotated[str, Field(description="Component PV Prefix")]
desc: Annotated[str | None, Field(description="Component description")] = None
label: Annotated[str | None, Field(description="Component label")] = None
child_labels: Annotated[
dict[str, str] | None, Field(description="Component Children Label")
] = None
extras: Annotated[
list[str] | None,
Field(
Expand Down Expand Up @@ -273,6 +276,10 @@ class Entity(BaseModel):
desc: Annotated[
str | None, Field(description="Optional description of module entity")
] = None
child_labels: Annotated[
dict[str, str] | None,
Field(description="Optional child labels for module entity"),
] = None
M: Annotated[str | None, Field(description="Optional PV suffix for a motor")]
R: Annotated[
str | None, Field(description="Optional PV suffix for an ADAravis plugin")
Expand Down
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def builder_with_test_files(builder: Builder):
return builder


@pytest.fixture
def components(builder_with_test_files: Builder):
return builder_with_test_files.conf.components


@pytest.fixture
def test_files():
screen_path = Path("tests/test_files/test_bob.bob").absolute()
Expand Down
2 changes: 1 addition & 1 deletion tests/test_autofiller.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def test_autofiller_replace_content(
# Cannot use a Mock object as need P to be computed
fake_component = Component(
prefix=prefix,
desc=description,
label=description,
file=filename,
macros=macros,
)
Expand Down
Loading