ophyd-async 0.7.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +34 -9
- ophyd_async/core/_detector.py +5 -10
- ophyd_async/core/_device.py +170 -68
- ophyd_async/core/_device_filler.py +269 -0
- ophyd_async/core/_device_save_loader.py +6 -7
- ophyd_async/core/_mock_signal_backend.py +35 -40
- ophyd_async/core/_mock_signal_utils.py +25 -16
- ophyd_async/core/_protocol.py +28 -8
- ophyd_async/core/_readable.py +133 -134
- ophyd_async/core/_signal.py +219 -163
- ophyd_async/core/_signal_backend.py +131 -64
- ophyd_async/core/_soft_signal_backend.py +131 -194
- ophyd_async/core/_status.py +22 -6
- ophyd_async/core/_table.py +102 -100
- ophyd_async/core/_utils.py +143 -32
- ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
- ophyd_async/epics/adaravis/_aravis_io.py +8 -6
- ophyd_async/epics/adcore/_core_io.py +5 -7
- ophyd_async/epics/adcore/_core_logic.py +3 -1
- ophyd_async/epics/adcore/_hdf_writer.py +2 -2
- ophyd_async/epics/adcore/_single_trigger.py +6 -10
- ophyd_async/epics/adcore/_utils.py +15 -10
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
- ophyd_async/epics/adkinetix/_kinetix_io.py +4 -5
- ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus_io.py +3 -4
- ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
- ophyd_async/epics/advimba/__init__.py +4 -1
- ophyd_async/epics/advimba/_vimba_controller.py +6 -3
- ophyd_async/epics/advimba/_vimba_io.py +8 -9
- ophyd_async/epics/core/__init__.py +26 -0
- ophyd_async/epics/core/_aioca.py +323 -0
- ophyd_async/epics/core/_epics_connector.py +53 -0
- ophyd_async/epics/core/_epics_device.py +13 -0
- ophyd_async/epics/core/_p4p.py +383 -0
- ophyd_async/epics/core/_pvi_connector.py +91 -0
- ophyd_async/epics/core/_signal.py +171 -0
- ophyd_async/epics/core/_util.py +61 -0
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/_sensor.py +14 -13
- ophyd_async/epics/eiger/_eiger.py +1 -2
- ophyd_async/epics/eiger/_eiger_controller.py +7 -2
- ophyd_async/epics/eiger/_eiger_io.py +3 -5
- ophyd_async/epics/eiger/_odin_io.py +5 -5
- ophyd_async/epics/motor.py +4 -5
- ophyd_async/epics/signal.py +11 -0
- ophyd_async/epics/testing/__init__.py +24 -0
- ophyd_async/epics/testing/_example_ioc.py +105 -0
- ophyd_async/epics/testing/_utils.py +78 -0
- ophyd_async/epics/testing/test_records.db +152 -0
- ophyd_async/epics/testing/test_records_pva.db +177 -0
- ophyd_async/fastcs/core.py +9 -0
- ophyd_async/fastcs/panda/__init__.py +4 -4
- ophyd_async/fastcs/panda/_block.py +18 -13
- ophyd_async/fastcs/panda/_control.py +3 -5
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
- ophyd_async/fastcs/panda/_table.py +30 -52
- ophyd_async/fastcs/panda/_trigger.py +8 -8
- ophyd_async/fastcs/panda/_writer.py +2 -5
- ophyd_async/plan_stubs/_ensure_connected.py +20 -13
- ophyd_async/plan_stubs/_fly.py +2 -2
- ophyd_async/plan_stubs/_nd_attributes.py +5 -4
- ophyd_async/py.typed +0 -0
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
- ophyd_async/sim/demo/_sim_motor.py +3 -4
- ophyd_async/tango/__init__.py +0 -45
- ophyd_async/tango/{signal → core}/__init__.py +9 -6
- ophyd_async/tango/core/_base_device.py +132 -0
- ophyd_async/tango/{signal → core}/_signal.py +42 -53
- ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
- ophyd_async/tango/{signal → core}/_tango_transport.py +38 -40
- ophyd_async/tango/demo/_counter.py +12 -23
- ophyd_async/tango/demo/_mover.py +13 -13
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/METADATA +52 -55
- ophyd_async-0.8.0.dist-info/RECORD +116 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/WHEEL +1 -1
- ophyd_async/epics/pvi/__init__.py +0 -3
- ophyd_async/epics/pvi/_pvi.py +0 -338
- ophyd_async/epics/signal/__init__.py +0 -21
- ophyd_async/epics/signal/_aioca.py +0 -378
- ophyd_async/epics/signal/_common.py +0 -57
- ophyd_async/epics/signal/_epics_transport.py +0 -34
- ophyd_async/epics/signal/_p4p.py +0 -518
- ophyd_async/epics/signal/_signal.py +0 -114
- ophyd_async/tango/base_devices/__init__.py +0 -4
- ophyd_async/tango/base_devices/_base_device.py +0 -225
- ophyd_async-0.7.0.dist-info/RECORD +0 -108
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/LICENSE +0 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
Generic,
|
|
8
|
+
NewType,
|
|
9
|
+
NoReturn,
|
|
10
|
+
Protocol,
|
|
11
|
+
TypeVar,
|
|
12
|
+
cast,
|
|
13
|
+
get_args,
|
|
14
|
+
get_type_hints,
|
|
15
|
+
runtime_checkable,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ._device import Device, DeviceConnector, DeviceVector
|
|
19
|
+
from ._signal import Signal, SignalX
|
|
20
|
+
from ._signal_backend import SignalBackend, SignalDatatype
|
|
21
|
+
from ._utils import get_origin_class
|
|
22
|
+
|
|
23
|
+
SignalBackendT = TypeVar("SignalBackendT", bound=SignalBackend)
|
|
24
|
+
DeviceConnectorT = TypeVar("DeviceConnectorT", bound=DeviceConnector)
|
|
25
|
+
# Unique name possibly with trailing understore, the attribute name on the Device
|
|
26
|
+
UniqueName = NewType("UniqueName", str)
|
|
27
|
+
# Logical name without trailing underscore, the name in the control system
|
|
28
|
+
LogicalName = NewType("LogicalName", str)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_datatype(annotation: Any) -> type | None:
|
|
32
|
+
"""Return int from SignalRW[int]."""
|
|
33
|
+
args = get_args(annotation)
|
|
34
|
+
if len(args) == 1 and get_origin_class(args[0]):
|
|
35
|
+
return args[0]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _logical(name: UniqueName) -> LogicalName:
|
|
39
|
+
return LogicalName(name.rstrip("_"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@runtime_checkable
|
|
43
|
+
class DeviceAnnotation(Protocol):
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def __call__(self, parent: Device, child: Device): ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
device: Device,
|
|
52
|
+
signal_backend_factory: Callable[[type[SignalDatatype] | None], SignalBackendT],
|
|
53
|
+
device_connector_factory: Callable[[], DeviceConnectorT],
|
|
54
|
+
):
|
|
55
|
+
self._device = device
|
|
56
|
+
self._signal_backend_factory = signal_backend_factory
|
|
57
|
+
self._device_connector_factory = device_connector_factory
|
|
58
|
+
# Annotations stored ready for the creation phase
|
|
59
|
+
self._uncreated_signals: dict[UniqueName, type[Signal]] = {}
|
|
60
|
+
self._uncreated_devices: dict[UniqueName, type[Device]] = {}
|
|
61
|
+
self._extras: dict[UniqueName, Sequence[Any]] = {}
|
|
62
|
+
self._signal_datatype: dict[LogicalName, type | None] = {}
|
|
63
|
+
self._vector_device_type: dict[LogicalName, type[Device] | None] = {}
|
|
64
|
+
# Backends and Connectors stored ready for the connection phase
|
|
65
|
+
self._unfilled_backends: dict[
|
|
66
|
+
LogicalName, tuple[SignalBackendT, type[Signal]]
|
|
67
|
+
] = {}
|
|
68
|
+
self._unfilled_connectors: dict[LogicalName, DeviceConnectorT] = {}
|
|
69
|
+
# Once they are filled they go here in case we reconnect
|
|
70
|
+
self._filled_backends: dict[
|
|
71
|
+
LogicalName, tuple[SignalBackendT, type[Signal]]
|
|
72
|
+
] = {}
|
|
73
|
+
self._filled_connectors: dict[LogicalName, DeviceConnectorT] = {}
|
|
74
|
+
self._scan_for_annotations()
|
|
75
|
+
|
|
76
|
+
def _raise(self, name: str, error: str) -> NoReturn:
|
|
77
|
+
raise TypeError(f"{type(self._device).__name__}.{name}: {error}")
|
|
78
|
+
|
|
79
|
+
def _store_signal_datatype(self, name: UniqueName, annotation: Any):
|
|
80
|
+
origin = get_origin_class(annotation)
|
|
81
|
+
datatype = _get_datatype(annotation)
|
|
82
|
+
if origin == SignalX:
|
|
83
|
+
# SignalX doesn't need datatype
|
|
84
|
+
self._signal_datatype[_logical(name)] = None
|
|
85
|
+
elif origin and issubclass(origin, Signal) and datatype:
|
|
86
|
+
# All other Signals need one
|
|
87
|
+
self._signal_datatype[_logical(name)] = datatype
|
|
88
|
+
else:
|
|
89
|
+
# Not recognized
|
|
90
|
+
self._raise(
|
|
91
|
+
name,
|
|
92
|
+
f"Expected SignalX or SignalR/W/RW[type], got {annotation}",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def _scan_for_annotations(self):
|
|
96
|
+
# Get type hints on the class, not the instance
|
|
97
|
+
# https://github.com/python/cpython/issues/124840
|
|
98
|
+
cls = type(self._device)
|
|
99
|
+
# Get hints without Annotated for determining types
|
|
100
|
+
hints = get_type_hints(cls)
|
|
101
|
+
# Get hints with Annotated for wrapping signals and backends
|
|
102
|
+
extra_hints = get_type_hints(cls, include_extras=True)
|
|
103
|
+
for attr_name, annotation in hints.items():
|
|
104
|
+
name = UniqueName(attr_name)
|
|
105
|
+
origin = get_origin_class(annotation)
|
|
106
|
+
if (
|
|
107
|
+
name == "parent"
|
|
108
|
+
or name.startswith("_")
|
|
109
|
+
or not origin
|
|
110
|
+
or not issubclass(origin, Device)
|
|
111
|
+
):
|
|
112
|
+
# Ignore any child that is not a public Device
|
|
113
|
+
continue
|
|
114
|
+
self._extras[name] = getattr(extra_hints[attr_name], "__metadata__", ())
|
|
115
|
+
if issubclass(origin, Signal):
|
|
116
|
+
self._store_signal_datatype(name, annotation)
|
|
117
|
+
self._uncreated_signals[name] = origin
|
|
118
|
+
elif origin == DeviceVector:
|
|
119
|
+
child_type = _get_datatype(annotation)
|
|
120
|
+
child_origin = get_origin_class(child_type)
|
|
121
|
+
if child_origin is None or not issubclass(child_origin, Device):
|
|
122
|
+
self._raise(
|
|
123
|
+
name,
|
|
124
|
+
f"Expected DeviceVector[SomeDevice], got {annotation}",
|
|
125
|
+
)
|
|
126
|
+
if issubclass(child_origin, Signal):
|
|
127
|
+
self._store_signal_datatype(name, child_type)
|
|
128
|
+
self._vector_device_type[_logical(name)] = child_origin
|
|
129
|
+
setattr(self._device, name, DeviceVector({}))
|
|
130
|
+
else:
|
|
131
|
+
self._uncreated_devices[name] = origin
|
|
132
|
+
|
|
133
|
+
def check_created(self):
|
|
134
|
+
uncreated = sorted(set(self._uncreated_signals).union(self._uncreated_devices))
|
|
135
|
+
if uncreated:
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
f"{self._device.name}: {uncreated} have not been created yet"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def create_signals_from_annotations(
|
|
141
|
+
self,
|
|
142
|
+
filled=True,
|
|
143
|
+
) -> Iterator[tuple[SignalBackendT, list[Any]]]:
|
|
144
|
+
for name in list(self._uncreated_signals):
|
|
145
|
+
child_type = self._uncreated_signals.pop(name)
|
|
146
|
+
backend = self._signal_backend_factory(
|
|
147
|
+
self._signal_datatype[_logical(name)]
|
|
148
|
+
)
|
|
149
|
+
extras = list(self._extras[name])
|
|
150
|
+
yield backend, extras
|
|
151
|
+
signal = child_type(backend)
|
|
152
|
+
for anno in extras:
|
|
153
|
+
assert isinstance(anno, DeviceAnnotation), anno
|
|
154
|
+
anno(self._device, signal)
|
|
155
|
+
setattr(self._device, name, signal)
|
|
156
|
+
dest = self._filled_backends if filled else self._unfilled_backends
|
|
157
|
+
dest[_logical(name)] = (backend, child_type)
|
|
158
|
+
|
|
159
|
+
def create_devices_from_annotations(
|
|
160
|
+
self,
|
|
161
|
+
filled=True,
|
|
162
|
+
) -> Iterator[tuple[DeviceConnectorT, list[Any]]]:
|
|
163
|
+
for name in list(self._uncreated_devices):
|
|
164
|
+
child_type = self._uncreated_devices.pop(name)
|
|
165
|
+
connector = self._device_connector_factory()
|
|
166
|
+
extras = list(self._extras[name])
|
|
167
|
+
yield connector, extras
|
|
168
|
+
device = child_type(connector=connector)
|
|
169
|
+
for anno in extras:
|
|
170
|
+
assert isinstance(anno, DeviceAnnotation), anno
|
|
171
|
+
anno(self._device, device)
|
|
172
|
+
setattr(self._device, name, device)
|
|
173
|
+
dest = self._filled_connectors if filled else self._unfilled_connectors
|
|
174
|
+
dest[_logical(name)] = connector
|
|
175
|
+
|
|
176
|
+
def create_device_vector_entries_to_mock(self, num: int):
|
|
177
|
+
for name, cls in self._vector_device_type.items():
|
|
178
|
+
assert cls, "Shouldn't happen"
|
|
179
|
+
for i in range(1, num + 1):
|
|
180
|
+
if issubclass(cls, Signal):
|
|
181
|
+
self.fill_child_signal(name, cls, i)
|
|
182
|
+
elif issubclass(cls, Device):
|
|
183
|
+
self.fill_child_device(name, cls, i)
|
|
184
|
+
else:
|
|
185
|
+
self._raise(name, f"Can't make {cls}")
|
|
186
|
+
|
|
187
|
+
def check_filled(self, source: str):
|
|
188
|
+
unfilled = sorted(set(self._unfilled_connectors).union(self._unfilled_backends))
|
|
189
|
+
if unfilled:
|
|
190
|
+
raise RuntimeError(
|
|
191
|
+
f"{self._device.name}: cannot provision {unfilled} from {source}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def _ensure_device_vector(self, name: LogicalName) -> DeviceVector:
|
|
195
|
+
if not hasattr(self._device, name):
|
|
196
|
+
# We have no type hints, so use whatever we are told
|
|
197
|
+
self._vector_device_type[name] = None
|
|
198
|
+
setattr(self._device, name, DeviceVector({}))
|
|
199
|
+
vector = getattr(self._device, name)
|
|
200
|
+
if not isinstance(vector, DeviceVector):
|
|
201
|
+
self._raise(name, f"Expected DeviceVector, got {vector}")
|
|
202
|
+
return vector
|
|
203
|
+
|
|
204
|
+
def fill_child_signal(
|
|
205
|
+
self,
|
|
206
|
+
name: str,
|
|
207
|
+
signal_type: type[Signal],
|
|
208
|
+
vector_index: int | None = None,
|
|
209
|
+
) -> SignalBackendT:
|
|
210
|
+
name = cast(LogicalName, name)
|
|
211
|
+
if name in self._unfilled_backends:
|
|
212
|
+
# We made it above
|
|
213
|
+
backend, expected_signal_type = self._unfilled_backends.pop(name)
|
|
214
|
+
self._filled_backends[name] = backend, expected_signal_type
|
|
215
|
+
elif name in self._filled_backends:
|
|
216
|
+
# We made it and filled it so return for validation
|
|
217
|
+
backend, expected_signal_type = self._filled_backends[name]
|
|
218
|
+
elif vector_index:
|
|
219
|
+
# We need to add a new entry to a DeviceVector
|
|
220
|
+
vector = self._ensure_device_vector(name)
|
|
221
|
+
backend = self._signal_backend_factory(self._signal_datatype.get(name))
|
|
222
|
+
expected_signal_type = self._vector_device_type[name] or signal_type
|
|
223
|
+
vector[vector_index] = signal_type(backend)
|
|
224
|
+
elif child := getattr(self._device, name, None):
|
|
225
|
+
# There is an existing child, so raise
|
|
226
|
+
self._raise(name, f"Cannot make child as it would shadow {child}")
|
|
227
|
+
else:
|
|
228
|
+
# We need to add a new child to the top level Device
|
|
229
|
+
backend = self._signal_backend_factory(None)
|
|
230
|
+
expected_signal_type = signal_type
|
|
231
|
+
setattr(self._device, name, signal_type(backend))
|
|
232
|
+
if signal_type is not expected_signal_type:
|
|
233
|
+
self._raise(
|
|
234
|
+
name,
|
|
235
|
+
f"is a {signal_type.__name__} not a {expected_signal_type.__name__}",
|
|
236
|
+
)
|
|
237
|
+
return backend
|
|
238
|
+
|
|
239
|
+
def fill_child_device(
|
|
240
|
+
self,
|
|
241
|
+
name: str,
|
|
242
|
+
device_type: type[Device] = Device,
|
|
243
|
+
vector_index: int | None = None,
|
|
244
|
+
) -> DeviceConnectorT:
|
|
245
|
+
name = cast(LogicalName, name)
|
|
246
|
+
if name in self._unfilled_connectors:
|
|
247
|
+
# We made it above
|
|
248
|
+
connector = self._unfilled_connectors.pop(name)
|
|
249
|
+
self._filled_connectors[name] = connector
|
|
250
|
+
elif name in self._filled_backends:
|
|
251
|
+
# We made it and filled it so return for validation
|
|
252
|
+
connector = self._filled_connectors[name]
|
|
253
|
+
elif vector_index:
|
|
254
|
+
# We need to add a new entry to a DeviceVector
|
|
255
|
+
vector = self._ensure_device_vector(name)
|
|
256
|
+
vector_device_type = self._vector_device_type[name] or device_type
|
|
257
|
+
assert issubclass(
|
|
258
|
+
vector_device_type, Device
|
|
259
|
+
), f"{vector_device_type} is not a Device"
|
|
260
|
+
connector = self._device_connector_factory()
|
|
261
|
+
vector[vector_index] = vector_device_type(connector=connector)
|
|
262
|
+
elif child := getattr(self._device, name, None):
|
|
263
|
+
# There is an existing child, so raise
|
|
264
|
+
self._raise(name, f"Cannot make child as it would shadow {child}")
|
|
265
|
+
else:
|
|
266
|
+
# We need to add a new child to the top level Device
|
|
267
|
+
connector = self._device_connector_factory()
|
|
268
|
+
setattr(self._device, name, device_type(connector=connector))
|
|
269
|
+
return connector
|
|
@@ -27,11 +27,8 @@ def pydantic_model_abstraction_representer(
|
|
|
27
27
|
return dumper.represent_data(model.model_dump(mode="python"))
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if isinstance(data, Enum):
|
|
33
|
-
return self.represent_data(data.value)
|
|
34
|
-
return super().represent_data(data)
|
|
30
|
+
def enum_representer(dumper: yaml.Dumper, enum: Enum) -> yaml.Node:
|
|
31
|
+
return dumper.represent_data(enum.value)
|
|
35
32
|
|
|
36
33
|
|
|
37
34
|
def get_signal_values(
|
|
@@ -74,7 +71,7 @@ def get_signal_values(
|
|
|
74
71
|
for key, value in zip(selected_signals, selected_values, strict=False)
|
|
75
72
|
}
|
|
76
73
|
# Ignored values place in with value None so we know which ones were ignored
|
|
77
|
-
named_values.update(
|
|
74
|
+
named_values.update(dict.fromkeys(ignore))
|
|
78
75
|
return named_values
|
|
79
76
|
|
|
80
77
|
|
|
@@ -111,6 +108,7 @@ def walk_rw_signals(
|
|
|
111
108
|
path_prefix = ""
|
|
112
109
|
|
|
113
110
|
signals: dict[str, SignalRW[Any]] = {}
|
|
111
|
+
|
|
114
112
|
for attr_name, attr in device.children():
|
|
115
113
|
dot_path = f"{path_prefix}{attr_name}"
|
|
116
114
|
if type(attr) is SignalRW:
|
|
@@ -145,9 +143,10 @@ def save_to_yaml(phases: Sequence[dict[str, Any]], save_path: str | Path) -> Non
|
|
|
145
143
|
pydantic_model_abstraction_representer,
|
|
146
144
|
Dumper=yaml.Dumper,
|
|
147
145
|
)
|
|
146
|
+
yaml.add_multi_representer(Enum, enum_representer, Dumper=yaml.Dumper)
|
|
148
147
|
|
|
149
148
|
with open(save_path, "w") as file:
|
|
150
|
-
yaml.dump(phases, file
|
|
149
|
+
yaml.dump(phases, file)
|
|
151
150
|
|
|
152
151
|
|
|
153
152
|
def load_from_yaml(save_path: str) -> Sequence[dict[str, Any]]:
|
|
@@ -5,51 +5,51 @@ from unittest.mock import AsyncMock
|
|
|
5
5
|
|
|
6
6
|
from bluesky.protocols import Descriptor, Reading
|
|
7
7
|
|
|
8
|
-
from ._signal_backend import SignalBackend
|
|
8
|
+
from ._signal_backend import SignalBackend, SignalDatatypeT
|
|
9
9
|
from ._soft_signal_backend import SoftSignalBackend
|
|
10
|
-
from ._utils import
|
|
10
|
+
from ._utils import Callback, LazyMock
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class MockSignalBackend(SignalBackend[
|
|
13
|
+
class MockSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
14
14
|
"""Signal backend for testing, created by ``Device.connect(mock=True)``."""
|
|
15
15
|
|
|
16
16
|
def __init__(
|
|
17
17
|
self,
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
initial_backend: SignalBackend[SignalDatatypeT],
|
|
19
|
+
mock: LazyMock,
|
|
20
20
|
) -> None:
|
|
21
21
|
if isinstance(initial_backend, MockSignalBackend):
|
|
22
|
-
raise ValueError("Cannot make a MockSignalBackend for a
|
|
22
|
+
raise ValueError("Cannot make a MockSignalBackend for a MockSignalBackend")
|
|
23
23
|
|
|
24
24
|
self.initial_backend = initial_backend
|
|
25
25
|
|
|
26
|
-
if
|
|
27
|
-
|
|
28
|
-
self.initial_backend
|
|
29
|
-
), "Must supply either initial_backend or datatype"
|
|
30
|
-
datatype = self.initial_backend.datatype
|
|
31
|
-
|
|
32
|
-
self.datatype = datatype
|
|
33
|
-
|
|
34
|
-
if not isinstance(self.initial_backend, SoftSignalBackend):
|
|
35
|
-
# If the backend is a hard signal backend, or not provided,
|
|
36
|
-
# then we create a soft signal to mimic it
|
|
37
|
-
|
|
38
|
-
self.soft_backend = SoftSignalBackend(datatype=datatype)
|
|
39
|
-
else:
|
|
26
|
+
if isinstance(self.initial_backend, SoftSignalBackend):
|
|
27
|
+
# Backend is already a SoftSignalBackend, so use it
|
|
40
28
|
self.soft_backend = self.initial_backend
|
|
29
|
+
else:
|
|
30
|
+
# Backend is not a SoftSignalBackend, so create one to mimic it
|
|
31
|
+
self.soft_backend = SoftSignalBackend(
|
|
32
|
+
datatype=self.initial_backend.datatype
|
|
33
|
+
)
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return f"mock+{name}"
|
|
46
|
-
|
|
47
|
-
async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
|
|
48
|
-
pass
|
|
35
|
+
# use existing Mock if provided
|
|
36
|
+
self.mock = mock
|
|
37
|
+
super().__init__(datatype=self.initial_backend.datatype)
|
|
49
38
|
|
|
50
39
|
@cached_property
|
|
51
40
|
def put_mock(self) -> AsyncMock:
|
|
52
|
-
|
|
41
|
+
put_mock = AsyncMock(name="put", spec=Callable)
|
|
42
|
+
self.mock().attach_mock(put_mock, "put")
|
|
43
|
+
return put_mock
|
|
44
|
+
|
|
45
|
+
def set_value(self, value: SignalDatatypeT):
|
|
46
|
+
self.soft_backend.set_value(value)
|
|
47
|
+
|
|
48
|
+
def source(self, name: str, read: bool) -> str:
|
|
49
|
+
return f"mock+{self.initial_backend.source(name, read)}"
|
|
50
|
+
|
|
51
|
+
async def connect(self, timeout: float) -> None:
|
|
52
|
+
raise RuntimeError("It is not possible to connect a MockSignalBackend")
|
|
53
53
|
|
|
54
54
|
@cached_property
|
|
55
55
|
def put_proceeds(self) -> asyncio.Event:
|
|
@@ -57,28 +57,23 @@ class MockSignalBackend(SignalBackend[T]):
|
|
|
57
57
|
put_proceeds.set()
|
|
58
58
|
return put_proceeds
|
|
59
59
|
|
|
60
|
-
async def put(self, value:
|
|
61
|
-
await self.put_mock(value, wait=wait
|
|
62
|
-
await self.soft_backend.put(value, wait=wait
|
|
63
|
-
|
|
60
|
+
async def put(self, value: SignalDatatypeT | None, wait: bool):
|
|
61
|
+
await self.put_mock(value, wait=wait)
|
|
62
|
+
await self.soft_backend.put(value, wait=wait)
|
|
64
63
|
if wait:
|
|
65
|
-
await
|
|
66
|
-
|
|
67
|
-
def set_value(self, value: T):
|
|
68
|
-
self.soft_backend.set_value(value)
|
|
64
|
+
await self.put_proceeds.wait()
|
|
69
65
|
|
|
70
66
|
async def get_reading(self) -> Reading:
|
|
71
67
|
return await self.soft_backend.get_reading()
|
|
72
68
|
|
|
73
|
-
async def get_value(self) ->
|
|
69
|
+
async def get_value(self) -> SignalDatatypeT:
|
|
74
70
|
return await self.soft_backend.get_value()
|
|
75
71
|
|
|
76
|
-
async def get_setpoint(self) ->
|
|
77
|
-
"""For a soft signal, the setpoint and readback values are the same."""
|
|
72
|
+
async def get_setpoint(self) -> SignalDatatypeT:
|
|
78
73
|
return await self.soft_backend.get_setpoint()
|
|
79
74
|
|
|
80
75
|
async def get_datakey(self, source: str) -> Descriptor:
|
|
81
76
|
return await self.soft_backend.get_datakey(source)
|
|
82
77
|
|
|
83
|
-
def set_callback(self, callback:
|
|
78
|
+
def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
|
|
84
79
|
self.soft_backend.set_callback(callback)
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
from collections.abc import Awaitable, Callable, Iterable
|
|
2
2
|
from contextlib import asynccontextmanager, contextmanager
|
|
3
|
-
from
|
|
4
|
-
from unittest.mock import AsyncMock
|
|
3
|
+
from unittest.mock import AsyncMock, Mock
|
|
5
4
|
|
|
5
|
+
from ._device import Device
|
|
6
6
|
from ._mock_signal_backend import MockSignalBackend
|
|
7
|
-
from ._signal import Signal
|
|
8
|
-
from .
|
|
7
|
+
from ._signal import Signal, SignalConnector, SignalR
|
|
8
|
+
from ._soft_signal_backend import SignalDatatypeT
|
|
9
|
+
from ._utils import LazyMock
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_mock(device: Device | Signal) -> Mock:
|
|
13
|
+
mock = device._mock # noqa: SLF001
|
|
14
|
+
assert isinstance(mock, LazyMock), f"Device {device} not connected in mock mode"
|
|
15
|
+
return mock()
|
|
9
16
|
|
|
10
17
|
|
|
11
18
|
def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend:
|
|
12
|
-
|
|
13
|
-
assert isinstance(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
)
|
|
17
|
-
return backend
|
|
19
|
+
connector = signal._connector # noqa: SLF001
|
|
20
|
+
assert isinstance(connector, SignalConnector), f"Expected Signal, got {signal}"
|
|
21
|
+
assert isinstance(
|
|
22
|
+
connector.backend, MockSignalBackend
|
|
23
|
+
), f"Signal {signal} not connected in mock mode"
|
|
24
|
+
return connector.backend
|
|
18
25
|
|
|
19
26
|
|
|
20
|
-
def set_mock_value(signal: Signal[
|
|
27
|
+
def set_mock_value(signal: Signal[SignalDatatypeT], value: SignalDatatypeT):
|
|
21
28
|
"""Set the value of a signal that is in mock mode."""
|
|
22
29
|
backend = _get_mock_signal_backend(signal)
|
|
23
30
|
backend.set_value(value)
|
|
@@ -59,8 +66,8 @@ class _SetValuesIterator:
|
|
|
59
66
|
|
|
60
67
|
def __init__(
|
|
61
68
|
self,
|
|
62
|
-
signal:
|
|
63
|
-
values: Iterable[
|
|
69
|
+
signal: SignalR[SignalDatatypeT],
|
|
70
|
+
values: Iterable[SignalDatatypeT],
|
|
64
71
|
require_all_consumed: bool = False,
|
|
65
72
|
):
|
|
66
73
|
self.signal = signal
|
|
@@ -99,8 +106,8 @@ class _SetValuesIterator:
|
|
|
99
106
|
|
|
100
107
|
|
|
101
108
|
def set_mock_values(
|
|
102
|
-
signal:
|
|
103
|
-
values: Iterable[
|
|
109
|
+
signal: SignalR[SignalDatatypeT],
|
|
110
|
+
values: Iterable[SignalDatatypeT],
|
|
104
111
|
require_all_consumed: bool = False,
|
|
105
112
|
) -> _SetValuesIterator:
|
|
106
113
|
"""Iterator to set a signal to a sequence of values, optionally repeating the
|
|
@@ -143,7 +150,9 @@ def _unset_side_effect_cm(put_mock: AsyncMock):
|
|
|
143
150
|
|
|
144
151
|
|
|
145
152
|
def callback_on_mock_put(
|
|
146
|
-
signal: Signal[
|
|
153
|
+
signal: Signal[SignalDatatypeT],
|
|
154
|
+
callback: Callable[[SignalDatatypeT, bool], None]
|
|
155
|
+
| Callable[[SignalDatatypeT, bool], Awaitable[None]],
|
|
147
156
|
):
|
|
148
157
|
"""For setting a callback when a backend is put to.
|
|
149
158
|
|
ophyd_async/core/_protocol.py
CHANGED
|
@@ -13,10 +13,38 @@ from typing import (
|
|
|
13
13
|
from bluesky.protocols import HasName, Reading
|
|
14
14
|
from event_model import DataKey
|
|
15
15
|
|
|
16
|
+
from ._utils import DEFAULT_TIMEOUT
|
|
17
|
+
|
|
16
18
|
if TYPE_CHECKING:
|
|
19
|
+
from unittest.mock import Mock
|
|
20
|
+
|
|
17
21
|
from ._status import AsyncStatus
|
|
18
22
|
|
|
19
23
|
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class Connectable(Protocol):
|
|
26
|
+
@abstractmethod
|
|
27
|
+
async def connect(
|
|
28
|
+
self,
|
|
29
|
+
mock: bool | Mock = False,
|
|
30
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
31
|
+
force_reconnect: bool = False,
|
|
32
|
+
):
|
|
33
|
+
"""Connect self and all child Devices.
|
|
34
|
+
|
|
35
|
+
Contains a timeout that gets propagated to child.connect methods.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
mock:
|
|
40
|
+
If True then use ``MockSignalBackend`` for all Signals
|
|
41
|
+
timeout:
|
|
42
|
+
Time to wait before failing with a TimeoutError.
|
|
43
|
+
force_reconnect:
|
|
44
|
+
Reconnect even if previous connect was successful.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
20
48
|
@runtime_checkable
|
|
21
49
|
class AsyncReadable(HasName, Protocol):
|
|
22
50
|
@abstractmethod
|
|
@@ -33,7 +61,6 @@ class AsyncReadable(HasName, Protocol):
|
|
|
33
61
|
('channel2',
|
|
34
62
|
{'value': 16, 'timestamp': 1472493713.539238}))
|
|
35
63
|
"""
|
|
36
|
-
...
|
|
37
64
|
|
|
38
65
|
@abstractmethod
|
|
39
66
|
async def describe(self) -> dict[str, DataKey]:
|
|
@@ -53,7 +80,6 @@ class AsyncReadable(HasName, Protocol):
|
|
|
53
80
|
'dtype': 'number',
|
|
54
81
|
'shape': []}))
|
|
55
82
|
"""
|
|
56
|
-
...
|
|
57
83
|
|
|
58
84
|
|
|
59
85
|
@runtime_checkable
|
|
@@ -63,14 +89,12 @@ class AsyncConfigurable(HasName, Protocol):
|
|
|
63
89
|
"""Same API as ``read`` but for slow-changing fields related to configuration.
|
|
64
90
|
e.g., exposure time. These will typically be read only once per run.
|
|
65
91
|
"""
|
|
66
|
-
...
|
|
67
92
|
|
|
68
93
|
@abstractmethod
|
|
69
94
|
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
70
95
|
"""Same API as ``describe``, but corresponding to the keys in
|
|
71
96
|
``read_configuration``.
|
|
72
97
|
"""
|
|
73
|
-
...
|
|
74
98
|
|
|
75
99
|
|
|
76
100
|
@runtime_checkable
|
|
@@ -78,12 +102,10 @@ class AsyncPausable(Protocol):
|
|
|
78
102
|
@abstractmethod
|
|
79
103
|
async def pause(self) -> None:
|
|
80
104
|
"""Perform device-specific work when the RunEngine pauses."""
|
|
81
|
-
...
|
|
82
105
|
|
|
83
106
|
@abstractmethod
|
|
84
107
|
async def resume(self) -> None:
|
|
85
108
|
"""Perform device-specific work when the RunEngine resumes after a pause."""
|
|
86
|
-
...
|
|
87
109
|
|
|
88
110
|
|
|
89
111
|
@runtime_checkable
|
|
@@ -95,7 +117,6 @@ class AsyncStageable(Protocol):
|
|
|
95
117
|
It should return a ``Status`` that is marked done when the device is
|
|
96
118
|
done staging.
|
|
97
119
|
"""
|
|
98
|
-
...
|
|
99
120
|
|
|
100
121
|
@abstractmethod
|
|
101
122
|
def unstage(self) -> AsyncStatus:
|
|
@@ -104,7 +125,6 @@ class AsyncStageable(Protocol):
|
|
|
104
125
|
It should return a ``Status`` that is marked done when the device is finished
|
|
105
126
|
unstaging.
|
|
106
127
|
"""
|
|
107
|
-
...
|
|
108
128
|
|
|
109
129
|
|
|
110
130
|
C = TypeVar("C", contravariant=True)
|