ophyd-async 0.7.0a1__py3-none-any.whl → 0.8.0a3__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.
Files changed (83) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +30 -9
  3. ophyd_async/core/_detector.py +5 -10
  4. ophyd_async/core/_device.py +146 -67
  5. ophyd_async/core/_device_filler.py +269 -0
  6. ophyd_async/core/_device_save_loader.py +6 -7
  7. ophyd_async/core/_mock_signal_backend.py +32 -40
  8. ophyd_async/core/_mock_signal_utils.py +22 -16
  9. ophyd_async/core/_protocol.py +28 -8
  10. ophyd_async/core/_readable.py +133 -134
  11. ophyd_async/core/_signal.py +140 -152
  12. ophyd_async/core/_signal_backend.py +131 -64
  13. ophyd_async/core/_soft_signal_backend.py +125 -194
  14. ophyd_async/core/_status.py +22 -6
  15. ophyd_async/core/_table.py +97 -100
  16. ophyd_async/core/_utils.py +79 -18
  17. ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
  18. ophyd_async/epics/adaravis/_aravis_io.py +8 -6
  19. ophyd_async/epics/adcore/_core_io.py +5 -7
  20. ophyd_async/epics/adcore/_hdf_writer.py +2 -2
  21. ophyd_async/epics/adcore/_single_trigger.py +4 -9
  22. ophyd_async/epics/adcore/_utils.py +15 -10
  23. ophyd_async/epics/adkinetix/__init__.py +2 -1
  24. ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
  25. ophyd_async/epics/adkinetix/_kinetix_io.py +4 -5
  26. ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
  27. ophyd_async/epics/adpilatus/_pilatus_io.py +3 -4
  28. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  29. ophyd_async/epics/advimba/__init__.py +4 -1
  30. ophyd_async/epics/advimba/_vimba_controller.py +6 -3
  31. ophyd_async/epics/advimba/_vimba_io.py +8 -9
  32. ophyd_async/epics/core/__init__.py +26 -0
  33. ophyd_async/epics/core/_aioca.py +323 -0
  34. ophyd_async/epics/core/_epics_connector.py +53 -0
  35. ophyd_async/epics/core/_epics_device.py +13 -0
  36. ophyd_async/epics/core/_p4p.py +382 -0
  37. ophyd_async/epics/core/_pvi_connector.py +92 -0
  38. ophyd_async/epics/core/_signal.py +171 -0
  39. ophyd_async/epics/core/_util.py +61 -0
  40. ophyd_async/epics/demo/_mover.py +4 -5
  41. ophyd_async/epics/demo/_sensor.py +14 -13
  42. ophyd_async/epics/eiger/_eiger.py +1 -2
  43. ophyd_async/epics/eiger/_eiger_controller.py +1 -1
  44. ophyd_async/epics/eiger/_eiger_io.py +3 -5
  45. ophyd_async/epics/eiger/_odin_io.py +5 -5
  46. ophyd_async/epics/motor.py +4 -5
  47. ophyd_async/epics/signal.py +11 -0
  48. ophyd_async/fastcs/core.py +9 -0
  49. ophyd_async/fastcs/panda/__init__.py +4 -4
  50. ophyd_async/fastcs/panda/_block.py +23 -11
  51. ophyd_async/fastcs/panda/_control.py +3 -5
  52. ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
  53. ophyd_async/fastcs/panda/_table.py +29 -51
  54. ophyd_async/fastcs/panda/_trigger.py +8 -8
  55. ophyd_async/fastcs/panda/_writer.py +4 -7
  56. ophyd_async/plan_stubs/_ensure_connected.py +3 -1
  57. ophyd_async/plan_stubs/_fly.py +2 -2
  58. ophyd_async/plan_stubs/_nd_attributes.py +5 -4
  59. ophyd_async/py.typed +0 -0
  60. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
  61. ophyd_async/sim/demo/_sim_motor.py +3 -4
  62. ophyd_async/tango/__init__.py +2 -4
  63. ophyd_async/tango/base_devices/_base_device.py +76 -144
  64. ophyd_async/tango/demo/_counter.py +8 -18
  65. ophyd_async/tango/demo/_mover.py +5 -6
  66. ophyd_async/tango/signal/__init__.py +2 -4
  67. ophyd_async/tango/signal/_signal.py +29 -50
  68. ophyd_async/tango/signal/_tango_transport.py +38 -40
  69. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/METADATA +8 -12
  70. ophyd_async-0.8.0a3.dist-info/RECORD +112 -0
  71. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/WHEEL +1 -1
  72. ophyd_async/epics/pvi/__init__.py +0 -3
  73. ophyd_async/epics/pvi/_pvi.py +0 -338
  74. ophyd_async/epics/signal/__init__.py +0 -21
  75. ophyd_async/epics/signal/_aioca.py +0 -378
  76. ophyd_async/epics/signal/_common.py +0 -57
  77. ophyd_async/epics/signal/_epics_transport.py +0 -34
  78. ophyd_async/epics/signal/_p4p.py +0 -518
  79. ophyd_async/epics/signal/_signal.py +0 -114
  80. ophyd_async-0.7.0a1.dist-info/RECORD +0 -108
  81. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/LICENSE +0 -0
  82. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/entry_points.txt +0 -0
  83. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.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
- class OphydDumper(yaml.Dumper):
31
- def represent_data(self, data: Any) -> Any:
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({key: None for key in ignore})
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, Dumper=OphydDumper, default_flow_style=False)
149
+ yaml.dump(phases, file)
151
150
 
152
151
 
153
152
  def load_from_yaml(save_path: str) -> Sequence[dict[str, Any]]:
@@ -1,84 +1,76 @@
1
1
  import asyncio
2
2
  from collections.abc import Callable
3
3
  from functools import cached_property
4
- from unittest.mock import AsyncMock
4
+ from unittest.mock import AsyncMock, Mock
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 DEFAULT_TIMEOUT, ReadingValueCallback, T
10
+ from ._utils import Callback
11
11
 
12
12
 
13
- class MockSignalBackend(SignalBackend[T]):
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
- datatype: type[T] | None = None,
19
- initial_backend: SignalBackend[T] | None = None,
18
+ initial_backend: SignalBackend[SignalDatatypeT],
19
+ mock: Mock,
20
20
  ) -> None:
21
21
  if isinstance(initial_backend, MockSignalBackend):
22
- raise ValueError("Cannot make a MockSignalBackend for a MockSignalBackends")
22
+ raise ValueError("Cannot make a MockSignalBackend for a MockSignalBackend")
23
23
 
24
24
  self.initial_backend = initial_backend
25
25
 
26
- if datatype is None:
27
- assert (
28
- self.initial_backend
29
- ), "Must supply either initial_backend or datatype"
30
- datatype = self.initial_backend.datatype
26
+ if isinstance(self.initial_backend, SoftSignalBackend):
27
+ # Backend is already a SoftSignalBackend, so use it
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
+ )
31
34
 
32
- self.datatype = datatype
35
+ # use existing Mock if provided
36
+ self.mock = mock
37
+ self.put_mock = AsyncMock(name="put", spec=Callable)
38
+ self.mock.attach_mock(self.put_mock, "put")
33
39
 
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
40
+ super().__init__(datatype=self.initial_backend.datatype)
37
41
 
38
- self.soft_backend = SoftSignalBackend(datatype=datatype)
39
- else:
40
- self.soft_backend = self.initial_backend
42
+ def set_value(self, value: SignalDatatypeT):
43
+ self.soft_backend.set_value(value)
41
44
 
42
- def source(self, name: str) -> str:
43
- if self.initial_backend:
44
- return f"mock+{self.initial_backend.source(name)}"
45
- return f"mock+{name}"
45
+ def source(self, name: str, read: bool) -> str:
46
+ return f"mock+{self.initial_backend.source(name, read)}"
46
47
 
47
- async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
48
+ async def connect(self, timeout: float) -> None:
48
49
  pass
49
50
 
50
- @cached_property
51
- def put_mock(self) -> AsyncMock:
52
- return AsyncMock(name="put", spec=Callable)
53
-
54
51
  @cached_property
55
52
  def put_proceeds(self) -> asyncio.Event:
56
53
  put_proceeds = asyncio.Event()
57
54
  put_proceeds.set()
58
55
  return put_proceeds
59
56
 
60
- async def put(self, value: T | None, wait=True, timeout=None):
61
- await self.put_mock(value, wait=wait, timeout=timeout)
62
- await self.soft_backend.put(value, wait=wait, timeout=timeout)
63
-
57
+ async def put(self, value: SignalDatatypeT | None, wait: bool):
58
+ await self.put_mock(value, wait=wait)
59
+ await self.soft_backend.put(value, wait=wait)
64
60
  if wait:
65
- await asyncio.wait_for(self.put_proceeds.wait(), timeout=timeout)
66
-
67
- def set_value(self, value: T):
68
- self.soft_backend.set_value(value)
61
+ await self.put_proceeds.wait()
69
62
 
70
63
  async def get_reading(self) -> Reading:
71
64
  return await self.soft_backend.get_reading()
72
65
 
73
- async def get_value(self) -> T:
66
+ async def get_value(self) -> SignalDatatypeT:
74
67
  return await self.soft_backend.get_value()
75
68
 
76
- async def get_setpoint(self) -> T:
77
- """For a soft signal, the setpoint and readback values are the same."""
69
+ async def get_setpoint(self) -> SignalDatatypeT:
78
70
  return await self.soft_backend.get_setpoint()
79
71
 
80
72
  async def get_datakey(self, source: str) -> Descriptor:
81
73
  return await self.soft_backend.get_datakey(source)
82
74
 
83
- def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
75
+ def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
84
76
  self.soft_backend.set_callback(callback)
@@ -1,23 +1,21 @@
1
1
  from collections.abc import Awaitable, Callable, Iterable
2
2
  from contextlib import asynccontextmanager, contextmanager
3
- from typing import Any
4
- from unittest.mock import AsyncMock
3
+ from unittest.mock import AsyncMock, Mock
5
4
 
5
+ from ._device import Device, _device_mocks
6
6
  from ._mock_signal_backend import MockSignalBackend
7
- from ._signal import Signal
8
- from ._utils import T
7
+ from ._signal import Signal, SignalR, _mock_signal_backends
8
+ from ._soft_signal_backend import SignalDatatypeT
9
9
 
10
10
 
11
11
  def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend:
12
- backend = signal._backend # noqa:SLF001
13
- assert isinstance(backend, MockSignalBackend), (
14
- "Expected to receive a `MockSignalBackend`, instead "
15
- f" received {type(backend)}. "
16
- )
17
- return backend
12
+ assert (
13
+ signal in _mock_signal_backends
14
+ ), f"Signal {signal} not connected in mock mode"
15
+ return _mock_signal_backends[signal]
18
16
 
19
17
 
20
- def set_mock_value(signal: Signal[T], value: T):
18
+ def set_mock_value(signal: Signal[SignalDatatypeT], value: SignalDatatypeT):
21
19
  """Set the value of a signal that is in mock mode."""
22
20
  backend = _get_mock_signal_backend(signal)
23
21
  backend.set_value(value)
@@ -47,6 +45,12 @@ def get_mock_put(signal: Signal) -> AsyncMock:
47
45
  return _get_mock_signal_backend(signal).put_mock
48
46
 
49
47
 
48
+ def get_mock(device: Device | Signal) -> Mock:
49
+ if isinstance(device, Signal):
50
+ return _get_mock_signal_backend(device).mock
51
+ return _device_mocks[device]
52
+
53
+
50
54
  def reset_mock_put_calls(signal: Signal):
51
55
  backend = _get_mock_signal_backend(signal)
52
56
  backend.put_mock.reset_mock()
@@ -59,8 +63,8 @@ class _SetValuesIterator:
59
63
 
60
64
  def __init__(
61
65
  self,
62
- signal: Signal,
63
- values: Iterable[Any],
66
+ signal: SignalR[SignalDatatypeT],
67
+ values: Iterable[SignalDatatypeT],
64
68
  require_all_consumed: bool = False,
65
69
  ):
66
70
  self.signal = signal
@@ -99,8 +103,8 @@ class _SetValuesIterator:
99
103
 
100
104
 
101
105
  def set_mock_values(
102
- signal: Signal,
103
- values: Iterable[Any],
106
+ signal: SignalR[SignalDatatypeT],
107
+ values: Iterable[SignalDatatypeT],
104
108
  require_all_consumed: bool = False,
105
109
  ) -> _SetValuesIterator:
106
110
  """Iterator to set a signal to a sequence of values, optionally repeating the
@@ -143,7 +147,9 @@ def _unset_side_effect_cm(put_mock: AsyncMock):
143
147
 
144
148
 
145
149
  def callback_on_mock_put(
146
- signal: Signal[T], callback: Callable[[T], None] | Callable[[T], Awaitable[None]]
150
+ signal: Signal[SignalDatatypeT],
151
+ callback: Callable[[SignalDatatypeT, bool], None]
152
+ | Callable[[SignalDatatypeT, bool], Awaitable[None]],
147
153
  ):
148
154
  """For setting a callback when a backend is put to.
149
155
 
@@ -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)