Skip to content

Commit e9e7d1e

Browse files
authored
Add API to update attributes once when Backend.serve() called (#188)
Add ONCE constant to update attribute only when serve called
1 parent f30128b commit e9e7d1e

File tree

3 files changed

+63
-14
lines changed

3 files changed

+63
-14
lines changed

src/fastcs/attributes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010
from .datatypes import ATTRIBUTE_TYPES, AttrCallback, DataType, T
1111

12+
ONCE = float("inf")
13+
"""Special value to indicate that an attribute should be updated once on start up."""
14+
1215

1316
class AttrMode(Enum):
1417
"""Access mode of an ``Attribute``."""

src/fastcs/backend.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import asyncio
22
from collections import defaultdict
3-
from collections.abc import Callable
3+
from collections.abc import Callable, Coroutine
44

55
from fastcs.cs_methods import Command, Put, Scan
66
from fastcs.datatypes import T
77

8-
from .attributes import AttrHandlerR, AttrHandlerW, AttrR, AttrW
8+
from .attributes import ONCE, AttrHandlerR, AttrHandlerW, AttrR, AttrW
99
from .controller import BaseController, Controller
1010
from .controller_api import ControllerAPI
1111
from .exceptions import FastCSException
@@ -40,18 +40,19 @@ def __del__(self):
4040
self._stop_scan_tasks()
4141

4242
async def serve(self):
43+
scans, initials = _get_scan_and_initial_coros(self.controller_api)
44+
self._initial_coros += initials
4345
await self._run_initial_coros()
44-
await self._start_scan_tasks()
46+
await self._start_scan_tasks(scans)
4547

4648
async def _run_initial_coros(self):
4749
for coro in self._initial_coros:
4850
await coro()
4951

50-
async def _start_scan_tasks(self):
51-
self._scan_tasks = {
52-
self._loop.create_task(coro())
53-
for coro in _get_scan_coros(self.controller_api)
54-
}
52+
async def _start_scan_tasks(
53+
self, coros: list[Callable[[], Coroutine[None, None, None]]]
54+
):
55+
self._scan_tasks = {self._loop.create_task(coro()) for coro in coros}
5556

5657
def _stop_scan_tasks(self):
5758
for task in self._scan_tasks:
@@ -96,15 +97,18 @@ async def callback(value):
9697
return callback
9798

9899

99-
def _get_scan_coros(root_controller_api: ControllerAPI) -> list[Callable]:
100+
def _get_scan_and_initial_coros(
101+
root_controller_api: ControllerAPI,
102+
) -> tuple[list[Callable], list[Callable]]:
100103
scan_dict: dict[float, list[Callable]] = defaultdict(list)
104+
initial_coros: list[Callable] = []
101105

102106
for controller_api in root_controller_api.walk_api():
103107
_add_scan_method_tasks(scan_dict, controller_api)
104-
_add_attribute_updater_tasks(scan_dict, controller_api)
108+
_add_attribute_updater_tasks(scan_dict, initial_coros, controller_api)
105109

106110
scan_coros = _get_periodic_scan_coros(scan_dict)
107-
return scan_coros
111+
return scan_coros, initial_coros
108112

109113

110114
def _add_scan_method_tasks(
@@ -115,13 +119,17 @@ def _add_scan_method_tasks(
115119

116120

117121
def _add_attribute_updater_tasks(
118-
scan_dict: dict[float, list[Callable]], controller_api: ControllerAPI
122+
scan_dict: dict[float, list[Callable]],
123+
initial_coros: list[Callable],
124+
controller_api: ControllerAPI,
119125
):
120126
for attribute in controller_api.attributes.values():
121127
match attribute:
122128
case AttrR(updater=AttrHandlerR(update_period=update_period)) as attribute:
123129
callback = _create_updater_callback(attribute)
124-
if update_period is not None:
130+
if update_period is ONCE:
131+
initial_coros.append(callback)
132+
elif update_period is not None:
125133
scan_dict[update_period].append(callback)
126134

127135

tests/test_backend.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
2+
from dataclasses import dataclass
23

3-
from fastcs.attributes import AttrRW
4+
from fastcs.attributes import ONCE, AttrHandlerR, AttrR, AttrRW
45
from fastcs.backend import Backend, build_controller_api
56
from fastcs.controller import Controller
67
from fastcs.cs_methods import Command
@@ -89,3 +90,40 @@ async def test_wrapper():
8990
await backend.controller_api.command_methods["do_nothing_dynamic"]()
9091

9192
loop.run_until_complete(test_wrapper())
93+
94+
95+
def test_update_periods():
96+
@dataclass
97+
class AttrHandlerTimesCalled(AttrHandlerR):
98+
update_period: float | None
99+
_times_called = 0
100+
101+
async def update(self, attr):
102+
self._times_called += 1
103+
await attr.set(self._times_called)
104+
105+
class MyController(Controller):
106+
update_once = AttrR(Int(), handler=AttrHandlerTimesCalled(update_period=ONCE))
107+
update_quickly = AttrR(Int(), handler=AttrHandlerTimesCalled(update_period=0.1))
108+
update_never = AttrR(Int(), handler=AttrHandlerTimesCalled(update_period=None))
109+
110+
controller = MyController()
111+
loop = asyncio.get_event_loop()
112+
113+
backend = Backend(controller, loop)
114+
115+
assert controller.update_quickly.get() == 0
116+
assert controller.update_once.get() == 0
117+
assert controller.update_never.get() == 0
118+
119+
async def test_wrapper():
120+
loop.create_task(backend.serve())
121+
await asyncio.sleep(1)
122+
123+
loop.run_until_complete(test_wrapper())
124+
assert controller.update_quickly.get() > 1
125+
assert controller.update_once.get() == 1
126+
assert controller.update_never.get() == 0
127+
128+
assert len(backend._scan_tasks) == 1
129+
assert len(backend._initial_coros) == 2

0 commit comments

Comments
 (0)