Skip to content

Commit 29aac21

Browse files
authored
Implement screen validation (#162)
2 parents 5ab1098 + c4bf483 commit 29aac21

File tree

13 files changed

+353
-93
lines changed

13 files changed

+353
-93
lines changed

src/techui_builder/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def main(
155155
)
156156

157157
gui.setup()
158-
gui.generate_screens()
158+
gui.create_screens()
159159

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

src/techui_builder/autofill.py

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import os
3+
from collections import defaultdict
34
from dataclasses import dataclass, field
45
from pathlib import Path
56

@@ -8,6 +9,7 @@
89

910
from techui_builder.builder import Builder, _get_action_group
1011
from techui_builder.models import Component
12+
from techui_builder.utils import read_bob
1113

1214
logger_ = logging.getLogger(__name__)
1315

@@ -16,41 +18,32 @@
1618
class Autofiller:
1719
path: Path
1820
macros: list[str] = field(default_factory=lambda: ["prefix", "desc", "file"])
21+
widgets: dict[str, ObjectifiedElement] = field(
22+
default_factory=defaultdict, init=False, repr=False
23+
)
1924

2025
def read_bob(self) -> None:
21-
# Read the bob file
22-
self.tree = objectify.parse(self.path)
23-
24-
# Find the root tag (in this case: <display version="2.0.0">)
25-
self.root = self.tree.getroot()
26+
self.tree, self.widgets = read_bob(self.path)
2627

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

30-
# Loop over objects in the xml
31-
# i.e. every tag below <display version="2.0.0">
32-
# but not any nested tags below them
33-
for child in self.root.iterchildren():
34-
# If widget is a symbol (i.e. a component)
35-
if child.tag == "widget" and child.get("type", default=None) == "symbol":
36-
# Extract it's name
37-
symbol_name = child.name
38-
39-
# If the name exists in the component list
40-
if symbol_name in gui.conf.components.keys():
41-
# Get first copy of component (should only be one)
42-
comp = next(
43-
(comp for comp in gui.conf.components if comp == symbol_name),
44-
)
45-
46-
self.replace_content(
47-
widget=child,
48-
component_name=comp,
49-
component=gui.conf.components[comp],
50-
)
51-
52-
# Add option to allow left mouse click to run action
53-
child["run_actions_on_mouse_click"] = "true"
31+
for symbol_name, child in self.widgets.items():
32+
# If the name exists in the component list
33+
if symbol_name in gui.conf.components.keys():
34+
# Get first copy of component (should only be one)
35+
comp = next(
36+
(comp for comp in gui.conf.components if comp == symbol_name),
37+
)
38+
39+
self.replace_content(
40+
widget=child,
41+
component_name=comp,
42+
component=gui.conf.components[comp],
43+
)
44+
45+
# Add option to allow left mouse click to run action
46+
child["run_actions_on_mouse_click"] = "true"
5447

5548
def write_bob(self, filename: Path):
5649
# Check if data/ dir exists and if not, make it

src/techui_builder/builder.py

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import logging
3+
import os
34
from collections import defaultdict
45
from dataclasses import _MISSING_TYPE, dataclass, field
56
from pathlib import Path
@@ -11,6 +12,7 @@
1112

1213
from techui_builder.generate import Generator
1314
from techui_builder.models import Entity, TechUi
15+
from techui_builder.validator import Validator
1416

1517
logger_ = logging.getLogger(__name__)
1618

@@ -57,8 +59,34 @@ def setup(self):
5759
"""Run intial setup, e.g. extracting entries from service ioc.yaml."""
5860
self._extract_services()
5961
synoptic_dir = self._write_directory
62+
63+
self.clean_bobs()
64+
6065
self.generator = Generator(synoptic_dir)
6166

67+
def clean_bobs(self):
68+
exclude = {"index.bob"}
69+
bobs = [
70+
bob
71+
for bob in self._write_directory.glob("*.bob")
72+
if bob.name not in exclude
73+
]
74+
75+
self.validator = Validator(bobs)
76+
self.validator.check_bobs()
77+
78+
# Get bobs that are only present in the bobs list (i.e. generated)
79+
self.generated_bobs = list(set(bobs) ^ set(self.validator.validate.values()))
80+
81+
logger_.info("Preserving edited screens for validation.")
82+
logger_.debug(f"Screens to validate: {list(self.validator.validate.keys())}")
83+
84+
logger_.info("Cleaning synoptic/ of generated screens.")
85+
# Remove any generated bobs that exist
86+
for bob in self.generated_bobs:
87+
logger_.debug(f"Removing generated screen: {bob.name}")
88+
os.remove(bob)
89+
6290
def _extract_services(self):
6391
"""
6492
Finds the services folders in the services directory
@@ -95,13 +123,20 @@ def _extract_entities(self, ioc_yaml: Path):
95123
)
96124
self.entities[new_entity.P].append(new_entity)
97125

98-
def _generate_screen(self, screen_name: str, screen_components: list[Entity]):
99-
self.generator.load_screen(screen_name, screen_components)
100-
self.generator.build_groups()
101-
self.generator.write_screen(self._write_directory)
126+
def _generate_screen(self, screen_name: str):
127+
self.generator.build_screen(screen_name)
128+
self.generator.write_screen(screen_name, self._write_directory)
129+
130+
def _validate_screen(self, screen_name: str):
131+
# Get the generated widgets to validate against
132+
widgets = self.generator.widgets
133+
widget_group = self.generator.group
134+
assert widget_group is not None
135+
widget_group_name = widget_group.get_element_value("name")
136+
self.validator.validate_bob(screen_name, widget_group_name, widgets)
102137

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

127-
self._generate_screen(component_name, screen_entities)
162+
# This is used by both generate and validate,
163+
# so called beforehand for tidyness
164+
self.generator.build_widgets(component_name, screen_entities)
165+
self.generator.build_groups(component_name)
166+
167+
screens_to_validate = list(self.validator.validate.keys())
168+
169+
if component_name in screens_to_validate:
170+
self._validate_screen(component_name)
171+
else:
172+
self._generate_screen(component_name)
173+
128174
else:
129175
logger_.warning(
130176
f"{self.techui.name}: The prefix [bold]{component.prefix}[/bold]\

src/techui_builder/generate.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@
2020
class Generator:
2121
synoptic_dir: Path = field(repr=False)
2222

23-
screen_name: str = field(init=False)
24-
screen_components: list[Entity] = field(init=False)
25-
2623
# These are global params for the class (not accessible by user)
2724
support_path: Path = field(init=False, repr=False)
2825
techui_support: dict = field(init=False, repr=False)
@@ -33,6 +30,7 @@ class Generator:
3330
widgets: list[ActionButton | EmbeddedDisplay] = field(
3431
default_factory=list[ActionButton | EmbeddedDisplay], init=False, repr=False
3532
)
33+
group: Group | None = field(default=None, init=False, repr=False)
3634

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

58-
def load_screen(self, screen_name: str, screen_components: list[Entity]):
59-
self.screen_name = screen_name
60-
self.screen_components = screen_components
61-
6256
def _get_screen_dimensions(self, file: str) -> tuple[int, int]:
6357
"""
6458
Parses the bob files for information on the height
@@ -248,7 +242,7 @@ def _allocate_widget(
248242
return new_widget
249243

250244
def _create_widget(
251-
self, component: Entity
245+
self, name: str, component: Entity
252246
) -> EmbeddedDisplay | ActionButton | None | list[EmbeddedDisplay | ActionButton]:
253247
# if statement below is check if the suffix is
254248
# missing from the component description. If
@@ -261,7 +255,7 @@ def _create_widget(
261255
except KeyError:
262256
logger_.warning(
263257
f"No available widget for {component.type} in screen \
264-
{self.screen_name}. Skipping..."
258+
{name}. Skipping..."
265259
)
266260
return None
267261

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

338332
return sorted_widgets
339333

340-
def build_groups(self):
341-
"""
342-
Create a group to fill with widgets
343-
"""
344-
# Create screen
345-
self.screen_ = pscreen.Screen(self.screen_name)
334+
def build_widgets(self, screen_name: str, screen_components: list[Entity]):
346335
# Empty widget buffer
347336
self.widgets = []
348337

349-
# create widget and group objects
350-
351338
# order is an enumeration of the components, used to list them,
352339
# and serves as functionality in the math for formatting.
353-
for component in self.screen_components:
354-
new_widget = self._create_widget(component=component)
340+
for component in screen_components:
341+
new_widget = self._create_widget(name=screen_name, component=component)
355342
if new_widget is None:
356343
continue
357344
if isinstance(new_widget, list):
358345
self.widgets.extend(new_widget)
359346
continue
360347
self.widgets.append(new_widget)
361348

349+
def build_groups(self, screen_name: str):
350+
"""
351+
Create a group to fill with widgets
352+
"""
353+
362354
if self.widgets == []:
363355
# No widgets found, so just back out
364356
return
@@ -369,28 +361,43 @@ def build_groups(self):
369361
height, width = self._get_group_dimensions(self.widgets)
370362

371363
self.group = Group(
372-
self.screen_name,
364+
screen_name,
373365
0,
374366
0,
375367
width,
376368
height,
377369
)
378370

371+
# TODO: we shouldn't need this assert; fix
372+
assert self.group is not None
379373
self.group.version("2.0.0")
380374
self.group.add_widget(self.widgets)
375+
376+
def build_screen(self, screen_name):
377+
"""
378+
Build the screen with the widget groups.
379+
"""
380+
# Create screen
381+
self.screen_ = pscreen.Screen(screen_name)
382+
383+
# TODO: I don't like this
384+
if self.group is None:
385+
# No group found, so just back out
386+
return
387+
381388
self.screen_.add_widget(self.group)
382389

383-
def write_screen(self, directory: Path):
390+
def write_screen(self, screen_name: str, directory: Path):
384391
"""Write the screen to file"""
385392

386393
if self.widgets == []:
387394
logger_.warning(
388-
f"Could not write screen: {self.screen_name} \
395+
f"Could not write screen: {screen_name} \
389396
as no widgets were available"
390397
)
391398
return
392399

393400
if not directory.exists():
394401
os.mkdir(directory)
395-
self.screen_.write_screen(f"{directory}/{self.screen_name}.bob")
396-
logger_.info(f"{self.screen_name}.bob has been created successfully")
402+
self.screen_.write_screen(f"{directory}/{screen_name}.bob")
403+
logger_.info(f"{screen_name}.bob has been created successfully")

src/techui_builder/utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from lxml import objectify
2+
from lxml.objectify import ObjectifiedElement
3+
4+
5+
def read_bob(path):
6+
# Read the bob file
7+
tree = objectify.parse(path)
8+
9+
# Find the root tag (in this case: <display version="2.0.0">)
10+
root = tree.getroot()
11+
12+
widgets = get_widgets(root)
13+
14+
return tree, widgets
15+
16+
17+
def get_widgets(root: ObjectifiedElement):
18+
widgets: dict[str, ObjectifiedElement] = {}
19+
# Loop over objects in the xml
20+
# i.e. every tag below <display version="2.0.0">
21+
# but not any nested tags below them
22+
for child in root.iterchildren():
23+
# If widget is a symbol (i.e. a component)
24+
if child.tag == "widget" and child.get("type", default=None) in [
25+
"symbol",
26+
"group",
27+
]:
28+
name = child.name.text
29+
assert name is not None
30+
widgets[name] = child
31+
32+
return widgets

0 commit comments

Comments
 (0)