|
5 | 5 | from copy import deepcopy |
6 | 6 | from typing import _GenericAlias, get_args, get_origin, get_type_hints # type: ignore |
7 | 7 |
|
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 |
10 | 18 | from fastcs.tracer import Tracer |
11 | 19 |
|
| 20 | +logger = bind_logger(logger_name=__name__) |
| 21 | + |
12 | 22 |
|
13 | 23 | class BaseController(Tracer): |
14 | 24 | """Base class for controllers |
@@ -44,27 +54,41 @@ def __init__( |
44 | 54 | # Internal state that should not be accessed directly by base classes |
45 | 55 | self.__attributes: dict[str, Attribute] = {} |
46 | 56 | self.__sub_controllers: dict[str, BaseController] = {} |
47 | | - self.__hinted_attributes = self._parse_attribute_type_hints() |
| 57 | + self.__hinted_attributes = self._validate_attribute_type_hints() |
48 | 58 |
|
49 | 59 | self._bind_attrs() |
50 | 60 |
|
51 | 61 | ios = ios or [] |
52 | 62 | self._attribute_ref_io_map = {io.ref_type: io for io in ios} |
53 | 63 | self._validate_io(ios) |
54 | 64 |
|
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 |
62 | 67 |
|
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 |
66 | 70 |
|
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) |
68 | 92 |
|
69 | 93 | return hinted_attributes |
70 | 94 |
|
@@ -136,18 +160,29 @@ def post_initialise(self): |
136 | 160 | self._connect_attribute_ios() |
137 | 161 |
|
138 | 162 | def _validate_hinted_attributes(self): |
139 | | - """Validate ``Attribute`` type-hints were introspected during initialisation""" |
| 163 | + """Validate all `Attribute` type-hints were introspected""" |
140 | 164 | 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) |
147 | 166 |
|
148 | 167 | for subcontroller in self.sub_controllers.values(): |
149 | 168 | subcontroller._validate_hinted_attributes() # noqa: SLF001 |
150 | 169 |
|
| 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 | + |
151 | 186 | def _connect_attribute_ios(self) -> None: |
152 | 187 | """Connect ``Attribute`` callbacks to ``AttributeIO``s""" |
153 | 188 | for attr in self.__attributes.values(): |
@@ -191,18 +226,18 @@ def add_attribute(self, name, attr: Attribute): |
191 | 226 | f"{self.__attributes[name]}" |
192 | 227 | ) |
193 | 228 | 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): |
196 | 231 | raise RuntimeError( |
197 | 232 | f"Controller '{self.__class__.__name__}' introspection of " |
198 | 233 | 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__}'." |
200 | 235 | ) |
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: |
202 | 237 | raise RuntimeError( |
203 | 238 | f"Controller '{self.__class__.__name__}' introspection of " |
204 | 239 | f"hinted attribute '{name}' does not match defined datatype. " |
205 | | - f"Expected '{attr_dtype.__name__}', " |
| 240 | + f"Expected '{hint.dtype.__name__}', " |
206 | 241 | f"got '{attr.datatype.dtype.__name__}'." |
207 | 242 | ) |
208 | 243 | elif name in self.__sub_controllers.keys(): |
|
0 commit comments