ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__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 (157) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +102 -74
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +158 -153
  8. ophyd_async/core/_device.py +143 -115
  9. ophyd_async/core/_device_filler.py +82 -9
  10. ophyd_async/core/_flyer.py +16 -7
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +74 -58
  17. ophyd_async/core/_settings.py +113 -0
  18. ophyd_async/core/_signal.py +304 -174
  19. ophyd_async/core/_signal_backend.py +60 -14
  20. ophyd_async/core/_soft_signal_backend.py +18 -12
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +54 -17
  23. ophyd_async/core/_utils.py +101 -52
  24. ophyd_async/core/_yaml_settings.py +66 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/__init__.py +9 -0
  27. ophyd_async/epics/adandor/_andor.py +45 -0
  28. ophyd_async/epics/adandor/_andor_controller.py +51 -0
  29. ophyd_async/epics/adandor/_andor_io.py +34 -0
  30. ophyd_async/epics/adaravis/__init__.py +8 -1
  31. ophyd_async/epics/adaravis/_aravis.py +23 -41
  32. ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
  33. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  34. ophyd_async/epics/adcore/__init__.py +36 -14
  35. ophyd_async/epics/adcore/_core_detector.py +81 -0
  36. ophyd_async/epics/adcore/_core_io.py +145 -95
  37. ophyd_async/epics/adcore/_core_logic.py +179 -88
  38. ophyd_async/epics/adcore/_core_writer.py +223 -0
  39. ophyd_async/epics/adcore/_hdf_writer.py +51 -92
  40. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  41. ophyd_async/epics/adcore/_single_trigger.py +6 -5
  42. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  43. ophyd_async/epics/adcore/_utils.py +3 -2
  44. ophyd_async/epics/adkinetix/__init__.py +2 -1
  45. ophyd_async/epics/adkinetix/_kinetix.py +32 -27
  46. ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
  47. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  48. ophyd_async/epics/adpilatus/__init__.py +7 -2
  49. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  50. ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
  51. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  52. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  53. ophyd_async/epics/adsimdetector/_sim.py +22 -16
  54. ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
  55. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  56. ophyd_async/epics/advimba/__init__.py +10 -1
  57. ophyd_async/epics/advimba/_vimba.py +26 -25
  58. ophyd_async/epics/advimba/_vimba_controller.py +12 -24
  59. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  60. ophyd_async/epics/core/_aioca.py +66 -30
  61. ophyd_async/epics/core/_epics_connector.py +4 -0
  62. ophyd_async/epics/core/_epics_device.py +2 -0
  63. ophyd_async/epics/core/_p4p.py +50 -18
  64. ophyd_async/epics/core/_pvi_connector.py +65 -8
  65. ophyd_async/epics/core/_signal.py +51 -51
  66. ophyd_async/epics/core/_util.py +5 -5
  67. ophyd_async/epics/demo/__init__.py +11 -49
  68. ophyd_async/epics/demo/__main__.py +31 -0
  69. ophyd_async/epics/demo/_ioc.py +32 -0
  70. ophyd_async/epics/demo/_motor.py +82 -0
  71. ophyd_async/epics/demo/_point_detector.py +42 -0
  72. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  73. ophyd_async/epics/demo/_stage.py +15 -0
  74. ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
  75. ophyd_async/epics/demo/point_detector.db +59 -0
  76. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  77. ophyd_async/epics/eiger/_eiger.py +1 -3
  78. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  79. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  80. ophyd_async/epics/eiger/_odin_io.py +1 -2
  81. ophyd_async/epics/motor.py +83 -38
  82. ophyd_async/epics/signal.py +4 -1
  83. ophyd_async/epics/testing/__init__.py +14 -14
  84. ophyd_async/epics/testing/_example_ioc.py +68 -73
  85. ophyd_async/epics/testing/_utils.py +19 -44
  86. ophyd_async/epics/testing/test_records.db +16 -0
  87. ophyd_async/epics/testing/test_records_pva.db +17 -16
  88. ophyd_async/fastcs/__init__.py +1 -0
  89. ophyd_async/fastcs/core.py +6 -0
  90. ophyd_async/fastcs/odin/__init__.py +1 -0
  91. ophyd_async/fastcs/panda/__init__.py +8 -8
  92. ophyd_async/fastcs/panda/_block.py +29 -9
  93. ophyd_async/fastcs/panda/_control.py +12 -2
  94. ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
  95. ophyd_async/fastcs/panda/_table.py +13 -7
  96. ophyd_async/fastcs/panda/_trigger.py +23 -9
  97. ophyd_async/fastcs/panda/_writer.py +27 -30
  98. ophyd_async/plan_stubs/__init__.py +16 -0
  99. ophyd_async/plan_stubs/_ensure_connected.py +12 -17
  100. ophyd_async/plan_stubs/_fly.py +3 -5
  101. ophyd_async/plan_stubs/_nd_attributes.py +9 -5
  102. ophyd_async/plan_stubs/_panda.py +14 -0
  103. ophyd_async/plan_stubs/_settings.py +152 -0
  104. ophyd_async/plan_stubs/_utils.py +3 -0
  105. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  106. ophyd_async/sim/__init__.py +29 -0
  107. ophyd_async/sim/__main__.py +43 -0
  108. ophyd_async/sim/_blob_detector.py +33 -0
  109. ophyd_async/sim/_blob_detector_controller.py +48 -0
  110. ophyd_async/sim/_blob_detector_writer.py +105 -0
  111. ophyd_async/sim/_mirror_horizontal.py +46 -0
  112. ophyd_async/sim/_mirror_vertical.py +74 -0
  113. ophyd_async/sim/_motor.py +233 -0
  114. ophyd_async/sim/_pattern_generator.py +124 -0
  115. ophyd_async/sim/_point_detector.py +86 -0
  116. ophyd_async/sim/_stage.py +19 -0
  117. ophyd_async/tango/__init__.py +1 -0
  118. ophyd_async/tango/core/__init__.py +6 -1
  119. ophyd_async/tango/core/_base_device.py +41 -33
  120. ophyd_async/tango/core/_converters.py +81 -0
  121. ophyd_async/tango/core/_signal.py +21 -33
  122. ophyd_async/tango/core/_tango_readable.py +2 -19
  123. ophyd_async/tango/core/_tango_transport.py +148 -74
  124. ophyd_async/tango/core/_utils.py +47 -0
  125. ophyd_async/tango/demo/_counter.py +2 -0
  126. ophyd_async/tango/demo/_detector.py +2 -0
  127. ophyd_async/tango/demo/_mover.py +10 -6
  128. ophyd_async/tango/demo/_tango/_servers.py +4 -0
  129. ophyd_async/tango/testing/__init__.py +6 -0
  130. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  131. ophyd_async/testing/__init__.py +48 -7
  132. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  133. ophyd_async/testing/_assert.py +200 -96
  134. ophyd_async/testing/_mock_signal_utils.py +59 -73
  135. ophyd_async/testing/_one_of_everything.py +146 -0
  136. ophyd_async/testing/_single_derived.py +87 -0
  137. ophyd_async/testing/_utils.py +3 -0
  138. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
  139. ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
  140. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
  141. ophyd_async/core/_device_save_loader.py +0 -274
  142. ophyd_async/epics/demo/_mover.py +0 -95
  143. ophyd_async/epics/demo/_sensor.py +0 -37
  144. ophyd_async/epics/demo/sensor.db +0 -19
  145. ophyd_async/fastcs/panda/_utils.py +0 -16
  146. ophyd_async/sim/demo/__init__.py +0 -19
  147. ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
  148. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
  149. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
  150. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
  151. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
  152. ophyd_async/sim/demo/_sim_motor.py +0 -107
  153. ophyd_async/sim/testing/__init__.py +0 -0
  154. ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
  155. ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
  156. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
  157. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
@@ -2,14 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import functools
5
+ import inspect
5
6
  import time
6
7
  from collections.abc import AsyncGenerator, Awaitable, Callable
7
- from typing import Generic, cast
8
+ from typing import Any, Generic, TypeVar, cast
8
9
 
9
10
  from bluesky.protocols import (
11
+ Configurable,
10
12
  Locatable,
11
13
  Location,
12
14
  Movable,
15
+ Reading,
13
16
  Status,
14
17
  Subscribable,
15
18
  )
@@ -17,18 +20,10 @@ from event_model import DataKey
17
20
 
18
21
  from ._device import Device, DeviceConnector
19
22
  from ._mock_signal_backend import MockSignalBackend
20
- from ._protocol import (
21
- AsyncReadable,
22
- AsyncStageable,
23
- Reading,
24
- )
25
- from ._signal_backend import (
26
- SignalBackend,
27
- SignalDatatypeT,
28
- SignalDatatypeV,
29
- )
23
+ from ._protocol import AsyncReadable, AsyncStageable
24
+ from ._signal_backend import SignalBackend, SignalDatatypeT, SignalDatatypeV
30
25
  from ._soft_signal_backend import SoftSignalBackend
31
- from ._status import AsyncStatus, completed_status
26
+ from ._status import AsyncStatus
32
27
  from ._utils import (
33
28
  CALCULATE_TIMEOUT,
34
29
  DEFAULT_TIMEOUT,
@@ -55,6 +50,8 @@ def _add_timeout(func):
55
50
 
56
51
 
57
52
  class SignalConnector(DeviceConnector):
53
+ """Used for connecting signals with a given backend."""
54
+
58
55
  def __init__(self, backend: SignalBackend):
59
56
  self.backend = self._init_backend = backend
60
57
 
@@ -69,14 +66,19 @@ class SignalConnector(DeviceConnector):
69
66
 
70
67
  class _ChildrenNotAllowed(dict[str, Device]):
71
68
  def __setitem__(self, key: str, value: Device) -> None:
72
- raise AttributeError(
69
+ raise KeyError(
73
70
  f"Cannot add Device or Signal child {key}={value} of Signal, "
74
71
  "make a subclass of Device instead"
75
72
  )
76
73
 
77
74
 
78
75
  class Signal(Device, Generic[SignalDatatypeT]):
79
- """A Device with the concept of a value, with R, RW, W and X flavours"""
76
+ """A Device with the concept of a value, with R, RW, W and X flavours.
77
+
78
+ :param backend: The backend for providing Signal values.
79
+ :param timeout: The default timeout for operations on the Signal.
80
+ :param name: The name of the signal.
81
+ """
80
82
 
81
83
  _connector: SignalConnector
82
84
  _child_devices = _ChildrenNotAllowed() # type: ignore
@@ -92,35 +94,46 @@ class Signal(Device, Generic[SignalDatatypeT]):
92
94
 
93
95
  @property
94
96
  def source(self) -> str:
95
- """Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
97
+ """Returns the source of the signal.
98
+
99
+ E.g. "ca://PV_PREFIX:SIGNAL", or "" if not available until connection.
100
+ """
96
101
  return self._connector.backend.source(self.name, read=True)
97
102
 
98
103
 
104
+ SignalT = TypeVar("SignalT", bound=Signal)
105
+
106
+
99
107
  class _SignalCache(Generic[SignalDatatypeT]):
100
- def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal):
101
- self._signal = signal
108
+ def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal) -> None:
109
+ self._signal: Signal[Any] = signal
102
110
  self._staged = False
103
111
  self._listeners: dict[Callback, bool] = {}
104
112
  self._valid = asyncio.Event()
105
113
  self._reading: Reading[SignalDatatypeT] | None = None
106
- self.backend = backend
114
+ self.backend: SignalBackend[SignalDatatypeT] = backend
107
115
  signal.log.debug(f"Making subscription on source {signal.source}")
108
116
  backend.set_callback(self._callback)
109
117
 
110
- def close(self):
118
+ def close(self) -> None:
111
119
  self.backend.set_callback(None)
112
120
  self._signal.log.debug(f"Closing subscription on source {self._signal.source}")
113
121
 
122
+ def _ensure_reading(self) -> Reading[SignalDatatypeT]:
123
+ if not self._reading:
124
+ msg = "Monitor not working"
125
+ raise RuntimeError(msg)
126
+ return self._reading
127
+
114
128
  async def get_reading(self) -> Reading[SignalDatatypeT]:
115
129
  await self._valid.wait()
116
- assert self._reading is not None, "Monitor not working"
117
- return self._reading
130
+ return self._ensure_reading()
118
131
 
119
132
  async def get_value(self) -> SignalDatatypeT:
120
- reading = await self.get_reading()
133
+ reading: Reading[SignalDatatypeT] = await self.get_reading()
121
134
  return reading["value"]
122
135
 
123
- def _callback(self, reading: Reading[SignalDatatypeT]):
136
+ def _callback(self, reading: Reading[SignalDatatypeT]) -> None:
124
137
  self._signal.log.debug(
125
138
  f"Updated subscription: reading of source {self._signal.source} changed "
126
139
  f"from {self._reading} to {reading}"
@@ -134,12 +147,10 @@ class _SignalCache(Generic[SignalDatatypeT]):
134
147
  self,
135
148
  function: Callback[dict[str, Reading[SignalDatatypeT]] | SignalDatatypeT],
136
149
  want_value: bool,
137
- ):
138
- assert self._reading, "Monitor not working"
139
- if want_value:
140
- function(self._reading["value"])
141
- else:
142
- function({self._signal.name: self._reading})
150
+ ) -> None:
151
+ function(self._ensure_reading()["value"]) if want_value else function(
152
+ {self._signal.name: self._ensure_reading()}
153
+ )
143
154
 
144
155
  def subscribe(self, function: Callback, want_value: bool) -> None:
145
156
  self._listeners[function] = want_value
@@ -150,13 +161,13 @@ class _SignalCache(Generic[SignalDatatypeT]):
150
161
  self._listeners.pop(function)
151
162
  return self._staged or bool(self._listeners)
152
163
 
153
- def set_staged(self, staged: bool):
164
+ def set_staged(self, staged: bool) -> bool:
154
165
  self._staged = staged
155
166
  return self._staged or bool(self._listeners)
156
167
 
157
168
 
158
169
  class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribable):
159
- """Signal that can be read from and monitored"""
170
+ """Signal that can be read from and monitored."""
160
171
 
161
172
  _cache: _SignalCache | None = None
162
173
 
@@ -167,7 +178,10 @@ class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribab
167
178
  if cached is None:
168
179
  cached = self._cache is not None
169
180
  if cached:
170
- assert self._cache, f"{self.source} not being monitored"
181
+ if not self._cache:
182
+ msg = f"{self.source} not being monitored"
183
+ raise RuntimeError(msg)
184
+ # assert self._cache, f"{self.source} not being monitored"
171
185
  return self._cache
172
186
  else:
173
187
  return self._connector.backend
@@ -184,46 +198,71 @@ class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribab
184
198
 
185
199
  @_add_timeout
186
200
  async def read(self, cached: bool | None = None) -> dict[str, Reading]:
187
- """Return a single item dict with the reading in it"""
201
+ """Return a single item dict with the reading in it.
202
+
203
+ :param cached:
204
+ Whether to use the cached monitored value:
205
+ - If None, use the cache if it exists.
206
+ - If False, do an explicit get.
207
+ - If True, explicitly use the cache and raise an error if it doesn't exist.
208
+ """
188
209
  return {self.name: await self._backend_or_cache(cached).get_reading()}
189
210
 
190
211
  @_add_timeout
191
212
  async def describe(self) -> dict[str, DataKey]:
192
- """Return a single item dict with the descriptor in it"""
213
+ """Return a single item dict describing the signal value."""
193
214
  return {self.name: await self._connector.backend.get_datakey(self.source)}
194
215
 
195
216
  @_add_timeout
196
217
  async def get_value(self, cached: bool | None = None) -> SignalDatatypeT:
197
- """The current value"""
218
+ """Return the current value.
219
+
220
+ :param cached:
221
+ Whether to use the cached monitored value:
222
+ - If None, use the cache if it exists.
223
+ - If False, do an explicit get.
224
+ - If True, explicitly use the cache and raise an error if it doesn't exist.
225
+ """
198
226
  value = await self._backend_or_cache(cached).get_value()
199
227
  self.log.debug(f"get_value() on source {self.source} returned {value}")
200
228
  return value
201
229
 
202
230
  def subscribe_value(self, function: Callback[SignalDatatypeT]):
203
- """Subscribe to updates in value of a device"""
231
+ """Subscribe to updates in value of a device.
232
+
233
+ :param function: The callback function to call when the value changes.
234
+ """
204
235
  self._get_cache().subscribe(function, want_value=True)
205
236
 
206
- def subscribe(self, function: Callback[dict[str, Reading]]) -> None:
207
- """Subscribe to updates in the reading"""
237
+ def subscribe(
238
+ self, function: Callback[dict[str, Reading[SignalDatatypeT]]]
239
+ ) -> None:
240
+ """Subscribe to updates in the reading.
241
+
242
+ :param function: The callback function to call when the reading changes.
243
+ """
208
244
  self._get_cache().subscribe(function, want_value=False)
209
245
 
210
246
  def clear_sub(self, function: Callback) -> None:
211
- """Remove a subscription."""
247
+ """Remove a subscription passed to `subscribe` or `subscribe_value`.
248
+
249
+ :param function: The callback function to remove.
250
+ """
212
251
  self._del_cache(self._get_cache().unsubscribe(function))
213
252
 
214
253
  @AsyncStatus.wrap
215
254
  async def stage(self) -> None:
216
- """Start caching this signal"""
255
+ """Start caching this signal."""
217
256
  self._get_cache().set_staged(True)
218
257
 
219
258
  @AsyncStatus.wrap
220
259
  async def unstage(self) -> None:
221
- """Stop caching this signal"""
260
+ """Stop caching this signal."""
222
261
  self._del_cache(self._get_cache().set_staged(False))
223
262
 
224
263
 
225
264
  class SignalW(Signal[SignalDatatypeT], Movable):
226
- """Signal that can be set"""
265
+ """Signal that can be set."""
227
266
 
228
267
  @AsyncStatus.wrap
229
268
  async def set(
@@ -232,7 +271,12 @@ class SignalW(Signal[SignalDatatypeT], Movable):
232
271
  wait=True,
233
272
  timeout: CalculatableTimeout = CALCULATE_TIMEOUT,
234
273
  ) -> None:
235
- """Set the value and return a status saying when it's done"""
274
+ """Set the value and return a status saying when it's done.
275
+
276
+ :param value: The value to set.
277
+ :param wait: If True, wait for the set to complete.
278
+ :param timeout: The timeout for the set.
279
+ """
236
280
  if timeout == CALCULATE_TIMEOUT:
237
281
  timeout = self._timeout
238
282
  source = self._connector.backend.source(self.name, read=False)
@@ -242,7 +286,7 @@ class SignalW(Signal[SignalDatatypeT], Movable):
242
286
 
243
287
 
244
288
  class SignalRW(SignalR[SignalDatatypeT], SignalW[SignalDatatypeT], Locatable):
245
- """Signal that can be both read and set"""
289
+ """Signal that can be both read and set."""
246
290
 
247
291
  @_add_timeout
248
292
  async def locate(self) -> Location:
@@ -254,13 +298,17 @@ class SignalRW(SignalR[SignalDatatypeT], SignalW[SignalDatatypeT], Locatable):
254
298
 
255
299
 
256
300
  class SignalX(Signal):
257
- """Signal that puts the default value"""
301
+ """Signal that puts the default value."""
258
302
 
259
303
  @AsyncStatus.wrap
260
304
  async def trigger(
261
305
  self, wait=True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
262
306
  ) -> None:
263
- """Trigger the action and return a status saying when it's done"""
307
+ """Trigger the action and return a status saying when it's done.
308
+
309
+ :param wait: If True, wait for the trigger to complete.
310
+ :param timeout: The timeout for the trigger.
311
+ """
264
312
  if timeout == CALCULATE_TIMEOUT:
265
313
  timeout = self._timeout
266
314
  source = self._connector.backend.source(self.name, read=False)
@@ -276,8 +324,15 @@ def soft_signal_rw(
276
324
  units: str | None = None,
277
325
  precision: int | None = None,
278
326
  ) -> SignalRW[SignalDatatypeT]:
279
- """Creates a read-writable Signal with a SoftSignalBackend.
327
+ """Create a read-writable Signal with a [](#SoftSignalBackend).
328
+
280
329
  May pass metadata, which are propagated into describe.
330
+
331
+ :param datatype: The datatype of the signal.
332
+ :param initial_value: The initial value of the signal.
333
+ :param name: The name of the signal.
334
+ :param units: The units of the signal.
335
+ :param precision: The precision of the signal.
281
336
  """
282
337
  backend = SoftSignalBackend(datatype, initial_value, units, precision)
283
338
  signal = SignalRW(backend=backend, name=name)
@@ -291,10 +346,17 @@ def soft_signal_r_and_setter(
291
346
  units: str | None = None,
292
347
  precision: int | None = None,
293
348
  ) -> tuple[SignalR[SignalDatatypeT], Callable[[SignalDatatypeT], None]]:
294
- """Returns a tuple of a read-only Signal and a callable through
295
- which the signal can be internally modified within the device.
349
+ """Create a read-only Signal with a [](#SoftSignalBackend).
350
+
296
351
  May pass metadata, which are propagated into describe.
297
- Use soft_signal_rw if you want a device that is externally modifiable
352
+ Use soft_signal_rw if you want a device that is externally modifiable.
353
+
354
+ :param datatype: The datatype of the signal.
355
+ :param initial_value: The initial value of the signal.
356
+ :param name: The name of the signal.
357
+ :param units: The units of the signal.
358
+ :param precision: The precision of the signal.
359
+ :return: A tuple of the created SignalR and a callable to set its value.
298
360
  """
299
361
  backend = SoftSignalBackend(datatype, initial_value, units, precision)
300
362
  signal = SignalR(backend=backend, name=name)
@@ -309,34 +371,34 @@ async def observe_value(
309
371
  ) -> AsyncGenerator[SignalDatatypeT, None]:
310
372
  """Subscribe to the value of a signal so it can be iterated from.
311
373
 
312
- Parameters
313
- ----------
314
- signal:
315
- Call subscribe_value on this at the start, and clear_sub on it at the
316
- end
317
- timeout:
318
- If given, how long to wait for each updated value in seconds. If an update
319
- is not produced in this time then raise asyncio.TimeoutError
320
- done_status:
321
- If this status is complete, stop observing and make the iterator return.
322
- If it raises an exception then this exception will be raised by the iterator.
323
- done_timeout:
324
- If given, the maximum time to watch a signal, in seconds. If the loop is still
325
- being watched after this length, raise asyncio.TimeoutError. This should be used
326
- instead of on an 'asyncio.wait_for' timeout
327
-
328
- Notes
329
- -----
330
- Due to a rare condition with busy signals, it is not recommended to use this
331
- function with asyncio.timeout, including in an 'asyncio.wait_for' loop. Instead,
332
- this timeout should be given to the done_timeout parameter.
374
+ The first value yielded in the iterator will be the current value of the
375
+ Signal, and subsequent updates from the control system will result in that
376
+ value being yielded, even if it is the same as the previous value.
333
377
 
334
- Example usage::
378
+ :param signal:
379
+ Call subscribe_value on this at the start, and clear_sub on it at the end.
380
+ :param timeout:
381
+ If given, how long to wait for each updated value in seconds. If an
382
+ update is not produced in this time then raise asyncio.TimeoutError.
383
+ :param done_status:
384
+ If this status is complete, stop observing and make the iterator return.
385
+ If it raises an exception then this exception will be raised by the
386
+ iterator.
387
+ :param done_timeout:
388
+ If given, the maximum time to watch a signal, in seconds. If the loop is
389
+ still being watched after this length, raise asyncio.TimeoutError. This
390
+ should be used instead of on an 'asyncio.wait_for' timeout.
335
391
 
336
- async for value in observe_value(sig):
337
- do_something_with(value)
392
+ Due to a rare condition with busy signals, it is not recommended to use this
393
+ function with asyncio.timeout, including in an `asyncio.wait_for` loop.
394
+ Instead, this timeout should be given to the done_timeout parameter.
395
+
396
+ :example:
397
+ ```python
398
+ async for value in observe_value(sig):
399
+ do_something_with(value)
400
+ ```
338
401
  """
339
-
340
402
  async for _, value in observe_signals_value(
341
403
  signal,
342
404
  timeout=timeout,
@@ -359,33 +421,35 @@ async def observe_signals_value(
359
421
  done_status: Status | None = None,
360
422
  done_timeout: float | None = None,
361
423
  ) -> AsyncGenerator[tuple[SignalR[SignalDatatypeT], SignalDatatypeT], None]:
362
- """Subscribe to the value of a signal so it can be iterated from.
363
-
364
- Parameters
365
- ----------
366
- signals:
367
- Call subscribe_value on all the signals at the start, and clear_sub on it at the
368
- end
369
- timeout:
370
- If given, how long to wait for each updated value in seconds. If an update
371
- is not produced in this time then raise asyncio.TimeoutError
372
- done_status:
424
+ """Subscribe to a set of signals so they can be iterated from.
425
+
426
+ The first values yielded in the iterator will be the current values of the
427
+ Signals, and subsequent updates from the control system will result in that
428
+ value being yielded, even if it is the same as the previous value.
429
+
430
+ :param signals:
431
+ Call subscribe_value on all the signals at the start, and clear_sub on
432
+ it at the end.
433
+ :param timeout:
434
+ If given, how long to wait for each updated value in seconds. If an
435
+ update is not produced in this time then raise asyncio.TimeoutError.
436
+ :param done_status:
373
437
  If this status is complete, stop observing and make the iterator return.
374
- If it raises an exception then this exception will be raised by the iterator.
375
- done_timeout:
376
- If given, the maximum time to watch a signal, in seconds. If the loop is still
377
- being watched after this length, raise asyncio.TimeoutError. This should be used
378
- instead of on an 'asyncio.wait_for' timeout
379
-
380
- Notes
381
- -----
382
- Example usage::
383
-
384
- async for signal,value in observe_signals_values(sig1,sig2,..):
385
- if signal is sig1:
386
- do_something_with(value)
387
- elif signal is sig2:
388
- do_something_else_with(value)
438
+ If it raises an exception then this exception will be raised by the
439
+ iterator.
440
+ :param done_timeout:
441
+ If given, the maximum time to watch a signal, in seconds. If the loop is
442
+ still being watched after this length, raise asyncio.TimeoutError. This
443
+ should be used instead of on an `asyncio.wait_for` timeout.
444
+
445
+ :example:
446
+ ```python
447
+ async for signal, value in observe_signals_values(sig1, sig2, ..):
448
+ if signal is sig1:
449
+ do_something_with(value)
450
+ elif signal is sig2:
451
+ do_something_else_with(value)
452
+ ```
389
453
  """
390
454
  q: asyncio.Queue[tuple[SignalR[SignalDatatypeT], SignalDatatypeT] | Status] = (
391
455
  asyncio.Queue()
@@ -453,29 +517,23 @@ async def wait_for_value(
453
517
  signal: SignalR[SignalDatatypeT],
454
518
  match: SignalDatatypeT | Callable[[SignalDatatypeT], bool],
455
519
  timeout: float | None,
456
- ):
520
+ ) -> None:
457
521
  """Wait for a signal to have a matching value.
458
522
 
459
- Parameters
460
- ----------
461
- signal:
523
+ :param signal:
462
524
  Call subscribe_value on this at the start, and clear_sub on it at the
463
- end
464
- match:
525
+ end.
526
+ :param match:
465
527
  If a callable, it should return True if the value matches. If not
466
528
  callable then value will be checked for equality with match.
467
- timeout:
468
- How long to wait for the value to match
469
-
470
- Notes
471
- -----
472
- Example usage::
473
-
474
- wait_for_value(device.acquiring, 1, timeout=1)
475
-
476
- Or::
477
-
478
- wait_for_value(device.num_captured, lambda v: v > 45, timeout=1)
529
+ :param timeout: How long to wait for the value to match.
530
+
531
+ :example:
532
+ ```python
533
+ await wait_for_value(device.acquiring, 1, timeout=1)
534
+ # or
535
+ await wait_for_value(device.num_captured, lambda v: v > 45, timeout=1)
536
+ ```
479
537
  """
480
538
  if callable(match):
481
539
  checker = _ValueChecker(match, match.__name__) # type: ignore
@@ -498,28 +556,25 @@ async def set_and_wait_for_other_value(
498
556
  This function sets a set_signal to a specified set_value and waits for
499
557
  a match_signal to have the match_value.
500
558
 
501
- Parameters
502
- ----------
503
- signal:
504
- The signal to set
505
- set_value:
506
- The value to set it to
507
- match_signal:
508
- The signal to monitor
509
- match_value:
510
- The value to wait for
511
- timeout:
512
- How long to wait for the signal to have the value
513
- set_timeout:
514
- How long to wait for the set to complete
515
- wait_for_set_completion:
516
- This will wait for set completion #More info in how-to docs
517
-
518
- Notes
519
- -----
520
- Example usage::
521
-
522
- set_and_wait_for_value(device.acquire, 1, device.acquire_rbv, 1)
559
+ :param set_signal: The signal to set.
560
+ :param set_value: The value to set it to.
561
+ :param match_signal: The signal to monitor.
562
+ :param match_value:
563
+ The value (or callable that says if the value matches) to wait for.
564
+ :param timeout: How long to wait for the signal to have the value.
565
+ :param set_timeout: How long to wait for the set to complete.
566
+ :param wait_for_set_completion:
567
+ If False then return as soon as the match_signal matches match_value. If
568
+ True then also wait for the set operation to complete before returning.
569
+
570
+ :seealso:
571
+ [](#interact-with-signals)
572
+
573
+ :example:
574
+ To set the setpoint and wait for the readback to match:
575
+ ```python
576
+ await set_and_wait_for_value(device.setpoint, 1, device.readback, 1)
577
+ ```
523
578
  """
524
579
  # Start monitoring before the set to avoid a race condition
525
580
  values_gen = observe_value(match_signal)
@@ -529,25 +584,34 @@ async def set_and_wait_for_other_value(
529
584
 
530
585
  status = set_signal.set(set_value, timeout=set_timeout)
531
586
 
587
+ if callable(match_value):
588
+ matcher: Callable[[SignalDatatypeV], bool] = match_value # type: ignore
589
+ else:
590
+
591
+ def matcher(value):
592
+ return value == match_value
593
+
594
+ matcher.__name__ = f"equals_{match_value}"
595
+
532
596
  # If the value was the same as before no need to wait for it to change
533
- if current_value != match_value:
597
+ if not matcher(current_value):
534
598
 
535
599
  async def _wait_for_value():
536
600
  async for value in values_gen:
537
- if value == match_value:
601
+ if matcher(value):
538
602
  break
539
603
 
540
604
  try:
541
605
  await asyncio.wait_for(_wait_for_value(), timeout)
542
606
  if wait_for_set_completion:
543
607
  await status
544
- return status
545
608
  except asyncio.TimeoutError as e:
546
- raise TimeoutError(
547
- f"{match_signal.name} didn't match {match_value} in {timeout}s"
609
+ raise asyncio.TimeoutError(
610
+ f"{match_signal.name} value didn't match value from"
611
+ f" {matcher.__name__}() in {timeout}s"
548
612
  ) from e
549
613
 
550
- return completed_status()
614
+ return status
551
615
 
552
616
 
553
617
  async def set_and_wait_for_value(
@@ -555,37 +619,44 @@ async def set_and_wait_for_value(
555
619
  value: SignalDatatypeT,
556
620
  match_value: SignalDatatypeT | Callable[[SignalDatatypeT], bool] | None = None,
557
621
  timeout: float = DEFAULT_TIMEOUT,
558
- status_timeout: float | None = None,
622
+ set_timeout: float | None = None,
559
623
  wait_for_set_completion: bool = True,
560
624
  ) -> AsyncStatus:
561
- """Set a signal and monitor it until it has that value.
625
+ """Set a signal and monitor that same signal until it has the specified value.
562
626
 
563
- Useful for busy record, or other Signals with pattern:
564
- - Set Signal with wait=True and stash the Status
627
+ This function sets a set_signal to a specified set_value and waits for
628
+ a match_signal to have the match_value.
629
+
630
+ :param signal: The signal to set.
631
+ :param value: The value to set it to.
632
+ :param match_value:
633
+ The value (or callable that says if the value matches) to wait for.
634
+ :param timeout: How long to wait for the signal to have the value.
635
+ :param set_timeout: How long to wait for the set to complete.
636
+ :param wait_for_set_completion:
637
+ If False then return as soon as the match_signal matches match_value. If
638
+ True then also wait for the set operation to complete before returning.
639
+
640
+ :seealso:
641
+ [](#interact-with-signals)
642
+
643
+ :examples:
644
+ To set a parameter and wait for it's value to change:
645
+ ```python
646
+ await set_and_wait_for_value(device.parameter, 1)
647
+ ```
648
+ For busy record, or other Signals with pattern:
649
+ - Set Signal with `wait=True` and stash the Status
565
650
  - Read the same Signal to check the operation has started
566
651
  - Return the Status so calling code can wait for operation to complete
567
-
568
- Parameters
569
- ----------
570
- signal:
571
- The signal to set
572
- value:
573
- The value to set it to
574
- match_value:
575
- The expected value of the signal after the operation.
576
- Used to verify that the set operation was successful.
577
- timeout:
578
- How long to wait for the signal to have the value
579
- status_timeout:
580
- How long the returned Status will wait for the set to complete
581
- wait_for_set_completion:
582
- This will wait for set completion #More info in how-to docs
583
-
584
- Notes
585
- -----
586
- Example usage::
587
-
588
- set_and_wait_for_value(device.acquire, 1)
652
+ ```python
653
+ status = await set_and_wait_for_value(
654
+ device.acquire, 1, wait_for_set_completion=False
655
+ )
656
+ # device is now acquiring
657
+ await status
658
+ # device has finished acquiring
659
+ ```
589
660
  """
590
661
  if match_value is None:
591
662
  match_value = value
@@ -595,6 +666,65 @@ async def set_and_wait_for_value(
595
666
  signal,
596
667
  match_value,
597
668
  timeout,
598
- status_timeout,
669
+ set_timeout,
599
670
  wait_for_set_completion,
600
671
  )
672
+
673
+
674
+ def walk_rw_signals(device: Device, path_prefix: str = "") -> dict[str, SignalRW[Any]]:
675
+ """Retrieve all SignalRWs from a device.
676
+
677
+ Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
678
+ part of saving and loading a device.
679
+
680
+ :param device: Device to retrieve read-write signals from.
681
+ :param path_prefix: For internal use, leave blank when calling the method.
682
+ :return:
683
+ A dictionary matching the string attribute path of a SignalRW with the
684
+ signal itself.
685
+ """
686
+ signals: dict[str, SignalRW[Any]] = {}
687
+
688
+ for attr_name, attr in device.children():
689
+ dot_path = f"{path_prefix}{attr_name}"
690
+ if type(attr) is SignalRW:
691
+ signals[dot_path] = attr
692
+ attr_signals = walk_rw_signals(attr, path_prefix=dot_path + ".")
693
+ signals.update(attr_signals)
694
+ return signals
695
+
696
+
697
+ async def walk_config_signals(
698
+ device: Device, path_prefix: str = ""
699
+ ) -> dict[str, SignalRW[Any]]:
700
+ """Retrieve all configuration signals from a device.
701
+
702
+ Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
703
+ part of saving and loading a device.
704
+
705
+ :param device: Device to retrieve configuration signals from.
706
+ :param path_prefix: For internal use, leave blank when calling the method.
707
+ :return:
708
+ A dictionary matching the string attribute path of a SignalRW with the
709
+ signal itself.
710
+ """
711
+ signals: dict[str, SignalRW[Any]] = {}
712
+ config_names: list[str] = []
713
+ if isinstance(device, Configurable):
714
+ configuration = device.read_configuration()
715
+ if inspect.isawaitable(configuration):
716
+ configuration = await configuration
717
+ config_names = list(configuration.keys())
718
+ for attr_name, attr in device.children():
719
+ dot_path = f"{path_prefix}{attr_name}"
720
+ if isinstance(attr, SignalRW) and attr.name in config_names:
721
+ signals[dot_path] = attr
722
+ signals.update(await walk_config_signals(attr, path_prefix=dot_path + "."))
723
+
724
+ return signals
725
+
726
+
727
+ class Ignore:
728
+ """Annotation to ignore a signal when connecting a device."""
729
+
730
+ pass