Skip to content

Commit

Permalink
Refactor _format module
Browse files Browse the repository at this point in the history
- Rename classes and methods
  - Use Factory for classes that have methods to create classes
  - Use Formatter for classes that have a format method that produces
    a text representation of a widget for a UI file. This distinguishes
    from Device Widgets that determine the widget that will be created
    for a Signal.
  - Class renames:
    - WidgetTemplate -> UITemplate
    - WidgetFactory -> WidgetFormatter
    - GroupFactory -> GroupWidgetFormatter
    - LayoutProperties -> ScreenLayout
    - ScreenWidgets -> ScreenWidgetFormatter
    - Screen -> ScrenFormatterFactory
  - Screen method renames:
    - screen -> create_screen_formatter
    - create_sub_screens -> create_sub_screen_formatters
    - component -> generate_component_formatters
    - make_group_widget -> create_group_formatters
    - group -> create_group_formatter
    - make_component_widgets -> create_component_widget_formatters
- Make passing of ui properties to formatter more explicit and pass
  widget to formatter as a parameter separate to the properties map
- Expand one character variables
- mypy fixes
  • Loading branch information
GDYendell committed Jul 6, 2023
1 parent 1c756ef commit bdc818f
Show file tree
Hide file tree
Showing 8 changed files with 760 additions and 522 deletions.
23 changes: 15 additions & 8 deletions src/pvi/_format/adl.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
from __future__ import annotations

import re
from typing import List
from typing import List, Optional

from pvi._format.utils import Bounds, split_with_sep
from pvi._format.widget import WidgetFactory, WidgetTemplate
from pvi._format.widget import UITemplate, WidgetFormatter
from pvi.device import WidgetType


class AdlTemplate(WidgetTemplate[str]):
class AdlTemplate(UITemplate[str]):
def __init__(self, text: str):
assert "children {" not in text, "Can't do groups"
widgets = split_with_sep(text, "\n}\n")
self.screen = "".join(widgets[:3])
self.widgets = widgets[3:]

def set(self, t: str, bounds: Bounds = None, **properties) -> str:
def set(
self,
template: str,
bounds: Optional[Bounds] = None,
widget: Optional[WidgetType] = None,
**properties,
) -> str:
if bounds:
properties["x"] = bounds.x
properties["y"] = bounds.y
properties["width"] = bounds.w
properties["height"] = bounds.h
for item, value in properties.items():
if t.startswith('"related display"'):
if template.startswith('"related display"'):
value = f"{value}.adl" # Must include file extension
# Only need single line
pattern = re.compile(r"^(\s*%s)=.*$" % item, re.MULTILINE)
if isinstance(value, str):
value = f'"{value}"'
t, n = pattern.subn(r"\g<1>=" + str(value), t)
template, n = pattern.subn(r"\g<1>=" + str(value), template)
assert n == 1, f"No replacements made for {item}"
return t
return template

def search(self, search: str) -> str:
matches = [t for t in self.widgets if re.search(search, t)]
Expand All @@ -39,7 +46,7 @@ def search(self, search: str) -> str:
def create_group(
self,
group_object: List[str],
children: List[WidgetFactory[str]],
children: List[WidgetFormatter[str]],
padding: Bounds = Bounds(),
) -> List[str]:

Expand Down
147 changes: 93 additions & 54 deletions src/pvi/_format/aps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
from typing_extensions import Annotated

from pvi._format.adl import AdlTemplate
from pvi._format.screen import LayoutProperties, Screen, ScreenWidgets
from pvi._format.screen import (
ScreenFormatterFactory,
ScreenLayout,
ScreenWidgetFormatter,
)
from pvi._format.widget import (
ActionFactory,
GroupFactory,
LabelFactory,
PVWidgetFactory,
SubScreenFactory,
WidgetFactory,
ActionWidgetFormatter,
GroupFormatter,
LabelWidgetFormatter,
PVWidgetFormatter,
SubScreenWidgetFormatter,
WidgetFormatter,
)
from pvi._schema_utils import desc
from pvi.device import Device
Expand All @@ -33,7 +37,7 @@ class APSFormatter(Formatter):
def format(self, device: Device, prefix: str, path: Path):
assert path.suffix == ".adl", "Can only write adl files"
template = AdlTemplate((Path(__file__).parent / "aps.adl").read_text())
layout = LayoutProperties(
layout = ScreenLayout(
spacing=self.spacing,
title_height=self.title_height,
max_height=self.max_height,
Expand All @@ -44,97 +48,132 @@ def format(self, device: Device, prefix: str, path: Path):
group_widget_indent=0,
group_width_offset=0,
)
screen_widgets = ScreenWidgets(
heading_cls=LabelFactory.from_template(
template, search='"Heading"', textix="text"
widget_factory = ScreenWidgetFormatter(
heading_formatter=LabelWidgetFormatter.from_template(
template,
search='"Heading"',
property_map=dict(textix="text"),
),
label_cls=LabelFactory.from_template(
template, search='"Label"', textix="text"
label_formatter=LabelWidgetFormatter.from_template(
template,
search='"Label"',
property_map=dict(textix="text"),
),
led_cls=PVWidgetFactory.from_template(
template, search='"LED"', sized=Bounds.square, chan="pv"
led_formatter=PVWidgetFormatter.from_template(
template,
search='"LED"',
sized=Bounds.square,
property_map=dict(chan="pv"),
),
progress_bar_cls=PVWidgetFactory.from_template(
template, search='"ProgressBar"', chan="pv"
progress_bar_formatter=PVWidgetFormatter.from_template(
template,
search='"ProgressBar"',
property_map=dict(chan="pv"),
),
text_read_cls=PVWidgetFactory.from_template(
template, search='"TextRead"', chan="pv"
text_read_formatter=PVWidgetFormatter.from_template(
template,
search='"TextRead"',
property_map=dict(chan="pv"),
),
check_box_cls=PVWidgetFactory.from_template(
template, search='"CheckBox"', chan="pv"
check_box_formatter=PVWidgetFormatter.from_template(
template,
search='"CheckBox"',
property_map=dict(chan="pv"),
),
combo_box_cls=PVWidgetFactory.from_template(
template, search='"ComboBox"', chan="pv"
combo_box_formatter=PVWidgetFormatter.from_template(
template,
search='"ComboBox"',
property_map=dict(chan="pv"),
),
text_write_cls=PVWidgetFactory.from_template(
template, search='"TextWrite"', chan="pv"
text_write_formatter=PVWidgetFormatter.from_template(
template,
search='"TextWrite"',
property_map=dict(chan="pv"),
),
# Cannot handle dynamic tables so insert a label with the PV name
table_cls=PVWidgetFactory.from_template(
template, search='"Label"', textix="pv"
table_formatter=PVWidgetFormatter.from_template(
template,
search='"Label"',
property_map=dict(textix="pv"),
),
action_button_cls=ActionFactory.from_template(
template, search='"SignalX"', label="label", chan="pv"
action_button_formatter=ActionWidgetFormatter.from_template(
template,
search='"SignalX"',
property_map=dict(label="label", chan="pv"),
),
sub_screen_cls=SubScreenFactory.from_template(
template, search='"SubScreenFile"', name="file_name"
sub_screen_formatter=SubScreenWidgetFormatter.from_template(
template,
search='"SubScreenFile"',
property_map=dict(name="file_name"),
),
)

label_background_cls = group_box_cls = WidgetFactory.from_template(
label_background_formatter = WidgetFormatter.from_template(
template, search="clr=2"
)
screen_title_cls = LabelFactory.from_template(
template, search='"Title"', textix="text"
screen_title_formatter = LabelWidgetFormatter.from_template(
template,
search='"Title"',
property_map=dict(textix="text"),
)
group_title_formatter = LabelWidgetFormatter.from_template(
template,
search='"Group"',
property_map=dict(textix="text"),
)
group_title_cls = LabelFactory.from_template(
template, search='"Group"', textix="text"
group_box_formatter = WidgetFormatter.from_template(
template, search='fill="outline"'
)
group_box_cls = WidgetFactory.from_template(template, search='fill="outline"')

def make_group_widgets(bounds: Bounds, title: str) -> List[WidgetFactory[str]]:
def create_group_widget_formatters(
bounds: Bounds, title: str
) -> List[WidgetFormatter[str]]:
title_bounds = Bounds(
bounds.x + layout.spacing,
bounds.y + layout.spacing,
bounds.w - 2 * layout.spacing,
layout.group_label_height - layout.spacing,
)
return [
group_box_cls(bounds),
label_background_cls(title_bounds),
group_title_cls(title_bounds, title),
group_box_formatter(bounds),
label_background_formatter(title_bounds),
group_title_formatter(title_bounds, title),
]

def make_screen_widgets(bounds: Bounds, title: str) -> List[WidgetFactory[str]]:
def create_screen_widget_formatters(
bounds: Bounds, title: str
) -> List[WidgetFormatter[str]]:
title_bounds = Bounds(0, 0, bounds.w, layout.title_height)
return [
label_background_cls(title_bounds),
screen_title_cls(title_bounds, title),
label_background_formatter(title_bounds),
screen_title_formatter(title_bounds, title),
]

screen = Screen(
screen_cls=GroupFactory.from_template(
screen_factory = ScreenFormatterFactory(
screen_formatter_cls=GroupFormatter.from_template(
template,
search=GroupType.SCREEN,
sized=with_title(layout.spacing, layout.title_height),
make_widgets=make_screen_widgets,
widget_formatter_hook=create_screen_widget_formatters,
),
group_cls=GroupFactory.from_template(
group_formatter_cls=GroupFormatter.from_template(
template,
search=GroupType.GROUP,
sized=with_title(layout.spacing, layout.group_label_height),
make_widgets=make_group_widgets,
widget_formatter_hook=create_group_widget_formatters,
),
screen_widgets=screen_widgets,
widget_formatter=widget_factory,
prefix=prefix,
layout=layout,
base_file_name=path.stem,
)
title = f"{device.label} - {prefix}"

main_screen, sub_screens = screen.screen(device.children, title)
main_screen_formatter, sub_screens = screen_factory.create_screen_formatter(
device.children, title
)

path.write_text("".join(main_screen.format()))
for screen_name, screen_text in sub_screens:
path.write_text("".join(main_screen_formatter.format()))
for screen_name, screen_formatter in sub_screens:
screen_path = Path(path.parent / f"{screen_name}{path.suffix}")
screen_path.write_text("".join(screen_text.format()))
screen_path.write_text("".join(screen_formatter.format()))
53 changes: 25 additions & 28 deletions src/pvi/_format/bob.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
from __future__ import annotations

from copy import deepcopy
from typing import List, Sequence, Union
from typing import List, Optional, Sequence

from lxml.etree import ElementBase, SubElement, XMLParser, parse

from pvi._format.utils import Bounds
from pvi._format.widget import WidgetFactory, WidgetTemplate
from pvi._format.widget import UITemplate, WidgetFormatter
from pvi.device import (
LED,
ComboBox,
Group,
ReadWidget,
Row,
TableRead,
TableWidgetType,
TableWidgetTypes,
TableWrite,
WidgetType,
WriteWidget,
)


class BobTemplate(WidgetTemplate[ElementBase]):
class BobTemplate(UITemplate[ElementBase]):
"""Extract and modify elements from a template .bob file."""

def __init__(self, text: str):
Expand All @@ -29,34 +30,25 @@ def __init__(self, text: str):
self.tree = parse(text, parser=XMLParser(remove_blank_text=True))
self.screen = self.search("Display")

def set(self, t: ElementBase, bounds: Bounds = None, **properties) -> ElementBase:
"""Modify template elements (widgets) with component data.
Args:
t: A template element.
bounds: The size and position of the widget.
**properties: The element properties (SubElements) to update.
In the form: {[SubElement]: [Value]}
Returns:
The modified element.
"""
def set(
self,
template: ElementBase,
bounds: Optional[Bounds] = None,
widget: Optional[WidgetType] = None,
**properties,
) -> ElementBase:
if bounds:
properties["x"] = bounds.x
properties["y"] = bounds.y
properties["width"] = bounds.w
properties["height"] = bounds.h

t_copy = deepcopy(t)
for item, value in properties.items():
widget_type = t.attrib.get("type", "")
widget_type = template.attrib.get("type", "")

t_copy = deepcopy(template)
for item, value in properties.items():
new_text = ""
if widget_type == "table" and item == "widget":
add_table_columns(t_copy, value)
elif widget_type == "combo" and item == "widget":
add_combo_box_items(t_copy, value)
elif widget_type == "table" and item == "pv_name":
if widget_type == "table" and item == "pv_name":
new_text = f"pva://{value}" # Must include pva prefix
elif item == "file":
new_text = f"{value}.bob" # Must include file extension
Expand All @@ -66,6 +58,11 @@ def set(self, t: ElementBase, bounds: Bounds = None, **properties) -> ElementBas
if new_text:
replace_text(t_copy, item, new_text)

if widget_type == "table" and isinstance(widget, TableWidgetTypes):
add_table_columns(t_copy, widget)
elif widget_type == "combo" and isinstance(widget, ComboBox):
add_combo_box_items(t_copy, widget)

return t_copy

def search(self, search: str) -> ElementBase:
Expand Down Expand Up @@ -99,7 +96,7 @@ def search(self, search: str) -> ElementBase:
def create_group(
self,
group_object: List[ElementBase],
children: List[WidgetFactory[ElementBase]],
children: List[WidgetFormatter[ElementBase]],
padding: Bounds = Bounds(),
) -> List[ElementBase]:
"""Create an xml group object from a list of child widgets
Expand Down Expand Up @@ -128,7 +125,7 @@ def is_table(component: Group) -> bool:
)


def add_table_columns(widget_element: ElementBase, table: Union[TableRead, TableWrite]):
def add_table_columns(widget_element: ElementBase, table: TableWidgetType):
if not table.widgets:
# Default empty -> get options from pv
return
Expand All @@ -143,7 +140,7 @@ def add_table_columns(widget_element: ElementBase, table: Union[TableRead, Table
def add_table_column(
columns_element: ElementBase,
name: str,
widget: Union[ReadWidget, WriteWidget],
widget: WidgetType,
):
options: Sequence[str] = []
if isinstance(widget, LED):
Expand Down
Loading

0 comments on commit bdc818f

Please sign in to comment.