Skip to content

Commit

Permalink
Done the epics side, need to figure out tango
Browse files Browse the repository at this point in the history
  • Loading branch information
evalott100 committed Dec 9, 2024
1 parent 2153173 commit a4f16d9
Show file tree
Hide file tree
Showing 9 changed files with 395 additions and 327 deletions.
16 changes: 2 additions & 14 deletions src/fastcs/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,15 @@ def __init__(
access_mode: AttrMode,
group: str | None = None,
handler: Any = None,
allowed_values: list[T] | None = None,
description: str | None = None,
) -> None:
assert (
datatype.dtype in ATTRIBUTE_TYPES
assert issubclass(
datatype.dtype, 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
self.enabled = True
self._allowed_values: list[T] | None = allowed_values
self.description = description

# A callback to use when setting the datatype to a different value, for example
Expand All @@ -86,10 +84,6 @@ def access_mode(self) -> AttrMode:
def group(self) -> str | None:
return self._group

@property
def allowed_values(self) -> list[T] | None:
return self._allowed_values

def add_update_datatype_callback(
self, callback: Callable[[DataType[T]], None]
) -> None:
Expand All @@ -115,15 +109,13 @@ def __init__(
group: str | None = None,
handler: Updater | None = None,
initial_value: T | None = None,
allowed_values: list[T] | None = None,
description: str | None = None,
) -> None:
super().__init__(
datatype, # type: ignore
access_mode,
group,
handler,
allowed_values=allowed_values, # type: ignore
description=description,
)
self._value: T = (
Expand Down Expand Up @@ -158,15 +150,13 @@ def __init__(
access_mode=AttrMode.WRITE,
group: str | None = None,
handler: Sender | None = None,
allowed_values: list[T] | None = None,
description: str | None = None,
) -> None:
super().__init__(
datatype, # type: ignore
access_mode,
group,
handler,
allowed_values=allowed_values, # type: ignore
description=description,
)
self._process_callback: AttrCallback[T] | None = None
Expand Down Expand Up @@ -209,7 +199,6 @@ def __init__(
group: str | None = None,
handler: Handler | None = None,
initial_value: T | None = None,
allowed_values: list[T] | None = None,
description: str | None = None,
) -> None:
super().__init__(
Expand All @@ -218,7 +207,6 @@ def __init__(
group=group,
handler=handler,
initial_value=initial_value,
allowed_values=allowed_values, # type: ignore
description=description,
)

Expand Down
69 changes: 65 additions & 4 deletions src/fastcs/datatypes.py
Original file line number Diff line number Diff line change
@@ -1,36 +1,56 @@
from __future__ import annotations

import enum
from abc import abstractmethod
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from dataclasses import MISSING, dataclass, field
from functools import cached_property
from typing import Generic, TypeVar

T_Numerical = TypeVar("T_Numerical", int, float)
T = TypeVar("T", int, float, bool, str)
T = TypeVar("T", int, float, bool, str, enum.Enum)

ATTRIBUTE_TYPES: tuple[type] = T.__constraints__ # type: ignore


AttrCallback = Callable[[T], Awaitable[None]]


@dataclass(frozen=True) # So that we can type hint with dataclass methods
@dataclass(frozen=True)
class DataType(Generic[T]):
"""Generic datatype mapping to a python type, with additional metadata."""

# We move this to each datatype so that we can have positional
# args in subclasses.
allowed_values: list[T] | None = field(init=False, default=None)

@property
@abstractmethod
def dtype(self) -> type[T]: # Using property due to lack of Generic ClassVars
pass

def validate(self, value: T) -> T:
"""Validate a value against fields in the datatype."""
if not isinstance(value, self.dtype):
raise ValueError(f"Value {value} is not of type {self.dtype}")
if (
hasattr(self, "allowed_values")
and self.allowed_values is not None
and value not in self.allowed_values
):
raise ValueError(
f"Value {value} is not in the allowed values for this "
f"datatype {self.allowed_values}."
)
return value

@property
def initial_value(self) -> T:
return self.dtype()


T_Numerical = TypeVar("T_Numerical", int, float)


@dataclass(frozen=True)
class _Numerical(DataType[T_Numerical]):
units: str | None = None
Expand All @@ -40,6 +60,7 @@ class _Numerical(DataType[T_Numerical]):
max_alarm: int | None = None

def validate(self, value: T_Numerical) -> T_Numerical:
super().validate(value)
if self.min is not None and value < self.min:
raise ValueError(f"Value {value} is less than minimum {self.min}")
if self.max is not None and value > self.max:
Expand All @@ -51,6 +72,8 @@ def validate(self, value: T_Numerical) -> T_Numerical:
class Int(_Numerical[int]):
"""`DataType` mapping to builtin ``int``."""

allowed_values: list[int] | None = None

@property
def dtype(self) -> type[int]:
return int
Expand All @@ -61,6 +84,7 @@ class Float(_Numerical[float]):
"""`DataType` mapping to builtin ``float``."""

prec: int = 2
allowed_values: list[float] | None = None

@property
def dtype(self) -> type[float]:
Expand All @@ -73,6 +97,7 @@ class Bool(DataType[bool]):

znam: str = "OFF"
onam: str = "ON"
allowed_values: list[bool] | None = None

@property
def dtype(self) -> type[bool]:
Expand All @@ -83,6 +108,42 @@ def dtype(self) -> type[bool]:
class String(DataType[str]):
"""`DataType` mapping to builtin ``str``."""

allowed_values: list[str] | None = None

@property
def dtype(self) -> type[str]:
return str


T_Enum = TypeVar("T_Enum", bound=enum.Enum)


@dataclass(frozen=True)
class Enum(DataType[enum.Enum]):
enum_cls: type[enum.Enum]

@cached_property
def is_string_enum(self) -> bool:
return all(isinstance(member.value, str) for member in self.members)

@cached_property
def is_int_enum(self) -> bool:
return all(isinstance(member.value, int) for member in self.members)

def __post_init__(self):
if not issubclass(self.enum_cls, enum.Enum):
raise ValueError("Enum class has to take an enum.")
if not (self.is_string_enum or self.is_int_enum):
raise ValueError("All enum values must be of type str or int.")

@cached_property
def members(self) -> list[enum.Enum]:
return list(self.enum_cls)

@property
def dtype(self) -> type[enum.Enum]:
return self.enum_cls

@property
def initial_value(self) -> enum.Enum:
return self.members[0]
24 changes: 17 additions & 7 deletions src/fastcs/transport/epics/gui.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import enum

from pvi._format.dls import DLSFormatter
from pvi.device import (
LED,
Expand Down Expand Up @@ -25,7 +27,7 @@
from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW
from fastcs.controller import Controller, SingleMapping, _get_single_mapping
from fastcs.cs_methods import Command
from fastcs.datatypes import Bool, Float, Int, String
from fastcs.datatypes import Bool, Enum, Float, Int, String
from fastcs.exceptions import FastCSException
from fastcs.util import snake_to_pascal

Expand All @@ -50,24 +52,32 @@ def _get_read_widget(attribute: AttrR) -> ReadWidgetUnion:
return TextRead()
case String():
return TextRead(format=TextFormat.string)
case Enum():
return TextRead(format=TextFormat.string)
case datatype:
raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}")

@staticmethod
def _get_write_widget(attribute: AttrW) -> WriteWidgetUnion:
match attribute.allowed_values:
case allowed_values if allowed_values is not None:
return ComboBox(choices=allowed_values)
case _:
pass

match attribute.datatype:
case Bool():
return ToggleButton()
case Int() | Float():
return TextWrite()
case String():
return TextWrite(format=TextFormat.string)
case Enum(enum_cls=enum_cls):
match enum_cls:
case enum_cls if issubclass(enum_cls, enum.Enum):
return ComboBox(
choices=[
member.name for member in attribute.datatype.members
]
)
case _:
raise FastCSException(
f"Unsupported Enum type {type(enum_cls)}: {enum_cls}"
)
case datatype:
raise FastCSException(f"Unsupported type {type(datatype)}: {datatype}")

Expand Down
Loading

0 comments on commit a4f16d9

Please sign in to comment.