@@ -54,22 +54,24 @@ def __init__(
5454 # Internal state that should not be accessed directly by base classes
5555 self .__attributes : dict [str , Attribute ] = {}
5656 self .__sub_controllers : dict [str , BaseController ] = {}
57- self .__hinted_attributes = self ._validate_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 ()
5861
5962 self ._bind_attrs ()
6063
6164 ios = ios or []
6265 self ._attribute_ref_io_map = {io .ref_type : io for io in ios }
6366 self ._validate_io (ios )
6467
65- def _validate_attribute_type_hints (self ) -> dict [ str , HintedAttribute ] :
66- """Validate `Attribute` type hints for introspection
68+ def _find_type_hints (self ):
69+ """Find `Attribute` and `Controller` type hints for introspection validation
6770
6871 Returns:
6972 A dictionary of `HintedAttribute` from type hints on this controller class
7073
7174 """
72- hinted_attributes = {}
7375 for name , hint in get_type_hints (type (self )).items ():
7476 if isinstance (hint , _GenericAlias ): # e.g. AttrR[int]
7577 args = get_args (hint )
@@ -82,15 +84,18 @@ def _validate_attribute_type_hints(self) -> dict[str, HintedAttribute]:
8284 dtype = None
8385 else :
8486 if len (args ) == 2 :
85- dtype = args [1 ]
87+ dtype = args [0 ]
8688 else :
8789 raise TypeError (
8890 f"Invalid type hint for attribute { name } : { hint } "
8991 )
9092
91- hinted_attributes [name ] = HintedAttribute (attr_type = hint , dtype = dtype )
93+ self .__hinted_attributes [name ] = HintedAttribute (
94+ attr_type = hint , dtype = dtype
95+ )
9296
93- return hinted_attributes
97+ elif isinstance (hint , type ) and issubclass (hint , BaseController ):
98+ self .__hinted_sub_controllers [name ] = hint
9499
95100 def _bind_attrs (self ) -> None :
96101 """Search for Attributes and Methods to bind them to this instance.
@@ -156,16 +161,19 @@ async def initialise(self):
156161
157162 def post_initialise (self ):
158163 """Hook to call after all attributes added, before serving the application"""
159- self ._validate_hinted_attributes ()
164+ self ._validate_type_hints ()
160165 self ._connect_attribute_ios ()
161166
162- def _validate_hinted_attributes (self ):
163- """Validate all `Attribute` type-hints were introspected"""
167+ def _validate_type_hints (self ):
168+ """Validate all `Attribute` and `Controller` type-hints were introspected"""
164169 for name in self .__hinted_attributes :
165170 self ._validate_hinted_attribute (name )
166171
172+ for name in self .__hinted_sub_controllers :
173+ self ._validate_hinted_controller (name )
174+
167175 for subcontroller in self .sub_controllers .values ():
168- subcontroller ._validate_hinted_attributes () # noqa: SLF001
176+ subcontroller ._validate_type_hints () # noqa: SLF001
169177
170178 def _validate_hinted_attribute (self , name : str ):
171179 """Check that an `Attribute` with the given name exists on the controller"""
@@ -183,6 +191,22 @@ def _validate_hinted_attribute(self, name: str):
183191 attribute = attr ,
184192 )
185193
194+ def _validate_hinted_controller (self , name : str ):
195+ """Check that a sub controller with the given name exists on the controller"""
196+ controller = getattr (self , name , None )
197+ if controller is None or not isinstance (controller , BaseController ):
198+ raise RuntimeError (
199+ f"Controller `{ self .__class__ .__name__ } ` failed to introspect "
200+ f"hinted controller `{ name } ` during initialisation"
201+ )
202+ else :
203+ logger .debug (
204+ "Validated hinted sub controller" ,
205+ name = name ,
206+ controller = self ,
207+ sub_controller = controller ,
208+ )
209+
186210 def _connect_attribute_ios (self ) -> None :
187211 """Connect ``Attribute`` callbacks to ``AttributeIO``s"""
188212 for attr in self .__attributes .values ():
@@ -263,6 +287,15 @@ def add_sub_controller(self, name: str, sub_controller: BaseController):
263287 f"Controller { self } has existing sub controller { name } : "
264288 f"{ self .__sub_controllers [name ]} "
265289 )
290+ elif name in self .__hinted_sub_controllers :
291+ hint = self .__hinted_sub_controllers [name ]
292+ if not isinstance (sub_controller , hint ):
293+ raise RuntimeError (
294+ f"Controller '{ self .__class__ .__name__ } ' introspection of "
295+ f"hinted sub controller '{ name } ' does not match defined type. "
296+ f"Expected '{ hint .__name__ } ' got "
297+ f"'{ sub_controller .__class__ .__name__ } '."
298+ )
266299 elif name in self .__attributes :
267300 raise ValueError (
268301 f"Cannot add sub controller { sub_controller } . "
0 commit comments