Skip to content

Commit ac94896

Browse files
committed
validate non-GenericAlias attribute hints
1 parent 516d300 commit ac94896

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)
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, SubController
1212
from fastcs.datatypes import Bool, Enum, Float, Int, String
@@ -125,6 +125,15 @@ class ControllerWrongEnumClass(Controller):
125125
"Expected 'MyEnum', got 'MyEnum2'."
126126
)
127127

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

129138
def test_hinted_attributes_verified_on_subcontrollers():
130139
loop = asyncio.get_event_loop()
@@ -142,3 +151,25 @@ async def initialise(self):
142151

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

0 commit comments

Comments
 (0)