Skip to content

Commit

Permalink
Implement grouping of Attributes on generated UIs
Browse files Browse the repository at this point in the history
Attributes and Methods take an optional group to be placed in. If group is unset it will appear at the root of the UI.
SubControllers are displayed on a SubScreen of the parent Controller.
  • Loading branch information
GDYendell committed Jan 18, 2024
1 parent cbccbcb commit e8901f2
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 39 deletions.
21 changes: 16 additions & 5 deletions src/fastcs/attributes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from enum import Enum
from typing import Any, Generic, Protocol, runtime_checkable
from typing import Any, Generic, Optional, Protocol, runtime_checkable

from .datatypes import ATTRIBUTE_TYPES, AttrCallback, DataType, T

Expand Down Expand Up @@ -46,13 +46,17 @@ class Attribute(Generic[T]):
"""

def __init__(
self, datatype: DataType[T], access_mode: AttrMode, handler: Any = None
self,
datatype: DataType[T],
access_mode: AttrMode,
group: Optional[str] = None,
) -> None:
assert (
datatype.dtype in ATTRIBUTE_TYPES
), f"Attr type must be one of {ATTRIBUTE_TYPES}, received type {datatype.dtype}"
self._datatype: DataType[T] = datatype
self._access_mode: AttrMode = access_mode
self._group = group

Check warning on line 59 in src/fastcs/attributes.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/attributes.py#L59

Added line #L59 was not covered by tests

@property
def datatype(self) -> DataType[T]:
Expand All @@ -66,6 +70,10 @@ def dtype(self) -> type[T]:
def access_mode(self) -> AttrMode:
return self._access_mode

@property
def group(self) -> Optional[str]:
return self._group

Check warning on line 75 in src/fastcs/attributes.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/attributes.py#L75

Added line #L75 was not covered by tests


class AttrR(Attribute[T]):
"""A read-only `Attribute`."""
Expand All @@ -74,9 +82,10 @@ def __init__(
self,
datatype: DataType[T],
access_mode=AttrMode.READ,
group: Optional[str] = None,
handler: Updater | None = None,
) -> None:
super().__init__(datatype, access_mode, handler) # type: ignore
super().__init__(datatype, access_mode, group) # type: ignore

Check warning on line 88 in src/fastcs/attributes.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/attributes.py#L88

Added line #L88 was not covered by tests
self._value: T = datatype.dtype()
self._update_callback: AttrCallback[T] | None = None
self._updater = handler
Expand Down Expand Up @@ -105,9 +114,10 @@ def __init__(
self,
datatype: DataType[T],
access_mode=AttrMode.WRITE,
group: Optional[str] = None,
handler: Sender | None = None,
) -> None:
super().__init__(datatype, access_mode, handler) # type: ignore
super().__init__(datatype, access_mode, group, handler) # type: ignore

Check warning on line 120 in src/fastcs/attributes.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/attributes.py#L120

Added line #L120 was not covered by tests
self._process_callback: AttrCallback[T] | None = None
self._write_display_callback: AttrCallback[T] | None = None
self._sender = handler
Expand Down Expand Up @@ -143,9 +153,10 @@ def __init__(
self,
datatype: DataType[T],
access_mode=AttrMode.READ_WRITE,
group: Optional[str] = None,
handler: Handler | None = None,
) -> None:
super().__init__(datatype, access_mode, handler) # type: ignore
super().__init__(datatype, access_mode, group, handler) # type: ignore

Check warning on line 159 in src/fastcs/attributes.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/attributes.py#L159

Added line #L159 was not covered by tests

async def process(self, value: T) -> None:
await self.set(value)
Expand Down
80 changes: 55 additions & 25 deletions src/fastcs/backends/epics/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from enum import Enum
from pathlib import Path

from pvi._format.base import Formatter
from pvi._yaml_utils import deserialize_yaml
from pvi._format.dls import DLSFormatter
from pvi.device import (
LED,
CheckBox,
Expand All @@ -16,6 +15,7 @@
SignalRW,
SignalW,
SignalX,
SubScreen,
TextFormat,
TextRead,
TextWrite,
Expand All @@ -24,11 +24,10 @@
)

from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW
from fastcs.cs_methods import Command
from fastcs.datatypes import Bool, DataType, Float, Int, String
from fastcs.exceptions import FastCSException
from fastcs.mapping import Mapping

FORMATTER_YAML = Path.cwd() / ".." / "pvi" / "formatters" / "dls.bob.pvi.formatter.yaml"
from fastcs.mapping import Mapping, SingleMapping


class EpicsGUIFormat(Enum):
Expand Down Expand Up @@ -113,29 +112,60 @@ def create_gui(self, options: EpicsGUIOptions | None = None) -> None:

assert options.output_path.suffix == options.file_format.value

formatter = deserialize_yaml(Formatter, FORMATTER_YAML)
formatter = DLSFormatter(label_width=150, widget_width=200)

Check warning on line 115 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L115

Added line #L115 was not covered by tests

components: Tree[Component] = []
for single_mapping in self._mapping.get_controller_mappings():
attr_path = single_mapping.controller.path

group_name = type(single_mapping.controller).__name__ + " " + attr_path
group_children: list[Component] = []

for attr_name, attribute in single_mapping.attributes.items():
group_children.append(
self._get_attribute_component(
attr_path,
attr_name,
attribute,
)
)
controller_mapping = self._mapping.get_controller_mappings()[0]
sub_controller_mappings = self._mapping.get_controller_mappings()[1:]

Check warning on line 118 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L117-L118

Added lines #L117 - L118 were not covered by tests

for name in single_mapping.command_methods:
group_children.append(self._get_command_component(attr_path, name))
components = self.extract_mapping_components(controller_mapping)

Check warning on line 120 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L120

Added line #L120 was not covered by tests

components.append(Group(group_name, Grid(), group_children))
for sub_controller_mapping in sub_controller_mappings:
components.append(

Check warning on line 123 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L122-L123

Added lines #L122 - L123 were not covered by tests
Group(
sub_controller_mapping.controller.path,
SubScreen(),
self.extract_mapping_components(sub_controller_mapping),
)
)

device = Device("Simple Device", children=components)

formatter.format(device, "MY-DEVICE-PREFIX", options.output_path)
formatter.format(device, "GARY", options.output_path)

Check warning on line 133 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L133

Added line #L133 was not covered by tests

def extract_mapping_components(self, mapping: SingleMapping) -> list[Component]:
components: Tree[Component] = []
attr_path = mapping.controller.path

Check warning on line 137 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L136-L137

Added lines #L136 - L137 were not covered by tests

groups: dict[str, list[Component]] = {}
for attr_name, attribute in mapping.attributes.items():
signal = self._get_attribute_component(

Check warning on line 141 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L139-L141

Added lines #L139 - L141 were not covered by tests
attr_path,
attr_name,
attribute,
)

match attribute:
case Attribute(group=group) if group is not None:
if group not in groups:
groups[group] = []

Check warning on line 150 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L147-L150

Added lines #L147 - L150 were not covered by tests

groups[group].append(signal)
case _:
components.append(signal)

Check warning on line 154 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L152-L154

Added lines #L152 - L154 were not covered by tests

for name, command in mapping.command_methods.items():
signal = self._get_command_component(attr_path, name)

Check warning on line 157 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L156-L157

Added lines #L156 - L157 were not covered by tests

match command:
case Command(group=group) if group is not None:
if group not in groups:
groups[group] = []

Check warning on line 162 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L159-L162

Added lines #L159 - L162 were not covered by tests

groups[group].append(signal)
case _:
components.append(signal)

Check warning on line 166 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L164-L166

Added lines #L164 - L166 were not covered by tests

for name, children in groups.items():
components.append(Group(name, Grid(), children))

Check warning on line 169 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L168-L169

Added lines #L168 - L169 were not covered by tests

return components

Check warning on line 171 in src/fastcs/backends/epics/gui.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/gui.py#L171

Added line #L171 was not covered by tests
2 changes: 1 addition & 1 deletion src/fastcs/backends/epics/ioc.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def run(self, options: EpicsIOCOptions | None = None) -> None:
backend = Backend(self._mapping, dispatcher.loop)

# Set the record prefix
builder.SetDeviceName("MY-DEVICE-PREFIX")
builder.SetDeviceName("GARY")

Check warning on line 127 in src/fastcs/backends/epics/ioc.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/backends/epics/ioc.py#L127

Added line #L127 was not covered by tests

_create_and_link_attribute_pvs(self._mapping)

Expand Down
13 changes: 9 additions & 4 deletions src/fastcs/cs_methods.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from asyncio import iscoroutinefunction
from inspect import Signature, getdoc, signature
from typing import Awaitable, Callable
from typing import Awaitable, Callable, Optional

from .exceptions import FastCSException

ScanCallback = Callable[..., Awaitable[None]]


class Method:
def __init__(self, fn: Callable) -> None:
def __init__(self, fn: Callable, *, group: Optional[str] = None) -> None:
self._docstring = getdoc(fn)

sig = signature(fn, eval_str=True)
Expand All @@ -17,6 +17,7 @@ def __init__(self, fn: Callable) -> None:
self._validate(fn)

self._fn = fn
self._group = group

Check warning on line 20 in src/fastcs/cs_methods.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/cs_methods.py#L20

Added line #L20 was not covered by tests

def _validate(self, fn: Callable) -> None:
if self.return_type not in (None, Signature.empty):
Expand All @@ -41,6 +42,10 @@ def docstring(self):
def fn(self):
return self._fn

@property
def group(self):
return self._group

Check warning on line 47 in src/fastcs/cs_methods.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/cs_methods.py#L47

Added line #L47 was not covered by tests


class Scan(Method):
def __init__(self, fn: Callable, period) -> None:
Expand Down Expand Up @@ -71,8 +76,8 @@ def _validate(self, fn: Callable) -> None:


class Command(Method):
def __init__(self, fn: Callable) -> None:
super().__init__(fn)
def __init__(self, fn: Callable, *, group: Optional[str] = None) -> None:
super().__init__(fn, group=group)

Check warning on line 80 in src/fastcs/cs_methods.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/cs_methods.py#L80

Added line #L80 was not covered by tests

def _validate(self, fn: Callable) -> None:
super()._validate(fn)
Expand Down
18 changes: 14 additions & 4 deletions src/fastcs/wrappers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Protocol, runtime_checkable
from typing import Any, Optional, Protocol, runtime_checkable

from .cs_methods import Command, Method, Put, Scan
from .exceptions import FastCSException
Expand Down Expand Up @@ -26,6 +26,16 @@ def put(fn) -> Any:
return fn


def command(fn) -> Any:
fn.fastcs_method = Command(fn)
return fn
def command(*, group: Optional[str] = None) -> Any:
"""Decorator to map a `Controller` method into a `Command`.
Args:
group: Group to display the widget for this command in on the UI
"""

def wrapper(fn):
fn.fastcs_method = Command(fn, group=group)
return fn

Check warning on line 39 in src/fastcs/wrappers.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/wrappers.py#L37-L39

Added lines #L37 - L39 were not covered by tests

return wrapper

Check warning on line 41 in src/fastcs/wrappers.py

View check run for this annotation

Codecov / codecov/patch

src/fastcs/wrappers.py#L41

Added line #L41 was not covered by tests

0 comments on commit e8901f2

Please sign in to comment.