From 054702fdef3e13c5c4f977aba4b8e6b8b5bab082 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Tue, 29 Sep 2026 10:55:51 +0000 Subject: [PATCH 1/8] ref: changed the pipeline of main.py to go to bob_parser first --- src/techui_builder/__main__.py | 2 +- src/techui_builder/autofill.py | 4 +-- src/techui_builder/bob_parser.py | 31 ++++++++++++++++++ src/techui_builder/main_app.py | 56 ++++++++++++++------------------ src/techui_builder/utils.py | 4 ++- 5 files changed, 62 insertions(+), 35 deletions(-) create mode 100644 src/techui_builder/bob_parser.py diff --git a/src/techui_builder/__main__.py b/src/techui_builder/__main__.py index 97a4fb4f..1cab69cd 100644 --- a/src/techui_builder/__main__.py +++ b/src/techui_builder/__main__.py @@ -31,7 +31,7 @@ `-- synoptic\n . |-- techui-support/\n | | `-- ...\n - . |-- techui.yaml\n + . |-- techui.yaml?\n . `-- index.bob\n """, no_args_is_help=True, diff --git a/src/techui_builder/autofill.py b/src/techui_builder/autofill.py index c4afbd13..9e64db04 100644 --- a/src/techui_builder/autofill.py +++ b/src/techui_builder/autofill.py @@ -29,7 +29,7 @@ class Autofiller: def read_bobs(self) -> None: for path in self.index_paths: - tree, widget_dict = read_bob(path) + tree, widget_dict, _ = read_bob(path) self.index_trees[path] = (tree, widget_dict) def autofill_bobs(self) -> None: @@ -63,7 +63,7 @@ def _autofill_from_path(self, path: Path): self._autofill_from_path(resolved_path) else: if resolved_path.exists(): - nav_tree, nav_widgets = read_bob(resolved_path) + nav_tree, nav_widgets, _ = read_bob(resolved_path) self.index_trees[resolved_path] = ( nav_tree, nav_widgets, diff --git a/src/techui_builder/bob_parser.py b/src/techui_builder/bob_parser.py new file mode 100644 index 00000000..daa525fd --- /dev/null +++ b/src/techui_builder/bob_parser.py @@ -0,0 +1,31 @@ +import logging +from dataclasses import dataclass, field +from pathlib import Path + +from lxml.etree import ElementTree +from lxml.objectify import ObjectifiedElement + +from techui_builder.utils import ( + read_bob, +) + +logger_ = logging.getLogger(__name__) + +macros = dict[str, str] +WidgetDict = dict[str, ObjectifiedElement] +TreeWidgetDictTuple = tuple[ElementTree, WidgetDict] +MacroTreeTuple = tuple[macros, TreeWidgetDictTuple] +IndexObjectDict = dict[Path, MacroTreeTuple] + + +@dataclass +class BobParser: + bob_path: Path + index_trees: IndexObjectDict = field(default_factory=dict, init=False, repr=False) + + def read_bob(self): + # Read the bob file + tree, widget_dict, macros = read_bob(self.bob_path) + self.index_trees[self.bob_path] = (macros, (tree, widget_dict)) + print(self.index_trees) + return self.index_trees diff --git a/src/techui_builder/main_app.py b/src/techui_builder/main_app.py index d20fbfe0..7d5bfe66 100644 --- a/src/techui_builder/main_app.py +++ b/src/techui_builder/main_app.py @@ -6,8 +6,7 @@ import typer from techui_builder._logger import log_level -from techui_builder.autofill import Autofiller -from techui_builder.builder import Builder +from techui_builder.bob_parser import BobParser logger_ = logging.getLogger(__name__) @@ -114,16 +113,9 @@ def find_index_bobs( # This is the 'build' behaviour -@app.command("build", help="Run `techui-builder build` for a given techui.yaml") +@app.command("build", help="Run `techui-builder build` for a given index.bob") def main( - filename: Annotated[Path, typer.Argument(help="The path to techui.yaml")], - bobfile: Annotated[ - Path | None, - typer.Argument( - help="Override for template bob file location. This will be used to find" - " and other template bob files in the same location with similar names." - ), - ] = None, + filename: Annotated[Path, typer.Argument(help="The path to index.bob")], loglevel: Annotated[ str, typer.Option( @@ -137,33 +129,35 @@ def main( ) -> None: """Function to run when `techui-builder build` is called.""" - gui = Builder(techui=filename) + # gui = Builder(techui=filename) - ixx_services_dir, synoptic_dir = find_dirs(filename, gui.conf.beamline.domain) + # ixx_services_dir, synoptic_dir = find_dirs(filename, gui.conf.beamline.domain) - index_bob_path, bob_files = find_index_bobs(bobfile, synoptic_dir) + # index_bob_path, bob_files = find_index_bobs(filename, synoptic_dir) # # Overwrite after initialised to make sure this is picked up - gui._services_dir = ixx_services_dir / "services" # noqa: SLF001 - gui._write_directory = synoptic_dir # noqa: SLF001 + # gui._services_dir = ixx_services_dir / "services" # noqa: SLF001 + # gui._write_directory = synoptic_dir # noqa: SLF001 - logger_.debug( - f""" + # logger_.debug( + # f""" -Builder created for {gui.conf.beamline.domain}. -Services directory: {gui._services_dir} -Write directory: {gui._write_directory} -""", # noqa: SLF001 - ) + # Builder created for {gui.conf.beamline.domain}. + # Services directory: {gui._services_dir} + # Write directory: {gui._write_directory} + # """, # noqa: SLF001 + # ) - gui.setup() - gui.create_screens() + # gui.setup() + # gui.create_screens() - logger_.info(f"Screens generated for {gui.conf.beamline.domain}.") + # logger_.info(f"Screens generated for {gui.conf.beamline.domain}.") - autofiller = Autofiller(bob_files, index_bob_path, gui.conf.components) - autofiller.read_bobs() - autofiller.autofill_bobs() - autofiller.write_bobs() + # autofiller = Autofiller(bob_files, index_bob_path, gui.conf.components) + # Extract information from index.bob + bp = BobParser(filename) + bp.read_bob() + # autofiller.autofill_bobs() + # autofiller.write_bobs() - logger_.info(f"Screens autofilled for {gui.conf.beamline.domain}.") + # logger_.info(f"Screens autofilled for {gui.conf.beamline.domain}.") diff --git a/src/techui_builder/utils.py b/src/techui_builder/utils.py index 8e67e459..7ae358d9 100644 --- a/src/techui_builder/utils.py +++ b/src/techui_builder/utils.py @@ -26,7 +26,9 @@ def read_bob(path): widgets = get_widgets(root) - return tree, widgets + macros = _get_macros(root) + + return tree, widgets, macros def get_widgets(root: ObjectifiedElement): From 1e369e6394125878b47afb2d9fff65bb114d784b Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Tue, 29 Sep 2026 13:09:14 +0000 Subject: [PATCH 2/8] ref: using models instead of linked dictionaries for Widget and File type --- src/techui_builder/bob_parser.py | 57 +++++++++++++++++++++----------- src/techui_builder/main_app.py | 12 +++++-- src/techui_builder/models.py | 47 +++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/techui_builder/bob_parser.py b/src/techui_builder/bob_parser.py index daa525fd..a3466932 100644 --- a/src/techui_builder/bob_parser.py +++ b/src/techui_builder/bob_parser.py @@ -1,31 +1,50 @@ import logging -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path -from lxml.etree import ElementTree from lxml.objectify import ObjectifiedElement -from techui_builder.utils import ( - read_bob, -) +from techui_builder.models import BobFile, BobWidget +from techui_builder.utils import WidgetType, _get_macros, read_bob logger_ = logging.getLogger(__name__) -macros = dict[str, str] -WidgetDict = dict[str, ObjectifiedElement] -TreeWidgetDictTuple = tuple[ElementTree, WidgetDict] -MacroTreeTuple = tuple[macros, TreeWidgetDictTuple] -IndexObjectDict = dict[Path, MacroTreeTuple] - @dataclass class BobParser: bob_path: Path - index_trees: IndexObjectDict = field(default_factory=dict, init=False, repr=False) - - def read_bob(self): - # Read the bob file - tree, widget_dict, macros = read_bob(self.bob_path) - self.index_trees[self.bob_path] = (macros, (tree, widget_dict)) - print(self.index_trees) - return self.index_trees + + def _parse_widgets(self, container: ObjectifiedElement) -> list[BobWidget]: + result = [] + + for element in container.iterchildren("widget"): + name_element = element.find("name") + name = name_element.text if name_element is not None else "" + + widget = BobWidget( + name=name or "", + widget_type=element.get("type", default=""), + macros=_get_macros(element), + element=element, + children=None, + ) + if widget.widget_type == "group": + widget.children = self._parse_widgets(element) + + if widget.widget_type in WidgetType: + result.append(widget) + + return result + + def parse_bob( + self, + ) -> BobFile: + tree, _, display_macros = read_bob(self.bob_path) + root = tree.getroot() + + return BobFile( + path=self.bob_path, + macros=display_macros, + tree=tree, + widgets=self._parse_widgets(root), + ) diff --git a/src/techui_builder/main_app.py b/src/techui_builder/main_app.py index 7d5bfe66..139a3145 100644 --- a/src/techui_builder/main_app.py +++ b/src/techui_builder/main_app.py @@ -19,13 +19,13 @@ def find_dirs(file_path: Path, beamline: str) -> tuple: # Get the relative path to the techui file from working dir abs_path = file_path.absolute() - logger_.debug(f"techui.yaml absolute path: {abs_path}") + logger_.debug(f"index.bob absolute path: {abs_path}") # Get the current working dir cwd = Path.cwd() logger_.debug(f"Working directory: {cwd}") - directory = beamline + directory = beamline.lower() # Get the relative path of ixx-services to techui.yaml ixx_services_dir = next( @@ -156,7 +156,13 @@ def main( # autofiller = Autofiller(bob_files, index_bob_path, gui.conf.components) # Extract information from index.bob bp = BobParser(filename) - bp.read_bob() + synoptic_macros = bp.parse_bob() + print(synoptic_macros) + ixx_services_dir, synoptic_dir = find_dirs( + filename, synoptic_macros.macros["domain"] + ) + # Need to provide builder the macros to work on generating and validating bob files. + # autofiller.autofill_bobs() # autofiller.write_bobs() diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index d926baed..db9743c5 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -1,7 +1,10 @@ import logging import re +from pathlib import Path from typing import Annotated, Any +from lxml.etree import ElementTree +from lxml.objectify import ObjectifiedElement from pydantic import ( BaseModel, ConfigDict, @@ -13,7 +16,7 @@ logger_ = logging.getLogger(__name__) - +type MacroMap = dict[str, str] # Patterns: # long: 'bl23b' # short: 'b23', 'ixx-1' @@ -329,3 +332,45 @@ class TechUiSupport(BaseModel): dict[str, SupportEntity], Field(description="The dictionary of techui-support.yaml entities"), ] + + +""" +BobParser Models +""" + + +class BobWidget(BaseModel): + """Class representation of Bob Widget""" + + name: Annotated[str, Field(description="Widget name")] + widget_type: Annotated[str, Field(description="Widget type")] + macros: Annotated[MacroMap, Field(description="Macro dictionary")] + element: Annotated[ + ObjectifiedElement, + Field(description="XML tree representation of the widget", repr=False), + ] + children: Annotated[ + list["BobWidget"] | None, + Field(description="Children of parent BobWidget", default_factory=list), + ] + model_config = ConfigDict( + arbitrary_types_allowed=True, + ) + + +class BobFile(BaseModel): + """Class representation of BobFile suchs as index.bob and {component}.bob""" + + path: Annotated[Path, Field(description="Path to file")] + macros: Annotated[MacroMap, Field(description="Macro dictionary")] + tree: Annotated[ + ElementTree, + Field(description="XML tree representation of the file", repr=False), + ] + widgets: Annotated[ + list[BobWidget], + Field(description="Widgets in the .bob file", default_factory=list), + ] + model_config = ConfigDict( + arbitrary_types_allowed=True, + ) From 2f6cacf754ebc88160a9fcf73b2bfb43e293ebb4 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Tue, 29 Sep 2026 14:01:05 +0000 Subject: [PATCH 3/8] ref: Replacing techui with bob_description --- src/techui_builder/builder.py | 18 +++++++++++------- src/techui_builder/main_app.py | 3 ++- src/techui_builder/models.py | 13 ++++++++++++- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 18d84909..64facbe5 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -9,7 +9,7 @@ from jinja2 import Template from techui_builder.generate import Generator -from techui_builder.models import Entity, SupportEntity, TechUi, TechUiSupport +from techui_builder.models import BobFile, Entity, SupportEntity, TechUi, TechUiSupport from techui_builder.validator import Validator logger_ = logging.getLogger(__name__) @@ -32,7 +32,8 @@ class Builder: """ - techui: Path = field(default=Path("techui.yaml")) + bob_description: BobFile + techui: Path = field(init=False, repr=False) entities: defaultdict[str, list[Entity]] = field( default_factory=lambda: defaultdict(list), init=False @@ -42,9 +43,10 @@ class Builder: def __post_init__(self): # Populate beamline and components - self.conf = TechUi.model_validate( - yaml.safe_load(self.techui.read_text(encoding="utf-8")) - ) + if self.techui: + self.conf = TechUi.model_validate( + yaml.safe_load(self.techui.read_text(encoding="utf-8")) + ) def setup(self): """ @@ -62,7 +64,7 @@ def setup(self): self.generator = Generator( self._write_directory, - self.conf.beamline.url, + self.bob_description.macros["url"], self.support_path, self.techui_support, ) @@ -109,7 +111,9 @@ def _extract_services(self): """ # Loop over every dir in services, ignoring anything that isn't a service - for service in self._services_dir.glob(f"{self.conf.beamline.location}-*-*-*"): + for service in self._services_dir.glob( + f"{self.bob_description.macros['location']}-*-*-*" + ): service_name = service.name # If service doesn't exist, file open will fail throwing exception try: diff --git a/src/techui_builder/main_app.py b/src/techui_builder/main_app.py index 139a3145..284ce313 100644 --- a/src/techui_builder/main_app.py +++ b/src/techui_builder/main_app.py @@ -162,7 +162,8 @@ def main( filename, synoptic_macros.macros["domain"] ) # Need to provide builder the macros to work on generating and validating bob files. - + index_bob_path, bob_files = find_index_bobs(filename, synoptic_dir) + print(index_bob_path, bob_files) # autofiller.autofill_bobs() # autofiller.write_bobs() diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index db9743c5..c664b401 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -297,7 +297,17 @@ class Entity(BaseModel): "ADAravis.aravisCamera" ), ] - prefix: Annotated[str, Field(description="PV Prefix for module entity")] + prefix: Annotated[ + str, + Field( + description="Component PV Prefix", + # Make sure vscode is aware of schema validation + json_schema_extra={ + "pattern": _DLS_PREFIX_PATTERN_COMPACT, + "type": "string", + }, + ), + ] desc: Annotated[ str | None, Field(description="Optional description of module entity") ] = None @@ -345,6 +355,7 @@ class BobWidget(BaseModel): name: Annotated[str, Field(description="Widget name")] widget_type: Annotated[str, Field(description="Widget type")] macros: Annotated[MacroMap, Field(description="Macro dictionary")] + prefix: Annotated[str, _DLS_PREFIX_PATTERN_VERBOSE] element: Annotated[ ObjectifiedElement, Field(description="XML tree representation of the widget", repr=False), From 52bd174b6cc44a2398cb682757ae16fe128a8fcb Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Tue, 29 Sep 2026 14:52:19 +0000 Subject: [PATCH 4/8] ref: trying to establish contract between builder and parser --- src/techui_builder/bob_parser.py | 3 +++ src/techui_builder/builder.py | 15 ++++++++++++++- src/techui_builder/models.py | 12 +++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/techui_builder/bob_parser.py b/src/techui_builder/bob_parser.py index a3466932..c7b3e5bb 100644 --- a/src/techui_builder/bob_parser.py +++ b/src/techui_builder/bob_parser.py @@ -20,6 +20,8 @@ def _parse_widgets(self, container: ObjectifiedElement) -> list[BobWidget]: for element in container.iterchildren("widget"): name_element = element.find("name") name = name_element.text if name_element is not None else "" + prefix_element = element.find("pv_name") + prefix = prefix_element.text if prefix_element is not None else "" widget = BobWidget( name=name or "", @@ -27,6 +29,7 @@ def _parse_widgets(self, container: ObjectifiedElement) -> list[BobWidget]: macros=_get_macros(element), element=element, children=None, + prefix=prefix or "", ) if widget.widget_type == "group": widget.children = self._parse_widgets(element) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 64facbe5..562fbf26 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -9,7 +9,14 @@ from jinja2 import Template from techui_builder.generate import Generator -from techui_builder.models import BobFile, Entity, SupportEntity, TechUi, TechUiSupport +from techui_builder.models import ( + BobFile, + Component, + Entity, + SupportEntity, + TechUi, + TechUiSupport, +) from techui_builder.validator import Validator logger_ = logging.getLogger(__name__) @@ -47,6 +54,12 @@ def __post_init__(self): self.conf = TechUi.model_validate( yaml.safe_load(self.techui.read_text(encoding="utf-8")) ) + for widget in self.bob_description.widgets: + if widget.prefix: + self.conf.components[widget.name] = Component( + prefix=widget.prefix, + label=widget.name, + ) def setup(self): """ diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index c664b401..6f2c56aa 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -355,7 +355,17 @@ class BobWidget(BaseModel): name: Annotated[str, Field(description="Widget name")] widget_type: Annotated[str, Field(description="Widget type")] macros: Annotated[MacroMap, Field(description="Macro dictionary")] - prefix: Annotated[str, _DLS_PREFIX_PATTERN_VERBOSE] + prefix: Annotated[ + str, + Field( + description="Widget PV Name", + # Make sure vscode is aware of schema validation + json_schema_extra={ + "pattern": _DLS_PREFIX_PATTERN_VERBOSE, + "type": "string", + }, + ), + ] element: Annotated[ ObjectifiedElement, Field(description="XML tree representation of the widget", repr=False), From ebcf04338067092ba954ae967ad3eb870d71b726 Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Wed, 30 Sep 2026 08:56:00 +0000 Subject: [PATCH 5/8] ref: converted bob_descriptions into components --- src/techui_builder/builder.py | 21 +++++++++++++++------ src/techui_builder/main_app.py | 30 +++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 562fbf26..a4dc6929 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -40,7 +40,7 @@ class Builder: """ bob_description: BobFile - techui: Path = field(init=False, repr=False) + techui: Path = field(init=True, repr=False) entities: defaultdict[str, list[Entity]] = field( default_factory=lambda: defaultdict(list), init=False @@ -55,11 +55,20 @@ def __post_init__(self): yaml.safe_load(self.techui.read_text(encoding="utf-8")) ) for widget in self.bob_description.widgets: - if widget.prefix: - self.conf.components[widget.name] = Component( - prefix=widget.prefix, - label=widget.name, - ) + values = { + "prefix": widget.prefix, + "label": widget.name, + } + values.update( + { + key: value + for key, value in widget.macros.items() + if key in Component.model_fields and key not in {"prefix", "label"} + } + ) + + if values["prefix"]: + self.conf.components[widget.name] = Component(**values) def setup(self): """ diff --git a/src/techui_builder/main_app.py b/src/techui_builder/main_app.py index 284ce313..497c201f 100644 --- a/src/techui_builder/main_app.py +++ b/src/techui_builder/main_app.py @@ -6,7 +6,9 @@ import typer from techui_builder._logger import log_level +from techui_builder.autofill import Autofiller from techui_builder.bob_parser import BobParser +from techui_builder.builder import Builder logger_ = logging.getLogger(__name__) @@ -129,8 +131,6 @@ def main( ) -> None: """Function to run when `techui-builder build` is called.""" - # gui = Builder(techui=filename) - # ixx_services_dir, synoptic_dir = find_dirs(filename, gui.conf.beamline.domain) # index_bob_path, bob_files = find_index_bobs(filename, synoptic_dir) @@ -164,7 +164,27 @@ def main( # Need to provide builder the macros to work on generating and validating bob files. index_bob_path, bob_files = find_index_bobs(filename, synoptic_dir) print(index_bob_path, bob_files) - # autofiller.autofill_bobs() - # autofiller.write_bobs() - # logger_.info(f"Screens autofilled for {gui.conf.beamline.domain}.") + gui = Builder(synoptic_macros, synoptic_dir / "techui.yaml") + gui._services_dir = ixx_services_dir / "services" # noqa: SLF001 + gui._write_directory = synoptic_dir # noqa: SLF001 + + logger_.debug( + f""" + + Builder created for {gui.conf.beamline.domain}. + Services directory: {gui._services_dir} + Write directory: {gui._write_directory} + """, # noqa: SLF001 + ) + + gui.setup() + gui.create_screens() + + logger_.info(f"Screens generated for {gui.conf.beamline.domain}.") + + autofiller = Autofiller(bob_files, index_bob_path, gui.conf.components) + autofiller.autofill_bobs() + autofiller.write_bobs() + + logger_.info(f"Screens autofilled for {gui.conf.beamline.domain}.") From a8443ff018bc7ee0c84d1bd8a0617c4790c80cfc Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Wed, 30 Sep 2026 12:20:30 +0000 Subject: [PATCH 6/8] tests: fixed tests to work with the new bob parser stage --- example/t01-services/synoptic/index.bob | 22 ++++++++++++++-------- tests/conftest.py | 14 +++++++++++--- tests/test_autofiller.py | 2 +- tests/test_builder.py | 3 ++- tests/test_cli.py | 7 +++++++ tests/test_utils.py | 3 ++- 6 files changed, 37 insertions(+), 14 deletions(-) diff --git a/example/t01-services/synoptic/index.bob b/example/t01-services/synoptic/index.bob index e6cd7aca..200ed601 100644 --- a/example/t01-services/synoptic/index.bob +++ b/example/t01-services/synoptic/index.bob @@ -1,7 +1,13 @@ - - + + Display + + bl01t + t01 + Test Beamline + t01-opis.diamond.ac.uk + dcam1_Group 250 @@ -12,6 +18,7 @@ true dcam1 + BL01T-DI-CAM-01:STA techui-support/symbols/camera.svg @@ -26,11 +33,10 @@ replace + true $(pv_name) $(pv_value) $(actions) - BL01T-DI-CAM-01:STA - true dcam1_L:abel @@ -51,6 +57,7 @@ $(actions) true diode1 + BL01T-DI-PHDGN-01:STA techui-support/symbols/oscilloscope.svg @@ -65,11 +72,10 @@ $(actions) replace + true $(pv_name) $(pv_value) $(actions) - BL01T-DI-PHDGN-01:STA - true diode1_Label @@ -102,10 +108,10 @@ $(actions) replace + true $(pv_name) $(pv_value) $(actions) - true temp1_label @@ -141,10 +147,10 @@ $(actions) replace + true $(pv_name) $(pv_value) $(actions) - true motor1_label diff --git a/tests/conftest.py b/tests/conftest.py index d96898b4..17da3bad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,11 +8,12 @@ from phoebusgen import widget as pwidget from techui_builder.autofill import Autofiller +from techui_builder.bob_parser import BobParser from techui_builder.builder import Builder from techui_builder.generate import Generator from techui_builder.generate_jsonmap import JsonMapGenerator from techui_builder.jsonmap.nodes import ScreenNode -from techui_builder.models import Component, SupportEntity +from techui_builder.models import BobFile, Component, SupportEntity from techui_builder.status import GenerateStatusPvs from techui_builder.validator import Validator @@ -25,6 +26,12 @@ def tmp_t01_services(tmp_path) -> Path: return tmp_path / "t01-services" +@pytest.fixture +def bob_parser(tmp_t01_services) -> BobFile: + bob_document = BobParser(tmp_t01_services / "synoptic/index.bob").parse_bob() + return bob_document + + @pytest.fixture def tmp_test_files(tmp_path) -> Path: # Copy test files to tmp_path so they are visible to the tests @@ -34,11 +41,12 @@ def tmp_test_files(tmp_path) -> Path: @pytest.fixture -def builder(tmp_t01_services): +def builder(tmp_t01_services, bob_parser): ixx_services = tmp_t01_services + bob_document = bob_parser techui_path = ixx_services / "synoptic/techui.yaml" - b = Builder(techui_path) + b = Builder(bob_document, techui_path) b._services_dir = ixx_services / "services" b._write_directory = ixx_services / "synoptic" return b diff --git a/tests/test_autofiller.py b/tests/test_autofiller.py index f41be6a9..4a256450 100644 --- a/tests/test_autofiller.py +++ b/tests/test_autofiller.py @@ -12,7 +12,7 @@ # Imported in to autofill from utils, so that needs to be patched @patch("techui_builder.autofill.read_bob") def test_autofiller_read_bobs(mock_read_bob: MagicMock, autofiller): - mock_read_bob.return_value = (Mock(spec=ElementTree), Mock()) + mock_read_bob.return_value = (Mock(spec=ElementTree), Mock(), Mock()) autofiller.read_bobs() diff --git a/tests/test_builder.py b/tests/test_builder.py index bc9766f1..7f53ceaa 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -185,8 +185,9 @@ def test_gb_extract_services_both_yaml_files( builder._extract_entities = Mock() # overwrite to not see the bl01t service dirs - builder.conf.beamline.location = "bl01z" + builder.bob_description.macros["location"] = "bl01z" builder._services_dir = tmp_path + # Temporary files to test against (tmp_path / "bl01z-ea-temp-01").mkdir() (tmp_path / "bl01z-ea-temp-01/config").mkdir() diff --git a/tests/test_cli.py b/tests/test_cli.py index 080477d2..d368f324 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -192,6 +192,7 @@ def test_find_bob_no_bob_file_found(caplog: pytest.LogCaptureFixture): assert exc_info.value.code is None +@patch("techui_builder.main_app.BobParser") @patch("techui_builder.main_app.find_index_bobs") @patch("techui_builder.main_app.find_dirs") @patch("techui_builder.main_app.Autofiller") @@ -201,12 +202,18 @@ def test_main( mock_autofiller: MagicMock, mock_find_dirs: MagicMock, mock_find_index_bobs: MagicMock, + mock_bob_parser: MagicMock, ): mock_index_path = MagicMock(spec=Path) mock_find_dirs.return_value = MagicMock(spec=Path), MagicMock(spec=Path) mock_find_index_bobs.return_value = mock_index_path, [mock_index_path] + mock_bob_parser.return_value.parse_bob.return_value.macros.macros = { + "domain": "ixx" + } + main(mock_index_path) + mock_bob_parser.assert_called_once_with(mock_index_path) mock_find_dirs.assert_called_once() mock_find_index_bobs.assert_called_once() diff --git a/tests/test_utils.py b/tests/test_utils.py index 5a13a244..ee712e77 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -19,10 +19,11 @@ def test_read_bob(mock_get_widgets: MagicMock, tmp_test_files): mock_get_widgets.return_value = {"test_widget": MagicMock(spec=ObjectifiedElement)} - tree, widgets = read_bob(tmp_test_files / "index.bob") + tree, widgets, macros = read_bob(tmp_test_files / "index.bob") assert isinstance(tree, _ElementTree) assert isinstance(widgets["test_widget"], ObjectifiedElement) + assert isinstance(macros, dict) mock_get_widgets.assert_called_once() From 6afe6eded6640378c724c3fd32be51efa4c2c77e Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Thu, 1 Oct 2026 10:15:55 +0000 Subject: [PATCH 7/8] tests:changed builder to use both index.bob and techui.yaml --- src/techui_builder/builder.py | 52 +++++++++++++++++++++------------- src/techui_builder/main_app.py | 2 -- tests/test_builder.py | 4 +-- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index a4dc6929..5f571be5 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -40,7 +40,7 @@ class Builder: """ bob_description: BobFile - techui: Path = field(init=True, repr=False) + techui: Path | None = field(init=True, repr=False) entities: defaultdict[str, list[Entity]] = field( default_factory=lambda: defaultdict(list), init=False @@ -48,27 +48,41 @@ class Builder: _services_dir: Path = field(init=False, repr=False) _write_directory: Path = field(init=False, repr=False) + def _iter_prefixed_widgets(self, widgets): + for widget in widgets: + if widget.prefix and widget.prefix.strip(): + yield widget + + if widget.widget_type == "group": + yield from self._iter_prefixed_widgets(widget.children or []) + def __post_init__(self): - # Populate beamline and components - if self.techui: - self.conf = TechUi.model_validate( - yaml.safe_load(self.techui.read_text(encoding="utf-8")) - ) - for widget in self.bob_description.widgets: + components = {} + + for widget in self._iter_prefixed_widgets(self.bob_description.widgets): values = { - "prefix": widget.prefix, - "label": widget.name, + key: value + for key, value in widget.macros.items() + if key in Component.model_fields and key != "prefix" } - values.update( - { - key: value - for key, value in widget.macros.items() - if key in Component.model_fields and key not in {"prefix", "label"} - } - ) - - if values["prefix"]: - self.conf.components[widget.name] = Component(**values) + values["prefix"] = widget.prefix.strip() + values.setdefault("label", widget.name) + components[widget.name] = values + + macros = self.bob_description.macros + techui_data = { + "beamline": { + key: macros[key] for key in ("location", "domain", "desc", "url") + }, + "components": components, + } + + if self.techui is not None and self.techui.is_file(): + yaml_data = yaml.safe_load(self.techui.read_text(encoding="utf-8")) or {} + techui_data["beamline"].update(yaml_data.get("beamline", {})) + components.update(yaml_data.get("components", {})) + + self.conf = TechUi.model_validate(techui_data) def setup(self): """ diff --git a/src/techui_builder/main_app.py b/src/techui_builder/main_app.py index 497c201f..58d187b5 100644 --- a/src/techui_builder/main_app.py +++ b/src/techui_builder/main_app.py @@ -157,13 +157,11 @@ def main( # Extract information from index.bob bp = BobParser(filename) synoptic_macros = bp.parse_bob() - print(synoptic_macros) ixx_services_dir, synoptic_dir = find_dirs( filename, synoptic_macros.macros["domain"] ) # Need to provide builder the macros to work on generating and validating bob files. index_bob_path, bob_files = find_index_bobs(filename, synoptic_dir) - print(index_bob_path, bob_files) gui = Builder(synoptic_macros, synoptic_dir / "techui.yaml") gui._services_dir = ixx_services_dir / "services" # noqa: SLF001 diff --git a/tests/test_builder.py b/tests/test_builder.py index 7f53ceaa..0fe13686 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -20,7 +20,7 @@ def test_beamline_attributes(builder, attr, expected): @pytest.mark.parametrize( "index, name, label, P, R, attribute, file, extras, child_labels", [ - (0, "fshtr", "Fast Shutter", "BL01T-EA-FSHTR-01", None, None, None, None, None), + (4, "fshtr", "Fast Shutter", "BL01T-EA-FSHTR-01", None, None, None, None, None), ( 1, "diode1", @@ -33,7 +33,7 @@ def test_beamline_attributes(builder, attr, expected): None, ), ( - 4, + 3, "motor1", "Motor Stage", "BL01T-MO-MOTOR-01", From d2522954dcae53e5d23910d7e3ae4dbf8f32eefa Mon Sep 17 00:00:00 2001 From: "Sode, Adedamola (DLSLtd,RAL,LSCI)" Date: Thu, 1 Oct 2026 10:25:25 +0000 Subject: [PATCH 8/8] linting:optional techui.yaml, and new read_bob adjustments --- src/techui_builder/builder.py | 4 +++- src/techui_builder/validator.py | 4 ++-- tests/test_validator.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 5f571be5..9524b0d0 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -238,6 +238,8 @@ def _validate_screen(self, screen_name: str): def create_screens(self): """Create the screens for each component in techui.yaml""" + + techui_name = self.techui.name if self.techui is not None else "techui.yaml" if len(self.entities) == 0: logger_.critical( "No ioc entities found. This [italic]normally[/italic]" @@ -284,7 +286,7 @@ def create_screens(self): else: logger_.warning( - f"{self.techui.name}: The prefix [bold]{component.prefix}[/bold] " + f"{techui_name}: The prefix [bold]{component.prefix}[/bold] " f"set in the component [bold]{component_name}[/bold] does not match" " any P field in the ioc.yaml files in services" ) diff --git a/src/techui_builder/validator.py b/src/techui_builder/validator.py index 2f5257cc..23988f5b 100644 --- a/src/techui_builder/validator.py +++ b/src/techui_builder/validator.py @@ -36,8 +36,8 @@ def _check_bob(self, bob_path: Path): self.validate[bob_path.name.removesuffix(".bob")] = bob_path def _read_bob(self, path: Path): - tree, widgets = read_bob(path) - return tree.getroot(), widgets + tree, widget, _ = read_bob(path) + return tree.getroot(), widget def validate_bob( self, diff --git a/tests/test_validator.py b/tests/test_validator.py index de60c188..de7447ca 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -24,7 +24,7 @@ def test_validator_check_bob(validator): def test_validator_read_bob(mock_read_bob: MagicMock, validator): # We need to set the spec of the first Mock so it knows # it has a getroot() function - mock_read_bob.return_value = (Mock(spec=_ElementTree), Mock()) + mock_read_bob.return_value = (Mock(spec=_ElementTree), Mock(), Mock()) validator._read_bob(validator.bobs[0])