Skip to content

Commit 79c0234

Browse files
jsouterGDYendell
andauthored
Replace Handler with AttributeIO / AttributeIORef (#218)
Replace Handlers with AttributeIOs/AttributeIORefs Parameterise Attributes with AttributeIORef Update tests --------- Co-authored-by: Gary Yendell <[email protected]>
1 parent 58000ca commit 79c0234

File tree

20 files changed

+519
-290
lines changed

20 files changed

+519
-290
lines changed

src/fastcs/attribute_io.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import Any, Generic, cast, get_args
2+
3+
from fastcs.attribute_io_ref import AttributeIORef, AttributeIORefT
4+
from fastcs.attributes import AttrR, AttrRW
5+
from fastcs.datatypes import T
6+
7+
8+
class AttributeIO(Generic[T, AttributeIORefT]):
9+
ref_type = AttributeIORef
10+
11+
def __init_subclass__(cls) -> None:
12+
# sets ref_type from subclass generic args
13+
# from python 3.12 we can use types.get_original_bases
14+
args = get_args(cast(Any, cls).__orig_bases__[0])
15+
cls.ref_type = args[1]
16+
17+
async def update(self, attr: AttrR[T, AttributeIORefT]) -> None:
18+
raise NotImplementedError()
19+
20+
async def send(self, attr: AttrRW[T, AttributeIORefT], value: T) -> None:
21+
raise NotImplementedError()
22+
23+
24+
AnyAttributeIO = AttributeIO[T, AttributeIORef]

src/fastcs/attribute_io_ref.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from dataclasses import dataclass
2+
3+
from typing_extensions import TypeVar
4+
5+
6+
@dataclass(kw_only=True)
7+
class AttributeIORef:
8+
update_period: float | None = None
9+
10+
11+
AttributeIORefT = TypeVar(
12+
"AttributeIORefT", bound=AttributeIORef, default=AttributeIORef
13+
)

src/fastcs/attributes.py

Lines changed: 42 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,16 @@
22

33
import asyncio
44
from collections.abc import Callable
5-
from enum import Enum
6-
from typing import Any, Generic
5+
from typing import Generic
76

8-
import fastcs
9-
10-
from .datatypes import ATTRIBUTE_TYPES, AttrCallback, DataType, T
7+
from .attribute_io_ref import AttributeIORefT
8+
from .datatypes import ATTRIBUTE_TYPES, AttrSetCallback, AttrUpdateCallback, DataType, T
119

1210
ONCE = float("inf")
1311
"""Special value to indicate that an attribute should be updated once on start up."""
1412

1513

16-
class AttrMode(Enum):
17-
"""Access mode of an ``Attribute``."""
18-
19-
READ = 1
20-
WRITE = 2
21-
READ_WRITE = 3
22-
23-
24-
class _BaseAttrHandler:
25-
async def initialise(self, controller: fastcs.controller.BaseController) -> None:
26-
pass
27-
28-
29-
class AttrHandlerW(_BaseAttrHandler):
30-
"""Protocol for setting the value of an ``Attribute``."""
31-
32-
async def put(self, attr: AttrW[T], value: T) -> None:
33-
pass
34-
35-
36-
class AttrHandlerR(_BaseAttrHandler):
37-
"""Protocol for updating the cached readback value of an ``Attribute``."""
38-
39-
# If update period is None then the attribute will not be updated as a task.
40-
update_period: float | None = None
41-
42-
async def update(self, attr: AttrR[T]) -> None:
43-
pass
44-
45-
46-
class AttrHandlerRW(AttrHandlerR, AttrHandlerW):
47-
"""Protocol encapsulating both ``AttrHandlerR`` and ``AttHandlerW``."""
48-
49-
pass
50-
51-
52-
class SimpleAttrHandler(AttrHandlerRW):
53-
"""Handler for internal parameters"""
54-
55-
async def put(self, attr: AttrW[T], value: T) -> None:
56-
await attr.update_display_without_process(value)
57-
58-
if isinstance(attr, AttrRW):
59-
await attr.set(value)
60-
61-
async def update(self, attr: AttrR) -> None:
62-
raise RuntimeError("SimpleHandler cannot update")
63-
64-
65-
class Attribute(Generic[T]):
14+
class Attribute(Generic[T, AttributeIORefT]):
6615
"""Base FastCS attribute.
6716
6817
Instances of this class added to a ``Controller`` will be used by the backend.
@@ -71,26 +20,33 @@ class Attribute(Generic[T]):
7120
def __init__(
7221
self,
7322
datatype: DataType[T],
74-
access_mode: AttrMode,
23+
io_ref: AttributeIORefT | None = None,
7524
group: str | None = None,
76-
handler: Any = None,
7725
description: str | None = None,
7826
) -> None:
7927
assert issubclass(datatype.dtype, ATTRIBUTE_TYPES), (
8028
f"Attr type must be one of {ATTRIBUTE_TYPES}, "
8129
"received type {datatype.dtype}"
8230
)
31+
self._io_ref = io_ref
8332
self._datatype: DataType[T] = datatype
84-
self._access_mode: AttrMode = access_mode
8533
self._group = group
86-
self._handler = handler
8734
self.enabled = True
8835
self.description = description
8936

9037
# A callback to use when setting the datatype to a different value, for example
9138
# changing the units on an int. This should be implemented in the backend.
9239
self._update_datatype_callbacks: list[Callable[[DataType[T]], None]] = []
9340

41+
@property
42+
def io_ref(self) -> AttributeIORefT:
43+
if self._io_ref is None:
44+
raise RuntimeError(f"{self} has no AttributeIORef")
45+
return self._io_ref
46+
47+
def has_io_ref(self):
48+
return self._io_ref is not None
49+
9450
@property
9551
def datatype(self) -> DataType[T]:
9652
return self._datatype
@@ -99,18 +55,10 @@ def datatype(self) -> DataType[T]:
9955
def dtype(self) -> type[T]:
10056
return self._datatype.dtype
10157

102-
@property
103-
def access_mode(self) -> AttrMode:
104-
return self._access_mode
105-
10658
@property
10759
def group(self) -> str | None:
10860
return self._group
10961

110-
async def initialise(self, controller: fastcs.controller.BaseController) -> None:
111-
if self._handler is not None:
112-
await self._handler.initialise(controller)
113-
11462
def add_update_datatype_callback(
11563
self, callback: Callable[[DataType[T]], None]
11664
) -> None:
@@ -126,71 +74,71 @@ def update_datatype(self, datatype: DataType[T]) -> None:
12674
callback(datatype)
12775

12876

129-
class AttrR(Attribute[T]):
77+
class AttrR(Attribute[T, AttributeIORefT]):
13078
"""A read-only ``Attribute``."""
13179

13280
def __init__(
13381
self,
13482
datatype: DataType[T],
135-
access_mode=AttrMode.READ,
83+
io_ref: AttributeIORefT | None = None,
13684
group: str | None = None,
137-
handler: AttrHandlerR | None = None,
13885
initial_value: T | None = None,
13986
description: str | None = None,
14087
) -> None:
14188
super().__init__(
14289
datatype, # type: ignore
143-
access_mode,
90+
io_ref,
14491
group,
145-
handler,
14692
description=description,
14793
)
14894
self._value: T = (
14995
datatype.initial_value if initial_value is None else initial_value
15096
)
151-
self._update_callbacks: list[AttrCallback[T]] | None = None
152-
self._updater = handler
97+
self._on_set_callbacks: list[AttrSetCallback[T]] | None = None
98+
self._on_update_callbacks: list[AttrUpdateCallback] | None = None
15399

154100
def get(self) -> T:
155101
return self._value
156102

157103
async def set(self, value: T) -> None:
158104
self._value = self._datatype.validate(value)
159105

160-
if self._update_callbacks is not None:
161-
await asyncio.gather(*[cb(self._value) for cb in self._update_callbacks])
106+
if self._on_set_callbacks is not None:
107+
await asyncio.gather(*[cb(self._value) for cb in self._on_set_callbacks])
162108

163-
def add_update_callback(self, callback: AttrCallback[T]) -> None:
164-
if self._update_callbacks is None:
165-
self._update_callbacks = []
166-
self._update_callbacks.append(callback)
109+
def add_set_callback(self, callback: AttrSetCallback[T]) -> None:
110+
if self._on_set_callbacks is None:
111+
self._on_set_callbacks = []
112+
self._on_set_callbacks.append(callback)
167113

168-
@property
169-
def updater(self) -> AttrHandlerR | None:
170-
return self._updater
114+
def add_update_callback(self, callback: AttrUpdateCallback):
115+
if self._on_update_callbacks is None:
116+
self._on_update_callbacks = []
117+
self._on_update_callbacks.append(callback)
171118

119+
async def update(self):
120+
if self._on_update_callbacks is not None:
121+
await asyncio.gather(*[cb() for cb in self._on_update_callbacks])
172122

173-
class AttrW(Attribute[T]):
123+
124+
class AttrW(Attribute[T, AttributeIORefT]):
174125
"""A write-only ``Attribute``."""
175126

176127
def __init__(
177128
self,
178129
datatype: DataType[T],
179-
access_mode=AttrMode.WRITE,
130+
io_ref: AttributeIORefT | None = None,
180131
group: str | None = None,
181-
handler: AttrHandlerW | None = None,
182132
description: str | None = None,
183133
) -> None:
184134
super().__init__(
185135
datatype, # type: ignore
186-
access_mode,
136+
io_ref,
187137
group,
188-
handler,
189138
description=description,
190139
)
191-
self._process_callbacks: list[AttrCallback[T]] | None = None
192-
self._write_display_callbacks: list[AttrCallback[T]] | None = None
193-
self._setter = handler
140+
self._process_callbacks: list[AttrSetCallback[T]] | None = None
141+
self._write_display_callbacks: list[AttrSetCallback[T]] | None = None
194142

195143
async def process(self, value: T) -> None:
196144
await self.process_without_display_update(value)
@@ -206,45 +154,23 @@ async def update_display_without_process(self, value: T) -> None:
206154
if self._write_display_callbacks:
207155
await asyncio.gather(*[cb(value) for cb in self._write_display_callbacks])
208156

209-
def add_process_callback(self, callback: AttrCallback[T]) -> None:
157+
def add_process_callback(self, callback: AttrSetCallback[T]) -> None:
210158
if self._process_callbacks is None:
211159
self._process_callbacks = []
212160
self._process_callbacks.append(callback)
213161

214162
def has_process_callback(self) -> bool:
215163
return bool(self._process_callbacks)
216164

217-
def add_write_display_callback(self, callback: AttrCallback[T]) -> None:
165+
def add_write_display_callback(self, callback: AttrSetCallback[T]) -> None:
218166
if self._write_display_callbacks is None:
219167
self._write_display_callbacks = []
220168
self._write_display_callbacks.append(callback)
221169

222-
@property
223-
def sender(self) -> AttrHandlerW | None:
224-
return self._setter
225-
226170

227-
class AttrRW(AttrR[T], AttrW[T]):
171+
class AttrRW(AttrR[T, AttributeIORefT], AttrW[T, AttributeIORefT]):
228172
"""A read-write ``Attribute``."""
229173

230-
def __init__(
231-
self,
232-
datatype: DataType[T],
233-
access_mode=AttrMode.READ_WRITE,
234-
group: str | None = None,
235-
handler: AttrHandlerRW | None = None,
236-
initial_value: T | None = None,
237-
description: str | None = None,
238-
) -> None:
239-
super().__init__(
240-
datatype, # type: ignore
241-
access_mode,
242-
group=group,
243-
handler=handler if handler else SimpleAttrHandler(),
244-
initial_value=initial_value,
245-
description=description,
246-
)
247-
248174
async def process(self, value: T) -> None:
249175
await self.set(value)
250176

src/fastcs/backend.py

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
from collections import defaultdict
33
from collections.abc import Callable, Coroutine
44

5+
from fastcs.attribute_io_ref import AttributeIORef
56
from fastcs.cs_methods import Command, Put, Scan
67
from fastcs.datatypes import T
78

8-
from .attributes import ONCE, AttrHandlerR, AttrHandlerW, AttrR, AttrW
9+
from .attributes import ONCE, AttrR, AttrW
910
from .controller import BaseController, Controller
1011
from .controller_api import ControllerAPI
1112
from .exceptions import FastCSError
@@ -36,7 +37,6 @@ def __init__(
3637
def _link_process_tasks(self):
3738
for controller_api in self.controller_api.walk_api():
3839
_link_put_tasks(controller_api)
39-
_link_attribute_sender_class(controller_api)
4040

4141
def __del__(self):
4242
self._stop_scan_tasks()
@@ -87,30 +87,11 @@ def _link_put_tasks(controller_api: ControllerAPI) -> None:
8787
attribute.add_process_callback(method.fn)
8888
case _:
8989
raise FastCSError(
90-
f"Mode {attribute.access_mode} does not "
90+
f"Attribute type {type(attribute)} does not "
9191
f"support put operations for {name}"
9292
)
9393

9494

95-
def _link_attribute_sender_class(controller_api: ControllerAPI) -> None:
96-
for attr_name, attribute in controller_api.attributes.items():
97-
match attribute:
98-
case AttrW(sender=AttrHandlerW()):
99-
assert not attribute.has_process_callback(), (
100-
f"Cannot assign both put method and Sender object to {attr_name}"
101-
)
102-
103-
callback = _create_sender_callback(attribute)
104-
attribute.add_process_callback(callback)
105-
106-
107-
def _create_sender_callback(attribute):
108-
async def callback(value):
109-
await attribute.sender.put(attribute, value)
110-
111-
return callback
112-
113-
11495
def _get_scan_and_initial_coros(
11596
root_controller_api: ControllerAPI,
11697
) -> tuple[list[Callable], list[Callable]]:
@@ -139,7 +120,9 @@ def _add_attribute_updater_tasks(
139120
):
140121
for attribute in controller_api.attributes.values():
141122
match attribute:
142-
case AttrR(updater=AttrHandlerR(update_period=update_period)) as attribute:
123+
case (
124+
AttrR(_io_ref=AttributeIORef(update_period=update_period)) as attribute
125+
):
143126
callback = _create_updater_callback(attribute)
144127
if update_period is ONCE:
145128
initial_coros.append(callback)
@@ -148,14 +131,11 @@ def _add_attribute_updater_tasks(
148131

149132

150133
def _create_updater_callback(attribute: AttrR[T]):
151-
updater = attribute.updater
152-
assert updater is not None
153-
154134
async def callback():
155135
try:
156-
await updater.update(attribute)
136+
await attribute.update()
157137
except Exception as e:
158-
print(f"Update loop in {updater} stopped:\n{e.__class__.__name__}: {e}")
138+
print(f"Update loop in {attribute} stopped:\n{e.__class__.__name__}: {e}")
159139
raise
160140

161141
return callback

0 commit comments

Comments
 (0)