@@ -54,22 +54,19 @@ 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
67-
68- Returns:
69- A dictionary of `HintedAttribute` from type hints on this controller class
70-
71- """
72- hinted_attributes = {}
68+ def _find_type_hints (self ):
69+ """Find `Attribute` and `Controller` type hints for introspection validation"""
7370 for name , hint in get_type_hints (type (self )).items ():
7471 if isinstance (hint , _GenericAlias ): # e.g. AttrR[int]
7572 args = get_args (hint )
@@ -88,9 +85,12 @@ def _validate_attribute_type_hints(self) -> dict[str, HintedAttribute]:
8885 f"Invalid type hint for attribute { name } : { hint } "
8986 )
9087
91- hinted_attributes [name ] = HintedAttribute (attr_type = hint , dtype = dtype )
88+ self .__hinted_attributes [name ] = HintedAttribute (
89+ attr_type = hint , dtype = dtype
90+ )
9291
93- return hinted_attributes
92+ elif isinstance (hint , type ) and issubclass (hint , BaseController ):
93+ self .__hinted_sub_controllers [name ] = hint
9494
9595 def _bind_attrs (self ) -> None :
9696 """Search for Attributes and Methods to bind them to this instance.
@@ -156,16 +156,19 @@ async def initialise(self):
156156
157157 def post_initialise (self ):
158158 """Hook to call after all attributes added, before serving the application"""
159- self ._validate_hinted_attributes ()
159+ self ._validate_type_hints ()
160160 self ._connect_attribute_ios ()
161161
162- def _validate_hinted_attributes (self ):
163- """Validate all `Attribute` type-hints were introspected"""
162+ def _validate_type_hints (self ):
163+ """Validate all `Attribute` and `Controller` type-hints were introspected"""
164164 for name in self .__hinted_attributes :
165165 self ._validate_hinted_attribute (name )
166166
167+ for name in self .__hinted_sub_controllers :
168+ self ._validate_hinted_controller (name )
169+
167170 for subcontroller in self .sub_controllers .values ():
168- subcontroller ._validate_hinted_attributes () # noqa: SLF001
171+ subcontroller ._validate_type_hints () # noqa: SLF001
169172
170173 def _validate_hinted_attribute (self , name : str ):
171174 """Check that an `Attribute` with the given name exists on the controller"""
@@ -183,6 +186,22 @@ def _validate_hinted_attribute(self, name: str):
183186 attribute = attr ,
184187 )
185188
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+ )
204+
186205 def _connect_attribute_ios (self ) -> None :
187206 """Connect ``Attribute`` callbacks to ``AttributeIO``s"""
188207 for attr in self .__attributes .values ():
@@ -263,6 +282,15 @@ def add_sub_controller(self, name: str, sub_controller: BaseController):
263282 f"Controller { self } has existing sub controller { name } : "
264283 f"{ self .__sub_controllers [name ]} "
265284 )
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+ )
266294 elif name in self .__attributes :
267295 raise ValueError (
268296 f"Cannot add sub controller { sub_controller } . "
0 commit comments