Skip to content

Commit 291768b

Browse files
authored
validate hinted attributes on subcontrollers (#209)
1 parent aecb4c3 commit 291768b

File tree

2 files changed

+75
-13
lines changed

2 files changed

+75
-13
lines changed

src/fastcs/util.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,20 @@ def validate_hinted_attributes(controller: BaseController):
3737
For each type-hinted attribute, validate that a corresponding instance exists in the
3838
controller with the correct access mode and datatype.
3939
"""
40-
41-
hints = get_type_hints(type(controller))
42-
alias_hints = {k: v for k, v in hints.items() if isinstance(v, _GenericAlias)}
43-
for name, hint in alias_hints.items():
44-
attr_class = get_origin(hint)
40+
for subcontroller in controller.sub_controllers.values():
41+
validate_hinted_attributes(subcontroller)
42+
hints = {
43+
k: v
44+
for k, v in get_type_hints(type(controller)).items()
45+
if isinstance(v, _GenericAlias | type)
46+
}
47+
for name, hint in hints.items():
48+
if isinstance(hint, type):
49+
attr_class = hint
50+
attr_dtype = None
51+
else:
52+
attr_class = get_origin(hint)
53+
attr_dtype = get_args(hint)[0]
4554
if not issubclass(attr_class, Attribute):
4655
continue
4756

@@ -51,16 +60,13 @@ def validate_hinted_attributes(controller: BaseController):
5160
f"Controller `{controller.__class__.__name__}` failed to introspect "
5261
f"hinted attribute `{name}` during initialisation"
5362
)
54-
55-
if type(attr) is not attr_class:
63+
if attr_class is not type(attr):
5664
raise RuntimeError(
57-
f"Controller '{controller.__class__.__name__}' introspection of hinted "
58-
f"attribute '{name}' does not match defined access mode. "
65+
f"Controller '{controller.__class__.__name__}' introspection of "
66+
f"hinted attribute '{name}' does not match defined access mode. "
5967
f"Expected '{attr_class.__name__}', got '{type(attr).__name__}'."
6068
)
61-
62-
attr_dtype = get_args(hint)[0]
63-
if attr.datatype.dtype != attr_dtype:
69+
if attr_dtype is not None and attr_dtype != attr.datatype.dtype:
6470
raise RuntimeError(
6571
f"Controller '{controller.__class__.__name__}' introspection of hinted "
6672
f"attribute '{name}' does not match defined datatype. "

tests/test_util.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import asyncio
12
import enum
23

34
import numpy as np
45
import pytest
56
from pvi.device import SignalR
67
from pydantic import ValidationError
78

8-
from fastcs.attributes import AttrR, AttrRW
9+
from fastcs.attributes import Attribute, AttrR, AttrRW
910
from fastcs.controller import Controller
1011
from fastcs.datatypes import Bool, Enum, Float, Int, String
12+
from fastcs.launch import FastCS
1113
from fastcs.util import (
1214
numpy_to_fastcs_datatype,
1315
snake_to_pascal,
@@ -136,3 +138,57 @@ class ControllerWrongEnumClass(Controller):
136138
"'hinted_enum' does not match defined datatype. "
137139
"Expected 'MyEnum', got 'MyEnum2'."
138140
)
141+
142+
143+
def test_hinted_attributes_verified_on_subcontrollers():
144+
loop = asyncio.get_event_loop()
145+
146+
class ControllerWithWrongType(Controller):
147+
hinted_missing: AttrR[int]
148+
149+
async def connect(self):
150+
return
151+
152+
class TopController(Controller):
153+
async def initialise(self): # why does this not get called?
154+
subcontroller = ControllerWithWrongType()
155+
self.add_sub_controller("MySubController", subcontroller)
156+
157+
fastcs = FastCS(TopController(), [], loop)
158+
with pytest.raises(RuntimeError, match="failed to introspect hinted attribute"):
159+
fastcs.run()
160+
161+
162+
def test_hinted_attribute_access_mode_verified():
163+
# test verification works with non-GenericAlias type hints
164+
loop = asyncio.get_event_loop()
165+
166+
class ControllerAttrWrongAccessMode(Controller):
167+
read_attr: AttrR
168+
169+
async def initialise(self):
170+
self.read_attr = AttrRW(Int())
171+
172+
fastcs = FastCS(ControllerAttrWrongAccessMode(), [], loop)
173+
with pytest.raises(RuntimeError, match="does not match defined access mode"):
174+
fastcs.run()
175+
176+
177+
@pytest.mark.asyncio
178+
async def test_hinted_attributes_with_unspecified_access_mode():
179+
class ControllerUnspecifiedAccessMode(Controller):
180+
unspecified_access_mode: Attribute
181+
182+
async def initialise(self):
183+
self.unspecified_access_mode = AttrRW(Int())
184+
185+
controller = ControllerUnspecifiedAccessMode()
186+
await controller.initialise()
187+
# no assertion thrown
188+
with pytest.raises(
189+
RuntimeError,
190+
match=(
191+
"does not match defined access mode. Expected 'Attribute', got 'AttrRW'"
192+
),
193+
):
194+
validate_hinted_attributes(controller)

0 commit comments

Comments
 (0)