Source code for fastcs.datatypes
from __future__ import annotations
import enum
from abc import abstractmethod
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from functools import cached_property
from typing import Any, Generic, TypeVar
import numpy as np
from numpy.typing import DTypeLike
T = TypeVar(
"T",
int, # Int
float, # Float
bool, # Bool
str, # String
enum.Enum, # Enum
np.ndarray, # Waveform
list[tuple[str, DTypeLike]], # Table
)
ATTRIBUTE_TYPES: tuple[type] = T.__constraints__ # type: ignore
AttrCallback = Callable[[T], Awaitable[None]]
[docs]
@dataclass(frozen=True)
class DataType(Generic[T]):
"""Generic datatype mapping to a python type, with additional metadata."""
@property
@abstractmethod
def dtype(self) -> type[T]: # Using property due to lack of Generic ClassVars
pass
[docs]
def validate(self, value: Any) -> T:
"""Validate a value against the datatype.
The base implementation is to try the cast and raise a useful error if it fails.
Child classes can implement logic before calling ``super.validate(value)`` to
modify the value passed in and help the cast succeed or after to perform further
validation of the coerced type.
"""
if isinstance(value, self.dtype):
return value
try:
return self.dtype(value)
except (ValueError, TypeError) as e:
raise ValueError(f"Failed to cast {value} to type {self.dtype}") from e
@property
@abstractmethod
def initial_value(self) -> T:
pass
T_Numerical = TypeVar("T_Numerical", int, float)
@dataclass(frozen=True)
class _Numerical(DataType[T_Numerical]):
units: str | None = None
min: T_Numerical | None = None
max: T_Numerical | None = None
min_alarm: T_Numerical | None = None
max_alarm: T_Numerical | 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:
raise ValueError(f"Value {value} is greater than maximum {self.max}")
return value
@property
def initial_value(self) -> T_Numerical:
return self.dtype(0)
[docs]
@dataclass(frozen=True)
class Int(_Numerical[int]):
"""`DataType` mapping to builtin ``int``."""
@property
def dtype(self) -> type[int]:
return int
[docs]
@dataclass(frozen=True)
class Float(_Numerical[float]):
"""`DataType` mapping to builtin ``float``."""
prec: int = 2
@property
def dtype(self) -> type[float]:
return float
def validate(self, value: float) -> float:
super().validate(value)
if self.prec is not None:
value = round(value, self.prec)
return value
[docs]
@dataclass(frozen=True)
class Bool(DataType[bool]):
"""`DataType` mapping to builtin ``bool``."""
@property
def dtype(self) -> type[bool]:
return bool
@property
def initial_value(self) -> bool:
return False
[docs]
@dataclass(frozen=True)
class String(DataType[str]):
"""`DataType` mapping to builtin ``str``."""
@property
def dtype(self) -> type[str]:
return str
@property
def initial_value(self) -> str:
return ""
T_Enum = TypeVar("T_Enum", bound=enum.Enum)
[docs]
@dataclass(frozen=True)
class Enum(Generic[T_Enum], DataType[T_Enum]):
enum_cls: type[T_Enum]
def __post_init__(self):
if not issubclass(self.enum_cls, enum.Enum):
raise ValueError("Enum class has to take an Enum.")
def index_of(self, value: T_Enum) -> int:
return self.members.index(value)
@cached_property
def members(self) -> list[T_Enum]:
return list(self.enum_cls)
@cached_property
def names(self) -> list[str]:
return [member.name for member in self.members]
@property
def dtype(self) -> type[T_Enum]:
return self.enum_cls
@property
def initial_value(self) -> T_Enum:
return self.members[0]
[docs]
@dataclass(frozen=True)
class Table(DataType[np.ndarray]):
# https://numpy.org/devdocs/user/basics.rec.html#structured-datatype-creation
structured_dtype: list[tuple[str, DTypeLike]]
@property
def dtype(self) -> type[np.ndarray]:
return np.ndarray
@property
def initial_value(self) -> np.ndarray:
return np.array([], dtype=self.structured_dtype)
def validate(self, value: np.ndarray) -> np.ndarray:
super().validate(value)
if self.structured_dtype != value.dtype:
raise ValueError(
f"Value dtype {value.dtype.descr} is not the same as the structured "
f"dtype {self.structured_dtype}"
)
return value