Skip to content

Commit 0c113ce

Browse files
author
James Souter
committed
validate non-GenericAlias attribute hints
1 parent 38a39cf commit 0c113ce

File tree

2 files changed

+47
-10
lines changed

2 files changed

+47
-10
lines changed

src/fastcs/util.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,18 @@ def validate_hinted_attributes(controller: BaseController):
3939
"""
4040
for subcontroller in controller.sub_controllers.values():
4141
validate_hinted_attributes(subcontroller)
42-
hints = get_type_hints(type(controller))
43-
alias_hints = {k: v for k, v in hints.items() if isinstance(v, _GenericAlias)}
44-
for name, hint in alias_hints.items():
45-
attr_class = get_origin(hint)
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)
4654
if not issubclass(attr_class, Attribute):
4755
continue
4856

@@ -52,16 +60,14 @@ def validate_hinted_attributes(controller: BaseController):
5260
f"Controller `{controller.__class__.__name__}` failed to introspect "
5361
f"hinted attribute `{name}` during initialisation"
5462
)
55-
56-
if type(attr) is not attr_class:
63+
if attr_class not in [type(attr), Attribute]:
64+
# skip validation if access mode not specified
5765
raise RuntimeError(
5866
f"Controller '{controller.__class__.__name__}' introspection of hinted "
5967
f"attribute '{name}' does not match defined access mode. "
6068
f"Expected '{attr_class.__name__}', got '{type(attr).__name__}'."
6169
)
62-
63-
attr_dtype = get_args(hint)[0]
64-
if attr.datatype.dtype != attr_dtype:
70+
if attr_dtype not in [attr.datatype.dtype, None]:
6571
raise RuntimeError(
6672
f"Controller '{controller.__class__.__name__}' introspection of hinted "
6773
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
@@ -5,7 +5,7 @@
55
from pvi.device import SignalR
66
from pydantic import ValidationError
77

8-
from fastcs.attributes import AttrR, AttrRW
8+
from fastcs.attributes import Attribute, AttrR, AttrRW
99
from fastcs.backend import Backend
1010
from fastcs.controller import Controller
1111
from fastcs.datatypes import Bool, Enum, Float, Int, String
@@ -138,6 +138,15 @@ class ControllerWrongEnumClass(Controller):
138138
"Expected 'MyEnum', got 'MyEnum2'."
139139
)
140140

141+
class ControllerUnspecifiedAccessMode(Controller):
142+
hinted: Attribute[int]
143+
144+
async def initialise(self):
145+
self.hinted = AttrR(Int())
146+
147+
# no assertion thrown
148+
Backend(ControllerUnspecifiedAccessMode(), loop)
149+
141150

142151
def test_hinted_attributes_verified_on_subcontrollers():
143152
loop = asyncio.get_event_loop()
@@ -155,3 +164,25 @@ async def initialise(self):
155164

156165
with pytest.raises(RuntimeError, match="failed to introspect hinted attribute"):
157166
Backend(TopController(), loop)
167+
168+
169+
def test_hinted_attribute_types_verified():
170+
# test verification works with non-GenericAlias type hints
171+
loop = asyncio.get_event_loop()
172+
173+
class ControllerAttrWrongAccessMode(Controller):
174+
read_attr: AttrR
175+
176+
async def initialise(self):
177+
self.read_attr = AttrRW(Int())
178+
179+
with pytest.raises(RuntimeError, match="does not match defined access mode"):
180+
Backend(ControllerAttrWrongAccessMode(), loop)
181+
182+
class ControllerUnspecifiedAccessMode(Controller):
183+
unspecified_access_mode: Attribute
184+
185+
async def initialise(self):
186+
self.unspecified_access_mode = AttrRW(Int())
187+
188+
Backend(ControllerUnspecifiedAccessMode(), loop)

0 commit comments

Comments
 (0)