55from copy import deepcopy
66from 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
1018from fastcs .tracer import Tracer
1119
20+ logger = bind_logger (logger_name = __name__ )
21+
1222
1323class BaseController (Tracer ):
1424 """Base class for controllers
@@ -44,29 +54,43 @@ 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+
58+ self .__hinted_attributes : dict [str , HintedAttribute ] = {}
59+ self .__hinted_sub_controllers : dict [str , type [BaseController ]] = {}
60+ self ._find_type_hints ()
4861
4962 self ._bind_attrs ()
5063
5164 ios = ios or []
5265 self ._attribute_ref_io_map = {io .ref_type : io for io in ios }
5366 self ._validate_io (ios )
5467
55- def _parse_attribute_type_hints (
56- self ,
57- ) -> dict [str , tuple [type [Attribute ], type [DataType ]]]:
58- hinted_attributes = {}
68+ def _find_type_hints (self ):
69+ """Find `Attribute` and `Controller` type hints for introspection validation"""
5970 for name , hint in get_type_hints (type (self )).items ():
60- if not isinstance (hint , _GenericAlias ): # e.g. AttrR[int]
61- continue
62-
63- origin = get_origin (hint )
64- if not isinstance (origin , type ) or not issubclass (origin , Attribute ):
65- continue
66-
67- hinted_attributes [name ] = (origin , get_args (hint )[0 ])
71+ if isinstance (hint , _GenericAlias ): # e.g. AttrR[int]
72+ args = get_args (hint )
73+ hint = get_origin (hint )
74+ else :
75+ args = None
76+
77+ if isinstance (hint , type ) and issubclass (hint , Attribute ):
78+ if args is None :
79+ dtype = None
80+ else :
81+ if len (args ) == 2 :
82+ dtype = args [0 ]
83+ else :
84+ raise TypeError (
85+ f"Invalid type hint for attribute { name } : { hint } "
86+ )
87+
88+ self .__hinted_attributes [name ] = HintedAttribute (
89+ attr_type = hint , dtype = dtype
90+ )
6891
69- return hinted_attributes
92+ elif isinstance (hint , type ) and issubclass (hint , BaseController ):
93+ self .__hinted_sub_controllers [name ] = hint
7094
7195 def _bind_attrs (self ) -> None :
7296 """Search for Attributes and Methods to bind them to this instance.
@@ -132,21 +156,51 @@ async def initialise(self):
132156
133157 def post_initialise (self ):
134158 """Hook to call after all attributes added, before serving the application"""
135- self ._validate_hinted_attributes ()
159+ self ._validate_type_hints ()
136160 self ._connect_attribute_ios ()
137161
138- def _validate_hinted_attributes (self ):
139- """Validate `` Attribute`` type-hints were introspected during initialisation """
162+ def _validate_type_hints (self ):
163+ """Validate all ` Attribute` and `Controller` 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 )
166+
167+ for name in self .__hinted_sub_controllers :
168+ self ._validate_hinted_controller (name )
147169
148170 for subcontroller in self .sub_controllers .values ():
149- subcontroller ._validate_hinted_attributes () # noqa: SLF001
171+ subcontroller ._validate_type_hints () # noqa: SLF001
172+
173+ def _validate_hinted_attribute (self , name : str ):
174+ """Check that an `Attribute` with the given name exists on the controller"""
175+ attr = getattr (self , name , None )
176+ if attr is None or not isinstance (attr , Attribute ):
177+ raise RuntimeError (
178+ f"Controller `{ self .__class__ .__name__ } ` failed to introspect "
179+ f"hinted attribute `{ name } ` during initialisation"
180+ )
181+ else :
182+ logger .debug (
183+ "Validated hinted attribute" ,
184+ name = name ,
185+ controller = self ,
186+ attribute = attr ,
187+ )
188+
189+ def _validate_hinted_controller (self , name : str ):
190+ """Check that a sub controller with the given name exists on the controller"""
191+ controller = getattr (self , name , None )
192+ if controller is None or not isinstance (controller , BaseController ):
193+ raise RuntimeError (
194+ f"Controller `{ self .__class__ .__name__ } ` failed to introspect "
195+ f"hinted controller `{ name } ` during initialisation"
196+ )
197+ else :
198+ logger .debug (
199+ "Validated hinted sub controller" ,
200+ name = name ,
201+ controller = self ,
202+ sub_controller = controller ,
203+ )
150204
151205 def _connect_attribute_ios (self ) -> None :
152206 """Connect ``Attribute`` callbacks to ``AttributeIO``s"""
@@ -191,18 +245,18 @@ def add_attribute(self, name, attr: Attribute):
191245 f"{ self .__attributes [name ]} "
192246 )
193247 elif name in self .__hinted_attributes :
194- attr_class , attr_dtype = self .__hinted_attributes [name ]
195- if not isinstance (attr , attr_class ):
248+ hint = self .__hinted_attributes [name ]
249+ if not isinstance (attr , hint . attr_type ):
196250 raise RuntimeError (
197251 f"Controller '{ self .__class__ .__name__ } ' introspection of "
198252 f"hinted attribute '{ name } ' does not match defined access mode. "
199- f"Expected '{ attr_class . __name__ } ', got '{ type (attr ).__name__ } '."
253+ f"Expected '{ hint . attr_type . __name__ } ' got '{ type (attr ).__name__ } '."
200254 )
201- if attr_dtype is not None and attr_dtype != attr .datatype .dtype :
255+ if hint . dtype is not None and hint . dtype != attr .datatype .dtype :
202256 raise RuntimeError (
203257 f"Controller '{ self .__class__ .__name__ } ' introspection of "
204258 f"hinted attribute '{ name } ' does not match defined datatype. "
205- f"Expected '{ attr_dtype .__name__ } ', "
259+ f"Expected '{ hint . dtype .__name__ } ', "
206260 f"got '{ attr .datatype .dtype .__name__ } '."
207261 )
208262 elif name in self .__sub_controllers .keys ():
@@ -228,6 +282,15 @@ def add_sub_controller(self, name: str, sub_controller: BaseController):
228282 f"Controller { self } has existing sub controller { name } : "
229283 f"{ self .__sub_controllers [name ]} "
230284 )
285+ elif name in self .__hinted_sub_controllers :
286+ hint = self .__hinted_sub_controllers [name ]
287+ if not isinstance (sub_controller , hint ):
288+ raise RuntimeError (
289+ f"Controller '{ self .__class__ .__name__ } ' introspection of "
290+ f"hinted sub controller '{ name } ' does not match defined type. "
291+ f"Expected '{ hint .__name__ } ' got "
292+ f"'{ sub_controller .__class__ .__name__ } '."
293+ )
231294 elif name in self .__attributes :
232295 raise ValueError (
233296 f"Cannot add sub controller { sub_controller } . "
0 commit comments