Skip to content

Commit 42e965e

Browse files
committed
Add API for creating a Bound{Put,Scan,Command} directly
1 parent cd26aae commit 42e965e

File tree

4 files changed

+91
-41
lines changed

4 files changed

+91
-41
lines changed

src/fastcs/controller.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,14 @@ class method and a controller instance, so that it can be called from any
3535
3636
"""
3737
# Lazy import to avoid circular references
38-
from fastcs.cs_methods import (
39-
BoundCommand,
40-
BoundPut,
41-
BoundScan,
42-
Command,
43-
Put,
44-
Scan,
45-
)
38+
from fastcs.cs_methods import Command, Put, Scan
4639

4740
for attr_name in dir(self):
4841
attr = getattr(self, attr_name)
4942
if isinstance(attr, Attribute):
5043
setattr(self, attr_name, copy(attr))
51-
elif isinstance(attr, Command):
52-
setattr(self, attr_name, BoundCommand(attr, self))
53-
elif isinstance(attr, Put):
54-
setattr(self, attr_name, BoundPut(attr, self))
55-
elif isinstance(attr, Scan):
56-
setattr(self, attr_name, BoundScan(attr, self))
44+
elif isinstance(attr, Put | Scan | Command):
45+
setattr(self, attr_name, attr.bind(self))
5746

5847
def register_sub_controller(self, name: str, sub_controller: SubController):
5948
if name in self.__sub_controller_tree.keys():

src/fastcs/cs_methods.py

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from asyncio import iscoroutinefunction
22
from collections.abc import Callable, Coroutine
33
from inspect import Signature, getdoc, signature
4+
from types import MethodType
45
from typing import Any, Generic, TypeVar
56

67
from fastcs.controller import BaseController
@@ -13,6 +14,14 @@
1314
CommandCallback = Callable[[ControllerType], Coroutine[None, None, None]]
1415
ScanCallback = Callable[[ControllerType], Coroutine[None, None, None]]
1516
PutCallback = Callable[[ControllerType, Any], Coroutine[None, None, None]]
17+
BoundCommandCallback = Callable[[], Coroutine[None, None, None]]
18+
BoundScanCallback = Callable[[], Coroutine[None, None, None]]
19+
BoundPutCallback = Callable[[Any], Coroutine[None, None, None]]
20+
21+
22+
method_not_bound_error = NotImplementedError(
23+
"Method must be bound to a controller instance to be callable"
24+
)
1625

1726

1827
class Method(Generic[ControllerType]):
@@ -62,15 +71,21 @@ def __init__(self, fn: ScanCallback[ControllerType], period: float) -> None:
6271

6372
self._period = period
6473

74+
@property
75+
def period(self):
76+
return self._period
77+
6578
def _validate(self, fn: ScanCallback[ControllerType]) -> None:
6679
super()._validate(fn)
6780

6881
if not len(self.parameters) == 1:
6982
raise FastCSException("Scan method cannot have arguments")
7083

71-
@property
72-
def period(self):
73-
return self._period
84+
def bind(self, controller: ControllerType) -> "BoundScan":
85+
return BoundScan(MethodType(self.fn, controller), self._period)
86+
87+
def __call__(self):
88+
raise method_not_bound_error
7489

7590

7691
class Put(Method[ControllerType]):
@@ -83,6 +98,12 @@ def _validate(self, fn: PutCallback[ControllerType]) -> None:
8398
if not len(self.parameters) == 2:
8499
raise FastCSException("Put method can only take one argument")
85100

101+
def bind(self, controller: ControllerType) -> "BoundPut":
102+
return BoundPut(MethodType(self.fn, controller))
103+
104+
def __call__(self, value: Any):
105+
raise method_not_bound_error
106+
86107

87108
class Command(Method[ControllerType]):
88109
def __init__(
@@ -96,32 +117,56 @@ def _validate(self, fn: CommandCallback[ControllerType]) -> None:
96117
if not len(self.parameters) == 1:
97118
raise FastCSException("Command method cannot have arguments")
98119

120+
def bind(self, controller: ControllerType) -> "BoundCommand":
121+
return BoundCommand(MethodType(self.fn, controller))
99122

100-
class BoundScan(Scan):
101-
def __init__(self, scan: Scan[BaseController], controller: BaseController) -> None:
102-
super().__init__(scan.fn, scan.period)
123+
def __call__(self):
124+
raise method_not_bound_error
103125

104-
self._controller = controller
105126

106-
async def __call__(self):
107-
return await self._fn(self._controller)
127+
class BoundScan(Method[BaseController]):
128+
def __init__(self, fn: BoundScanCallback, period: float):
129+
super().__init__(fn)
108130

131+
self._period = period
109132

110-
class BoundPut(Put):
111-
def __init__(self, put: Put, controller: BaseController) -> None:
112-
super().__init__(put.fn)
133+
@property
134+
def period(self):
135+
return self._period
113136

114-
self._controller = controller
137+
def _validate(self, fn: BoundScanCallback) -> None:
138+
super()._validate(fn)
139+
140+
if not len(self.parameters) == 0:
141+
raise FastCSException("Scan method cannot have arguments")
142+
143+
def __call__(self):
144+
return self._fn()
145+
146+
147+
class BoundPut(Method[BaseController]):
148+
def __init__(self, fn: BoundPutCallback):
149+
super().__init__(fn)
150+
151+
def _validate(self, fn: BoundPutCallback) -> None:
152+
super()._validate(fn)
153+
154+
if not len(self.parameters) == 1:
155+
raise FastCSException("Put method can only take one argument")
115156

116-
async def __call__(self, value: bool | int | float | str):
117-
return await self._fn(self._controller, value)
157+
def __call__(self, value: Any):
158+
return self._fn(value)
118159

119160

120-
class BoundCommand(Command):
121-
def __init__(self, command: Command, controller: BaseController) -> None:
122-
super().__init__(command.fn)
161+
class BoundCommand(Method[BaseController]):
162+
def __init__(self, fn: BoundCommandCallback):
163+
super().__init__(fn)
164+
165+
def _validate(self, fn: BoundCommandCallback) -> None:
166+
super()._validate(fn)
123167

124-
self._controller = controller
168+
if not len(self.parameters) == 0:
169+
raise FastCSException("Command method cannot have arguments")
125170

126-
async def __call__(self):
127-
return await self._fn(self._controller)
171+
def __call__(self):
172+
return self._fn()

src/fastcs/mapping.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,20 @@ def get_single_mapping(controller: BaseController) -> SingleMapping:
4747
for attr_name in dir(controller):
4848
attr = getattr(controller, attr_name)
4949
match attr:
50+
case Attribute(enabled=True):
51+
attributes[attr_name] = attr
52+
case BoundPut(enabled=True):
53+
put_methods[attr_name] = attr
5054
case Put(enabled=True):
51-
put_methods[attr_name] = BoundPut(attr, controller)
55+
put_methods[attr_name] = attr.bind(controller)
56+
case BoundScan(enabled=True):
57+
scan_methods[attr_name] = attr
5258
case Scan(enabled=True):
53-
scan_methods[attr_name] = BoundScan(attr, controller)
59+
scan_methods[attr_name] = attr.bind(controller)
60+
case BoundCommand(enabled=True):
61+
command_methods[attr_name] = attr
5462
case Command(enabled=True):
55-
command_methods[attr_name] = BoundCommand(attr, controller)
56-
case Attribute(enabled=True):
57-
attributes[attr_name] = attr
63+
command_methods[attr_name] = attr.bind(controller)
5864
case _:
5965
pass
6066

tests/test_controller.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from fastcs.backend import Backend
44
from fastcs.controller import Controller, SubController
5+
from fastcs.cs_methods import BoundCommand
56
from fastcs.mapping import _walk_mappings, get_single_mapping
67
from fastcs.wrappers import command
78

@@ -36,11 +37,20 @@ def test_controller_nesting():
3637
@pytest.mark.asyncio
3738
async def test_controller_methods():
3839
class TestController(Controller):
40+
def __init__(self):
41+
super().__init__()
42+
self.do_nothing2 = BoundCommand(self.do_nothing2)
43+
3944
@command()
4045
async def do_nothing(self):
4146
pass
4247

48+
async def do_nothing2(self):
49+
pass
50+
4351
c = TestController()
44-
b = Backend(c)
4552
await c.do_nothing()
53+
await c.do_nothing2()
54+
b = Backend(c)
4655
await b._mapping.get_controller_mappings()[0].command_methods["do_nothing"]()
56+
await b._mapping.get_controller_mappings()[0].command_methods["do_nothing2"]()

0 commit comments

Comments
 (0)