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
2 changes: 1 addition & 1 deletion src/techui_builder/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def main(
)

gui.setup()
gui.generate_screens()
gui.create_screens()

logger_.info(f"Screens generated for {gui.conf.beamline.short_dom}.")

Expand Down
51 changes: 22 additions & 29 deletions src/techui_builder/autofill.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
from collections import defaultdict
from dataclasses import dataclass, field
from pathlib import Path

Expand All @@ -8,6 +9,7 @@

from techui_builder.builder import Builder, _get_action_group
from techui_builder.models import Component
from techui_builder.utils import read_bob

logger_ = logging.getLogger(__name__)

Expand All @@ -16,41 +18,32 @@
class Autofiller:
path: Path
macros: list[str] = field(default_factory=lambda: ["prefix", "desc", "file"])
widgets: dict[str, ObjectifiedElement] = field(
default_factory=defaultdict, init=False, repr=False
)

def read_bob(self) -> None:
# Read the bob file
self.tree = objectify.parse(self.path)

# Find the root tag (in this case: <display version="2.0.0">)
self.root = self.tree.getroot()
self.tree, self.widgets = read_bob(self.path)

def autofill_bob(self, gui: "Builder"):
# Get names from component list

# Loop over objects in the xml
# i.e. every tag below <display version="2.0.0">
# but not any nested tags below them
for child in self.root.iterchildren():
# If widget is a symbol (i.e. a component)
if child.tag == "widget" and child.get("type", default=None) == "symbol":
# Extract it's name
symbol_name = child.name

# If the name exists in the component list
if symbol_name in gui.conf.components.keys():
# Get first copy of component (should only be one)
comp = next(
(comp for comp in gui.conf.components if comp == symbol_name),
)

self.replace_content(
widget=child,
component_name=comp,
component=gui.conf.components[comp],
)

# Add option to allow left mouse click to run action
child["run_actions_on_mouse_click"] = "true"
for symbol_name, child in self.widgets.items():
# If the name exists in the component list
if symbol_name in gui.conf.components.keys():
# Get first copy of component (should only be one)
comp = next(
(comp for comp in gui.conf.components if comp == symbol_name),
)

self.replace_content(
widget=child,
component_name=comp,
component=gui.conf.components[comp],
)

# Add option to allow left mouse click to run action
child["run_actions_on_mouse_click"] = "true"

def write_bob(self, filename: Path):
# Check if data/ dir exists and if not, make it
Expand Down
60 changes: 53 additions & 7 deletions src/techui_builder/builder.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
import os
from collections import defaultdict
from dataclasses import _MISSING_TYPE, dataclass, field
from pathlib import Path
Expand All @@ -11,6 +12,7 @@

from techui_builder.generate import Generator
from techui_builder.models import Entity, TechUi
from techui_builder.validator import Validator

logger_ = logging.getLogger(__name__)

Expand Down Expand Up @@ -57,8 +59,34 @@ def setup(self):
"""Run intial setup, e.g. extracting entries from service ioc.yaml."""
self._extract_services()
synoptic_dir = self._write_directory

self.clean_bobs()

self.generator = Generator(synoptic_dir)

def clean_bobs(self):
exclude = {"index.bob"}
bobs = [
bob
for bob in self._write_directory.glob("*.bob")
if bob.name not in exclude
]

self.validator = Validator(bobs)
self.validator.check_bobs()

# Get bobs that are only present in the bobs list (i.e. generated)
self.generated_bobs = list(set(bobs) ^ set(self.validator.validate.values()))

logger_.info("Preserving edited screens for validation.")
logger_.debug(f"Screens to validate: {list(self.validator.validate.keys())}")

logger_.info("Cleaning synoptic/ of generated screens.")
# Remove any generated bobs that exist
for bob in self.generated_bobs:
logger_.debug(f"Removing generated screen: {bob.name}")
os.remove(bob)

def _extract_services(self):
"""
Finds the services folders in the services directory
Expand Down Expand Up @@ -95,13 +123,20 @@ def _extract_entities(self, ioc_yaml: Path):
)
self.entities[new_entity.P].append(new_entity)

def _generate_screen(self, screen_name: str, screen_components: list[Entity]):
self.generator.load_screen(screen_name, screen_components)
self.generator.build_groups()
self.generator.write_screen(self._write_directory)
def _generate_screen(self, screen_name: str):
self.generator.build_screen(screen_name)
self.generator.write_screen(screen_name, self._write_directory)

def _validate_screen(self, screen_name: str):
# Get the generated widgets to validate against
widgets = self.generator.widgets
widget_group = self.generator.group
assert widget_group is not None
widget_group_name = widget_group.get_element_value("name")
self.validator.validate_bob(screen_name, widget_group_name, widgets)

def generate_screens(self):
"""Generate the screens for each component in techui.yaml"""
def create_screens(self):
"""Create the screens for each component in techui.yaml"""
if len(self.entities) == 0:
logger_.critical("No ioc entities found, has setup() been run?")
exit()
Expand All @@ -124,7 +159,18 @@ def generate_screens(self):
continue
screen_entities.extend(self.entities[extra_p])

self._generate_screen(component_name, screen_entities)
# 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)

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

if component_name in screens_to_validate:
self._validate_screen(component_name)
else:
self._generate_screen(component_name)

else:
logger_.warning(
f"{self.techui.name}: The prefix [bold]{component.prefix}[/bold]\
Expand Down
55 changes: 31 additions & 24 deletions src/techui_builder/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@
class Generator:
synoptic_dir: Path = field(repr=False)

screen_name: str = field(init=False)
screen_components: list[Entity] = field(init=False)

# These are global params for the class (not accessible by user)
support_path: Path = field(init=False, repr=False)
techui_support: dict = field(init=False, repr=False)
Expand All @@ -33,6 +30,7 @@ class Generator:
widgets: list[ActionButton | EmbeddedDisplay] = field(
default_factory=list[ActionButton | EmbeddedDisplay], init=False, repr=False
)
group: Group | None = field(default=None, init=False, repr=False)

# Add group padding, and self.widget_x for placing widget in x direction relative to
# other widgets, with a widget count to reset the self.widget_x dimension when the
Expand All @@ -55,10 +53,6 @@ def _read_map(self):
with open(support_yaml) as map:
self.techui_support = yaml.safe_load(map)

def load_screen(self, screen_name: str, screen_components: list[Entity]):
self.screen_name = screen_name
self.screen_components = screen_components

def _get_screen_dimensions(self, file: str) -> tuple[int, int]:
"""
Parses the bob files for information on the height
Expand Down Expand Up @@ -248,7 +242,7 @@ def _allocate_widget(
return new_widget

def _create_widget(
self, component: Entity
self, name: str, component: Entity
) -> EmbeddedDisplay | ActionButton | None | list[EmbeddedDisplay | ActionButton]:
# if statement below is check if the suffix is
# missing from the component description. If
Expand All @@ -261,7 +255,7 @@ def _create_widget(
except KeyError:
logger_.warning(
f"No available widget for {component.type} in screen \
{self.screen_name}. Skipping..."
{name}. Skipping..."
)
return None

Expand Down Expand Up @@ -337,28 +331,26 @@ def layout_widgets(self, widgets: list[EmbeddedDisplay | ActionButton]):

return sorted_widgets

def build_groups(self):
"""
Create a group to fill with widgets
"""
# Create screen
self.screen_ = pscreen.Screen(self.screen_name)
def build_widgets(self, screen_name: str, screen_components: list[Entity]):
# Empty widget buffer
self.widgets = []

# create widget and group objects

# order is an enumeration of the components, used to list them,
# and serves as functionality in the math for formatting.
for component in self.screen_components:
new_widget = self._create_widget(component=component)
for component in screen_components:
new_widget = self._create_widget(name=screen_name, component=component)
if new_widget is None:
continue
if isinstance(new_widget, list):
self.widgets.extend(new_widget)
continue
self.widgets.append(new_widget)

def build_groups(self, screen_name: str):
"""
Create a group to fill with widgets
"""

if self.widgets == []:
# No widgets found, so just back out
return
Expand All @@ -369,28 +361,43 @@ def build_groups(self):
height, width = self._get_group_dimensions(self.widgets)

self.group = Group(
self.screen_name,
screen_name,
0,
0,
width,
height,
)

# TODO: we shouldn't need this assert; fix
assert self.group is not None
self.group.version("2.0.0")
self.group.add_widget(self.widgets)

def build_screen(self, screen_name):
"""
Build the screen with the widget groups.
"""
# Create screen
self.screen_ = pscreen.Screen(screen_name)

# TODO: I don't like this
if self.group is None:
# No group found, so just back out
return

self.screen_.add_widget(self.group)

def write_screen(self, directory: Path):
def write_screen(self, screen_name: str, directory: Path):
"""Write the screen to file"""

if self.widgets == []:
logger_.warning(
f"Could not write screen: {self.screen_name} \
f"Could not write screen: {screen_name} \
as no widgets were available"
)
return

if not directory.exists():
os.mkdir(directory)
self.screen_.write_screen(f"{directory}/{self.screen_name}.bob")
logger_.info(f"{self.screen_name}.bob has been created successfully")
self.screen_.write_screen(f"{directory}/{screen_name}.bob")
logger_.info(f"{screen_name}.bob has been created successfully")
32 changes: 32 additions & 0 deletions src/techui_builder/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from lxml import objectify
from lxml.objectify import ObjectifiedElement


def read_bob(path):
# Read the bob file
tree = objectify.parse(path)

# Find the root tag (in this case: <display version="2.0.0">)
root = tree.getroot()

widgets = get_widgets(root)

return tree, widgets


def get_widgets(root: ObjectifiedElement):
widgets: dict[str, ObjectifiedElement] = {}
# Loop over objects in the xml
# i.e. every tag below <display version="2.0.0">
# but not any nested tags below them
for child in root.iterchildren():
# If widget is a symbol (i.e. a component)
if child.tag == "widget" and child.get("type", default=None) in [
"symbol",
"group",
]:
name = child.name.text
assert name is not None
widgets[name] = child

return widgets
Loading