Skip to content

Commit eb5d51e

Browse files
committed
Add validation of hinted sub controllers
1 parent 6261959 commit eb5d51e

File tree

2 files changed

+63
-13
lines changed

2 files changed

+63
-13
lines changed

src/fastcs/controllers/base_controller.py

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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}. "

tests/test_controllers.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,10 @@ class HintedController(Controller):
162162

163163
with pytest.raises(RuntimeError, match="failed to introspect hinted attribute"):
164164
controller.read_write_int = 5 # type: ignore
165-
controller._validate_hinted_attributes()
165+
controller._validate_type_hints()
166166

167167
with pytest.raises(RuntimeError, match="failed to introspect hinted attribute"):
168-
controller._validate_hinted_attributes()
168+
controller._validate_type_hints()
169169

170170
controller.add_attribute("read_write_int", AttrRW(Int()))
171171

@@ -186,3 +186,20 @@ class HintedController(Controller):
186186
controller.add_attribute("enum", AttrRW(Enum(BadEnum)))
187187

188188
controller.add_attribute("enum", AttrRW(Enum(GoodEnum)))
189+
190+
191+
@pytest.mark.asyncio
192+
async def test_controller_introspection_hint_validation_controller():
193+
class HintedController(Controller):
194+
child: SomeSubController
195+
196+
controller = HintedController()
197+
198+
with pytest.raises(RuntimeError, match="failed to introspect hinted controller"):
199+
controller._validate_type_hints()
200+
201+
with pytest.raises(RuntimeError, match="does not match defined type"):
202+
controller.add_sub_controller("child", Controller())
203+
204+
controller.add_sub_controller("child", SomeSubController())
205+
controller._validate_type_hints()

0 commit comments

Comments
 (0)