Skip to content

Commit 2499e6d

Browse files
committed
Add validation of hinted sub controllers
1 parent 55641b8 commit 2499e6d

File tree

2 files changed

+64
-19
lines changed

2 files changed

+64
-19
lines changed

src/fastcs/controllers/base_controller.py

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

tests/test_controllers.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def test_controller_vector_iter():
148148
assert sub_controllers[index] == child
149149

150150

151-
def test_controller_introspection_hint_validation():
151+
def test_attribute_hint_validation():
152152
class HintedController(Controller):
153153
read_write_int: AttrRW[int]
154154

@@ -162,15 +162,15 @@ 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

172172

173-
def test_controller_introspection_hint_validation_enum():
173+
def test_enum_attribute_hint_validation():
174174
class GoodEnum(enum.IntEnum):
175175
VAL = 0
176176

@@ -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_sub_controller_hint_validation():
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)