Skip to content

Commit df188b6

Browse files
author
James Souter
committed
set default ref_type to AttributeIORef for AttributeIO, add test
1 parent c29a310 commit df188b6

File tree

3 files changed

+80
-3
lines changed

3 files changed

+80
-3
lines changed

src/fastcs/attribute_io.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77

88
class AttributeIO(Generic[T, AttributeIORefT]):
9+
ref_type = AttributeIORef
10+
911
def __init_subclass__(cls) -> None:
1012
# sets ref_type from subclass generic args
1113
# from python 3.12 we can use types.get_original_bases

src/fastcs/controller.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4+
from collections import Counter
45
from collections.abc import Sequence
56
from copy import deepcopy
67
from typing import get_type_hints
@@ -39,9 +40,19 @@ def __init__(
3940

4041
# TODO, should validation live inside the controller?
4142
ios = ios or []
43+
44+
self.__check_unique(ios)
45+
4246
self._attribute_ref_io_map = {io.ref_type: io for io in ios}
4347
self._validate_io()
4448

49+
def __check_unique(self, ios: Sequence[AttributeIO[T, AttributeIORefT]]):
50+
for ref_type, count in Counter([io.ref_type for io in ios]).items():
51+
if count > 1:
52+
raise RuntimeError(
53+
f"More than one AttributeIO class handles {ref_type.__name__}"
54+
)
55+
4556
async def initialise(self):
4657
pass
4758

@@ -61,7 +72,7 @@ async def attribute_initialise(self) -> None:
6172
def _add_io_callbacks(self):
6273
for attr in self.attributes.values():
6374
ref = attr.io_ref if attr.has_io_ref() else None
64-
io = self._attribute_ref_io_map.get(type(ref), None)
75+
io = self._attribute_ref_io_map.get(type(ref))
6576
if isinstance(attr, AttrW):
6677
# is it on process or write_display?
6778
attr.add_process_callback(self._create_send_callback(io, attr, ref))
@@ -84,10 +95,10 @@ async def send_callback(value):
8495
return send_callback
8596

8697
def _create_update_callback(self, io, attr, ref):
87-
if io is None or ref is None:
98+
if ref is None:
8899

89100
async def error_callback():
90-
raise RuntimeError("No AttributeIO registered to handle update")
101+
raise RuntimeError("Attributes without io_ref can not be updated")
91102

92103
return error_callback
93104
else:

tests/test_attribute.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass
22
from functools import partial
33
from typing import Generic, TypeVar
4+
from unittest.mock import call
45

56
import pytest
67
from pytest_mock import MockerFixture
@@ -201,3 +202,66 @@ async def initialise(self):
201202
RuntimeError, match="Could not set int_parameter to 101, max is 100"
202203
):
203204
await c.int_parameter.process(101)
205+
206+
207+
@pytest.mark.asyncio
208+
async def test_attribute_io_defaults(mocker: MockerFixture):
209+
class MyController(Controller):
210+
no_ref = AttrRW(Int())
211+
base_class_ref = AttrRW(Int(), io_ref=AttributeIORef())
212+
213+
with pytest.raises(
214+
AssertionError,
215+
match="MyController does not have an AttributeIO to handle AttributeIORef",
216+
):
217+
c = MyController()
218+
219+
class SimpleAttributeIO(AttributeIO[T, AttributeIORef]):
220+
async def update(self, attr):
221+
await attr.set(100)
222+
223+
with pytest.raises(
224+
RuntimeError, match="More than one AttributeIO class handles AttributeIORef"
225+
):
226+
MyController(ios=[AttributeIO(), SimpleAttributeIO()])
227+
228+
# we need to explicitly pass an AttributeIO if we want to handle instances of
229+
# the AttributeIORef base class
230+
c = MyController(ios=[AttributeIO()])
231+
assert not c.no_ref.has_io_ref()
232+
assert c.base_class_ref.has_io_ref()
233+
234+
await c.initialise()
235+
await c.attribute_initialise()
236+
237+
with pytest.raises(NotImplementedError):
238+
await c.base_class_ref.update()
239+
240+
with pytest.raises(NotImplementedError):
241+
await c.base_class_ref.process(25)
242+
243+
# There is a difference between providing an AttributeIO for the default
244+
# AttributeIORef class and not specifying the io_ref for an Attribute
245+
# default callbacks are not provided by AttributeIO subclasses
246+
247+
with pytest.raises(
248+
RuntimeError, match="Attributes without io_ref can not be updated"
249+
): # TODO, we need a clearer error message for this
250+
await c.no_ref.update()
251+
252+
process_spy = mocker.spy(c.no_ref, "update_display_without_process")
253+
await c.no_ref.process(40)
254+
process_spy.assert_called_with(40)
255+
256+
# this is correct, but we want to reconsider this logic, it seems wasteful to
257+
# call update_display twice...
258+
assert process_spy.call_args_list == [call(40), call(40)]
259+
260+
c2 = MyController(ios=[SimpleAttributeIO()])
261+
262+
await c2.initialise()
263+
await c2.attribute_initialise()
264+
265+
assert c2.base_class_ref.get() == 0
266+
await c2.base_class_ref.update()
267+
assert c2.base_class_ref.get() == 100

0 commit comments

Comments
 (0)