Skip to content

Commit 55641b8

Browse files
committed
Improve Attribute validation
Add HintedAttribute dataclass Allow for validation of Attribute type hints without a dtype
1 parent 082158c commit 55641b8

File tree

3 files changed

+80
-26
lines changed

3 files changed

+80
-26
lines changed

src/fastcs/attributes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
from .attribute_io import AttributeIO as AttributeIO
77
from .attribute_io_ref import AttributeIORef as AttributeIORef
88
from .attribute_io_ref import AttributeIORefT as AttributeIORefT
9+
from .hinted_attribute import HintedAttribute as HintedAttribute
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from dataclasses import dataclass
2+
3+
from fastcs.attributes.attribute import Attribute
4+
from fastcs.datatypes import DType
5+
6+
7+
@dataclass(kw_only=True)
8+
class HintedAttribute:
9+
"""An `Attribute` type hint found on a `Controller` class
10+
11+
e.g. ``attr: AttrR[int]``
12+
13+
"""
14+
15+
attr_type: type[Attribute]
16+
"""The type of the `Attribute` in the type hint - e.g. `AttrR`"""
17+
dtype: type[DType] | None
18+
"""The dtype of the `Attribute` in the type hint, if any - e.g. `int`"""

src/fastcs/controllers/base_controller.py

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@
55
from copy import deepcopy
66
from typing import _GenericAlias, get_args, get_origin, get_type_hints # type: ignore
77

8-
from fastcs.attributes import Attribute, AttributeIO, AttributeIORefT, AttrR, AttrW
9-
from fastcs.datatypes import DataType, DType_T
8+
from fastcs.attributes import (
9+
Attribute,
10+
AttributeIO,
11+
AttributeIORefT,
12+
AttrR,
13+
AttrW,
14+
HintedAttribute,
15+
)
16+
from fastcs.datatypes import DType_T
17+
from fastcs.logging import bind_logger
1018
from fastcs.tracer import Tracer
1119

20+
logger = bind_logger(logger_name=__name__)
21+
1222

1323
class BaseController(Tracer):
1424
"""Base class for controllers
@@ -44,27 +54,41 @@ def __init__(
4454
# Internal state that should not be accessed directly by base classes
4555
self.__attributes: dict[str, Attribute] = {}
4656
self.__sub_controllers: dict[str, BaseController] = {}
47-
self.__hinted_attributes = self._parse_attribute_type_hints()
57+
self.__hinted_attributes = self._validate_attribute_type_hints()
4858

4959
self._bind_attrs()
5060

5161
ios = ios or []
5262
self._attribute_ref_io_map = {io.ref_type: io for io in ios}
5363
self._validate_io(ios)
5464

55-
def _parse_attribute_type_hints(
56-
self,
57-
) -> dict[str, tuple[type[Attribute], type[DataType]]]:
58-
hinted_attributes = {}
59-
for name, hint in get_type_hints(type(self)).items():
60-
if not isinstance(hint, _GenericAlias): # e.g. AttrR[int]
61-
continue
65+
def _validate_attribute_type_hints(self) -> dict[str, HintedAttribute]:
66+
"""Validate `Attribute` type hints for introspection
6267
63-
origin = get_origin(hint)
64-
if not isinstance(origin, type) or not issubclass(origin, Attribute):
65-
continue
68+
Returns:
69+
A dictionary of `HintedAttribute` from type hints on this controller class
6670
67-
hinted_attributes[name] = (origin, get_args(hint)[0])
71+
"""
72+
hinted_attributes = {}
73+
for name, hint in get_type_hints(type(self)).items():
74+
if isinstance(hint, _GenericAlias): # e.g. AttrR[int]
75+
args = get_args(hint)
76+
hint = get_origin(hint)
77+
else:
78+
args = None
79+
80+
if isinstance(hint, type) and issubclass(hint, Attribute):
81+
if args is None:
82+
dtype = None
83+
else:
84+
if len(args) == 2:
85+
dtype = args[0]
86+
else:
87+
raise TypeError(
88+
f"Invalid type hint for attribute {name}: {hint}"
89+
)
90+
91+
hinted_attributes[name] = HintedAttribute(attr_type=hint, dtype=dtype)
6892

6993
return hinted_attributes
7094

@@ -136,18 +160,29 @@ def post_initialise(self):
136160
self._connect_attribute_ios()
137161

138162
def _validate_hinted_attributes(self):
139-
"""Validate ``Attribute`` type-hints were introspected during initialisation"""
163+
"""Validate all `Attribute` type-hints were introspected"""
140164
for name in self.__hinted_attributes:
141-
attr = getattr(self, name, None)
142-
if attr is None or not isinstance(attr, Attribute):
143-
raise RuntimeError(
144-
f"Controller `{self.__class__.__name__}` failed to introspect "
145-
f"hinted attribute `{name}` during initialisation"
146-
)
165+
self._validate_hinted_attribute(name)
147166

148167
for subcontroller in self.sub_controllers.values():
149168
subcontroller._validate_hinted_attributes() # noqa: SLF001
150169

170+
def _validate_hinted_attribute(self, name: str):
171+
"""Check that an `Attribute` with the given name exists on the controller"""
172+
attr = getattr(self, name, None)
173+
if attr is None or not isinstance(attr, Attribute):
174+
raise RuntimeError(
175+
f"Controller `{self.__class__.__name__}` failed to introspect "
176+
f"hinted attribute `{name}` during initialisation"
177+
)
178+
else:
179+
logger.debug(
180+
"Validated hinted attribute",
181+
name=name,
182+
controller=self,
183+
attribute=attr,
184+
)
185+
151186
def _connect_attribute_ios(self) -> None:
152187
"""Connect ``Attribute`` callbacks to ``AttributeIO``s"""
153188
for attr in self.__attributes.values():
@@ -191,18 +226,18 @@ def add_attribute(self, name, attr: Attribute):
191226
f"{self.__attributes[name]}"
192227
)
193228
elif name in self.__hinted_attributes:
194-
attr_class, attr_dtype = self.__hinted_attributes[name]
195-
if not isinstance(attr, attr_class):
229+
hint = self.__hinted_attributes[name]
230+
if not isinstance(attr, hint.attr_type):
196231
raise RuntimeError(
197232
f"Controller '{self.__class__.__name__}' introspection of "
198233
f"hinted attribute '{name}' does not match defined access mode. "
199-
f"Expected '{attr_class.__name__}', got '{type(attr).__name__}'."
234+
f"Expected '{hint.attr_type.__name__}' got '{type(attr).__name__}'."
200235
)
201-
if attr_dtype is not None and attr_dtype != attr.datatype.dtype:
236+
if hint.dtype is not None and hint.dtype != attr.datatype.dtype:
202237
raise RuntimeError(
203238
f"Controller '{self.__class__.__name__}' introspection of "
204239
f"hinted attribute '{name}' does not match defined datatype. "
205-
f"Expected '{attr_dtype.__name__}', "
240+
f"Expected '{hint.dtype.__name__}', "
206241
f"got '{attr.datatype.dtype.__name__}'."
207242
)
208243
elif name in self.__sub_controllers.keys():

0 commit comments

Comments
 (0)