Skip to content

Commit 9bd3823

Browse files
jsouterGDYendell
authored andcommitted
validate non-GenericAlias attribute hints
1 parent 1f22198 commit 9bd3823

File tree

2 files changed

+47
-8
lines changed

2 files changed

+47
-8
lines changed

src/fastcs/util.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,18 @@ def validate_hinted_attributes(controller: BaseController):
3737
"""
3838
for subcontroller in controller.get_sub_controllers().values():
3939
validate_hinted_attributes(subcontroller)
40-
hints = get_type_hints(type(controller))
41-
alias_hints = {k: v for k, v in hints.items() if isinstance(v, _GenericAlias)}
42-
for name, hint in alias_hints.items():
43-
attr_class = get_origin(hint)
40+
hints = {
41+
k: v
42+
for k, v in get_type_hints(type(controller)).items()
43+
if isinstance(v, _GenericAlias | type)
44+
}
45+
for name, hint in hints.items():
46+
if isinstance(hint, type):
47+
attr_class = hint
48+
attr_dtype = None
49+
else:
50+
attr_class = get_origin(hint)
51+
(attr_dtype,) = get_args(hint)
4452
if not issubclass(attr_class, Attribute):
4553
continue
4654
attr = getattr(controller, name, None)
@@ -49,14 +57,14 @@ def validate_hinted_attributes(controller: BaseController):
4957
f"Controller `{controller.__class__.__name__}` failed to introspect "
5058
f"hinted attribute `{name}` during initialisation"
5159
)
52-
if type(attr) is not attr_class:
60+
if attr_class not in [type(attr), Attribute]:
61+
# skip validation if access mode not specified
5362
raise RuntimeError(
5463
f"Controller '{controller.__class__.__name__}' introspection of hinted "
5564
f"attribute '{name}' does not match defined access mode. "
5665
f"Expected '{attr_class.__name__}', got '{type(attr).__name__}'."
5766
)
58-
attr_dtype = get_args(hint)[0]
59-
if attr.datatype.dtype != attr_dtype:
67+
if attr_dtype not in [attr.datatype.dtype, None]:
6068
raise RuntimeError(
6169
f"Controller '{controller.__class__.__name__}' introspection of hinted "
6270
f"attribute '{name}' does not match defined datatype. "

tests/test_util.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pvi.device import SignalR
77
from pydantic import ValidationError
88

9-
from fastcs.attributes import AttrR, AttrRW
9+
from fastcs.attributes import Attribute, AttrR, AttrRW
1010
from fastcs.backend import Backend
1111
from fastcs.controller import Controller
1212
from fastcs.datatypes import Bool, Enum, Float, Int, String
@@ -127,6 +127,15 @@ class ControllerWrongEnumClass(Controller):
127127
"Expected 'MyEnum', got 'MyEnum2'."
128128
)
129129

130+
class ControllerUnspecifiedAccessMode(Controller):
131+
hinted: Attribute[int]
132+
133+
async def initialise(self):
134+
self.hinted = AttrR(Int())
135+
136+
# no assertion thrown
137+
Backend(ControllerUnspecifiedAccessMode(), loop)
138+
130139

131140
def test_hinted_attributes_verified_on_subcontrollers():
132141
loop = asyncio.get_event_loop()
@@ -144,3 +153,25 @@ async def initialise(self):
144153

145154
with pytest.raises(RuntimeError, match="failed to introspect hinted attribute"):
146155
Backend(TopController(), loop)
156+
157+
158+
def test_hinted_attribute_types_verified():
159+
# test verification works with non-GenericAlias type hints
160+
loop = asyncio.get_event_loop()
161+
162+
class ControllerAttrWrongAccessMode(Controller):
163+
read_attr: AttrR
164+
165+
async def initialise(self):
166+
self.read_attr = AttrRW(Int())
167+
168+
with pytest.raises(RuntimeError, match="does not match defined access mode"):
169+
Backend(ControllerAttrWrongAccessMode(), loop)
170+
171+
class ControllerUnspecifiedAccessMode(Controller):
172+
unspecified_access_mode: Attribute
173+
174+
async def initialise(self):
175+
self.unspecified_access_mode = AttrRW(Int())
176+
177+
Backend(ControllerUnspecifiedAccessMode(), loop)

0 commit comments

Comments
 (0)