ophyd-async 0.9.0a2__py3-none-any.whl → 0.10.0a2__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 (151) 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 +97 -62
  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 +106 -125
  8. ophyd_async/core/_device.py +69 -63
  9. ophyd_async/core/_device_filler.py +65 -1
  10. ophyd_async/core/_flyer.py +14 -5
  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 +44 -35
  17. ophyd_async/core/_settings.py +36 -27
  18. ophyd_async/core/_signal.py +262 -170
  19. ophyd_async/core/_signal_backend.py +56 -13
  20. ophyd_async/core/_soft_signal_backend.py +16 -11
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +41 -11
  23. ophyd_async/core/_utils.py +96 -49
  24. ophyd_async/core/_yaml_settings.py +2 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/_andor.py +2 -2
  27. ophyd_async/epics/adandor/_andor_controller.py +4 -2
  28. ophyd_async/epics/adandor/_andor_io.py +2 -4
  29. ophyd_async/epics/adaravis/__init__.py +5 -0
  30. ophyd_async/epics/adaravis/_aravis.py +4 -8
  31. ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
  32. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  33. ophyd_async/epics/adcore/__init__.py +23 -8
  34. ophyd_async/epics/adcore/_core_detector.py +42 -2
  35. ophyd_async/epics/adcore/_core_io.py +124 -99
  36. ophyd_async/epics/adcore/_core_logic.py +106 -27
  37. ophyd_async/epics/adcore/_core_writer.py +12 -8
  38. ophyd_async/epics/adcore/_hdf_writer.py +21 -38
  39. ophyd_async/epics/adcore/_single_trigger.py +2 -2
  40. ophyd_async/epics/adcore/_utils.py +2 -2
  41. ophyd_async/epics/adkinetix/__init__.py +2 -1
  42. ophyd_async/epics/adkinetix/_kinetix.py +3 -3
  43. ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
  44. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  45. ophyd_async/epics/adpilatus/__init__.py +5 -0
  46. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  47. ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
  48. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  49. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  50. ophyd_async/epics/adsimdetector/_sim.py +4 -14
  51. ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
  52. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  53. ophyd_async/epics/advimba/__init__.py +10 -1
  54. ophyd_async/epics/advimba/_vimba.py +3 -2
  55. ophyd_async/epics/advimba/_vimba_controller.py +4 -2
  56. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  57. ophyd_async/epics/core/_aioca.py +35 -16
  58. ophyd_async/epics/core/_epics_connector.py +4 -0
  59. ophyd_async/epics/core/_epics_device.py +2 -0
  60. ophyd_async/epics/core/_p4p.py +10 -2
  61. ophyd_async/epics/core/_pvi_connector.py +65 -8
  62. ophyd_async/epics/core/_signal.py +51 -51
  63. ophyd_async/epics/core/_util.py +4 -4
  64. ophyd_async/epics/demo/__init__.py +16 -0
  65. ophyd_async/epics/demo/__main__.py +31 -0
  66. ophyd_async/epics/demo/_ioc.py +32 -0
  67. ophyd_async/epics/demo/_motor.py +82 -0
  68. ophyd_async/epics/demo/_point_detector.py +42 -0
  69. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  70. ophyd_async/epics/demo/_stage.py +15 -0
  71. ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
  72. ophyd_async/epics/demo/point_detector.db +59 -0
  73. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  74. ophyd_async/epics/eiger/_eiger.py +1 -3
  75. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  76. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  77. ophyd_async/epics/eiger/_odin_io.py +1 -2
  78. ophyd_async/epics/motor.py +65 -28
  79. ophyd_async/epics/signal.py +4 -1
  80. ophyd_async/epics/testing/_example_ioc.py +21 -9
  81. ophyd_async/epics/testing/_utils.py +3 -0
  82. ophyd_async/epics/testing/test_records.db +8 -0
  83. ophyd_async/epics/testing/test_records_pva.db +17 -16
  84. ophyd_async/fastcs/__init__.py +1 -0
  85. ophyd_async/fastcs/core.py +6 -0
  86. ophyd_async/fastcs/odin/__init__.py +1 -0
  87. ophyd_async/fastcs/panda/__init__.py +8 -6
  88. ophyd_async/fastcs/panda/_block.py +29 -9
  89. ophyd_async/fastcs/panda/_control.py +5 -0
  90. ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
  91. ophyd_async/fastcs/panda/_table.py +9 -6
  92. ophyd_async/fastcs/panda/_trigger.py +23 -9
  93. ophyd_async/fastcs/panda/_writer.py +27 -30
  94. ophyd_async/plan_stubs/__init__.py +2 -0
  95. ophyd_async/plan_stubs/_ensure_connected.py +1 -0
  96. ophyd_async/plan_stubs/_fly.py +2 -4
  97. ophyd_async/plan_stubs/_nd_attributes.py +2 -0
  98. ophyd_async/plan_stubs/_panda.py +1 -0
  99. ophyd_async/plan_stubs/_settings.py +43 -16
  100. ophyd_async/plan_stubs/_utils.py +3 -0
  101. ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
  102. ophyd_async/sim/__init__.py +24 -14
  103. ophyd_async/sim/__main__.py +43 -0
  104. ophyd_async/sim/_blob_detector.py +33 -0
  105. ophyd_async/sim/_blob_detector_controller.py +48 -0
  106. ophyd_async/sim/_blob_detector_writer.py +105 -0
  107. ophyd_async/sim/_mirror_horizontal.py +46 -0
  108. ophyd_async/sim/_mirror_vertical.py +74 -0
  109. ophyd_async/sim/_motor.py +233 -0
  110. ophyd_async/sim/_pattern_generator.py +124 -0
  111. ophyd_async/sim/_point_detector.py +86 -0
  112. ophyd_async/sim/_stage.py +19 -0
  113. ophyd_async/tango/__init__.py +1 -0
  114. ophyd_async/tango/core/__init__.py +6 -1
  115. ophyd_async/tango/core/_base_device.py +41 -33
  116. ophyd_async/tango/core/_converters.py +81 -0
  117. ophyd_async/tango/core/_signal.py +18 -32
  118. ophyd_async/tango/core/_tango_readable.py +2 -19
  119. ophyd_async/tango/core/_tango_transport.py +136 -60
  120. ophyd_async/tango/core/_utils.py +47 -0
  121. ophyd_async/tango/{sim → demo}/_counter.py +2 -0
  122. ophyd_async/tango/{sim → demo}/_detector.py +2 -0
  123. ophyd_async/tango/{sim → demo}/_mover.py +5 -4
  124. ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
  125. ophyd_async/tango/testing/__init__.py +6 -0
  126. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  127. ophyd_async/testing/__init__.py +29 -7
  128. ophyd_async/testing/_assert.py +145 -83
  129. ophyd_async/testing/_mock_signal_utils.py +56 -70
  130. ophyd_async/testing/_one_of_everything.py +41 -21
  131. ophyd_async/testing/_single_derived.py +89 -0
  132. ophyd_async/testing/_utils.py +3 -0
  133. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
  134. ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
  135. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
  136. ophyd_async/epics/sim/__init__.py +0 -54
  137. ophyd_async/epics/sim/_ioc.py +0 -29
  138. ophyd_async/epics/sim/_mover.py +0 -101
  139. ophyd_async/epics/sim/_sensor.py +0 -37
  140. ophyd_async/epics/sim/sensor.db +0 -19
  141. ophyd_async/sim/_pattern_detector/__init__.py +0 -13
  142. ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
  143. ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
  144. ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
  145. ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
  146. ophyd_async/sim/_sim_motor.py +0 -107
  147. ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
  148. /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
  149. /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
  150. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
  151. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.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 Any, 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,10 +94,16 @@ 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
108
  def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal) -> None:
101
109
  self._signal: Signal[Any] = signal
@@ -159,7 +167,7 @@ class _SignalCache(Generic[SignalDatatypeT]):
159
167
 
160
168
 
161
169
  class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribable):
162
- """Signal that can be read from and monitored"""
170
+ """Signal that can be read from and monitored."""
163
171
 
164
172
  _cache: _SignalCache | None = None
165
173
 
@@ -190,46 +198,71 @@ class SignalR(Signal[SignalDatatypeT], AsyncReadable, AsyncStageable, Subscribab
190
198
 
191
199
  @_add_timeout
192
200
  async def read(self, cached: bool | None = None) -> dict[str, Reading]:
193
- """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
+ """
194
209
  return {self.name: await self._backend_or_cache(cached).get_reading()}
195
210
 
196
211
  @_add_timeout
197
212
  async def describe(self) -> dict[str, DataKey]:
198
- """Return a single item dict with the descriptor in it"""
213
+ """Return a single item dict describing the signal value."""
199
214
  return {self.name: await self._connector.backend.get_datakey(self.source)}
200
215
 
201
216
  @_add_timeout
202
217
  async def get_value(self, cached: bool | None = None) -> SignalDatatypeT:
203
- """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
+ """
204
226
  value = await self._backend_or_cache(cached).get_value()
205
227
  self.log.debug(f"get_value() on source {self.source} returned {value}")
206
228
  return value
207
229
 
208
230
  def subscribe_value(self, function: Callback[SignalDatatypeT]):
209
- """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
+ """
210
235
  self._get_cache().subscribe(function, want_value=True)
211
236
 
212
- def subscribe(self, function: Callback[dict[str, Reading]]) -> None:
213
- """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
+ """
214
244
  self._get_cache().subscribe(function, want_value=False)
215
245
 
216
246
  def clear_sub(self, function: Callback) -> None:
217
- """Remove a subscription."""
247
+ """Remove a subscription passed to `subscribe` or `subscribe_value`.
248
+
249
+ :param function: The callback function to remove.
250
+ """
218
251
  self._del_cache(self._get_cache().unsubscribe(function))
219
252
 
220
253
  @AsyncStatus.wrap
221
254
  async def stage(self) -> None:
222
- """Start caching this signal"""
255
+ """Start caching this signal."""
223
256
  self._get_cache().set_staged(True)
224
257
 
225
258
  @AsyncStatus.wrap
226
259
  async def unstage(self) -> None:
227
- """Stop caching this signal"""
260
+ """Stop caching this signal."""
228
261
  self._del_cache(self._get_cache().set_staged(False))
229
262
 
230
263
 
231
264
  class SignalW(Signal[SignalDatatypeT], Movable):
232
- """Signal that can be set"""
265
+ """Signal that can be set."""
233
266
 
234
267
  @AsyncStatus.wrap
235
268
  async def set(
@@ -238,7 +271,12 @@ class SignalW(Signal[SignalDatatypeT], Movable):
238
271
  wait=True,
239
272
  timeout: CalculatableTimeout = CALCULATE_TIMEOUT,
240
273
  ) -> None:
241
- """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
+ """
242
280
  if timeout == CALCULATE_TIMEOUT:
243
281
  timeout = self._timeout
244
282
  source = self._connector.backend.source(self.name, read=False)
@@ -248,7 +286,7 @@ class SignalW(Signal[SignalDatatypeT], Movable):
248
286
 
249
287
 
250
288
  class SignalRW(SignalR[SignalDatatypeT], SignalW[SignalDatatypeT], Locatable):
251
- """Signal that can be both read and set"""
289
+ """Signal that can be both read and set."""
252
290
 
253
291
  @_add_timeout
254
292
  async def locate(self) -> Location:
@@ -260,13 +298,17 @@ class SignalRW(SignalR[SignalDatatypeT], SignalW[SignalDatatypeT], Locatable):
260
298
 
261
299
 
262
300
  class SignalX(Signal):
263
- """Signal that puts the default value"""
301
+ """Signal that puts the default value."""
264
302
 
265
303
  @AsyncStatus.wrap
266
304
  async def trigger(
267
305
  self, wait=True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
268
306
  ) -> None:
269
- """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
+ """
270
312
  if timeout == CALCULATE_TIMEOUT:
271
313
  timeout = self._timeout
272
314
  source = self._connector.backend.source(self.name, read=False)
@@ -282,8 +324,15 @@ def soft_signal_rw(
282
324
  units: str | None = None,
283
325
  precision: int | None = None,
284
326
  ) -> SignalRW[SignalDatatypeT]:
285
- """Creates a read-writable Signal with a SoftSignalBackend.
327
+ """Create a read-writable Signal with a [](#SoftSignalBackend).
328
+
286
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.
287
336
  """
288
337
  backend = SoftSignalBackend(datatype, initial_value, units, precision)
289
338
  signal = SignalRW(backend=backend, name=name)
@@ -297,10 +346,17 @@ def soft_signal_r_and_setter(
297
346
  units: str | None = None,
298
347
  precision: int | None = None,
299
348
  ) -> tuple[SignalR[SignalDatatypeT], Callable[[SignalDatatypeT], None]]:
300
- """Returns a tuple of a read-only Signal and a callable through
301
- which the signal can be internally modified within the device.
349
+ """Create a read-only Signal with a [](#SoftSignalBackend).
350
+
302
351
  May pass metadata, which are propagated into describe.
303
- 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.
304
360
  """
305
361
  backend = SoftSignalBackend(datatype, initial_value, units, precision)
306
362
  signal = SignalR(backend=backend, name=name)
@@ -315,34 +371,34 @@ async def observe_value(
315
371
  ) -> AsyncGenerator[SignalDatatypeT, None]:
316
372
  """Subscribe to the value of a signal so it can be iterated from.
317
373
 
318
- Parameters
319
- ----------
320
- signal:
321
- Call subscribe_value on this at the start, and clear_sub on it at the
322
- end
323
- timeout:
324
- If given, how long to wait for each updated value in seconds. If an update
325
- is not produced in this time then raise asyncio.TimeoutError
326
- done_status:
327
- If this status is complete, stop observing and make the iterator return.
328
- If it raises an exception then this exception will be raised by the iterator.
329
- done_timeout:
330
- If given, the maximum time to watch a signal, in seconds. If the loop is still
331
- being watched after this length, raise asyncio.TimeoutError. This should be used
332
- instead of on an 'asyncio.wait_for' timeout
333
-
334
- Notes
335
- -----
336
- Due to a rare condition with busy signals, it is not recommended to use this
337
- function with asyncio.timeout, including in an 'asyncio.wait_for' loop. Instead,
338
- 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.
339
377
 
340
- 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.
341
391
 
342
- async for value in observe_value(sig):
343
- 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
+ ```
344
401
  """
345
-
346
402
  async for _, value in observe_signals_value(
347
403
  signal,
348
404
  timeout=timeout,
@@ -365,33 +421,35 @@ async def observe_signals_value(
365
421
  done_status: Status | None = None,
366
422
  done_timeout: float | None = None,
367
423
  ) -> AsyncGenerator[tuple[SignalR[SignalDatatypeT], SignalDatatypeT], None]:
368
- """Subscribe to the value of a signal so it can be iterated from.
369
-
370
- Parameters
371
- ----------
372
- signals:
373
- Call subscribe_value on all the signals at the start, and clear_sub on it at the
374
- end
375
- timeout:
376
- If given, how long to wait for each updated value in seconds. If an update
377
- is not produced in this time then raise asyncio.TimeoutError
378
- 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:
379
437
  If this status is complete, stop observing and make the iterator return.
380
- If it raises an exception then this exception will be raised by the iterator.
381
- done_timeout:
382
- If given, the maximum time to watch a signal, in seconds. If the loop is still
383
- being watched after this length, raise asyncio.TimeoutError. This should be used
384
- instead of on an 'asyncio.wait_for' timeout
385
-
386
- Notes
387
- -----
388
- Example usage::
389
-
390
- async for signal,value in observe_signals_values(sig1,sig2,..):
391
- if signal is sig1:
392
- do_something_with(value)
393
- elif signal is sig2:
394
- 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
+ ```
395
453
  """
396
454
  q: asyncio.Queue[tuple[SignalR[SignalDatatypeT], SignalDatatypeT] | Status] = (
397
455
  asyncio.Queue()
@@ -459,29 +517,23 @@ async def wait_for_value(
459
517
  signal: SignalR[SignalDatatypeT],
460
518
  match: SignalDatatypeT | Callable[[SignalDatatypeT], bool],
461
519
  timeout: float | None,
462
- ):
520
+ ) -> None:
463
521
  """Wait for a signal to have a matching value.
464
522
 
465
- Parameters
466
- ----------
467
- signal:
523
+ :param signal:
468
524
  Call subscribe_value on this at the start, and clear_sub on it at the
469
- end
470
- match:
525
+ end.
526
+ :param match:
471
527
  If a callable, it should return True if the value matches. If not
472
528
  callable then value will be checked for equality with match.
473
- timeout:
474
- How long to wait for the value to match
475
-
476
- Notes
477
- -----
478
- Example usage::
479
-
480
- wait_for_value(device.acquiring, 1, timeout=1)
481
-
482
- Or::
483
-
484
- 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
+ ```
485
537
  """
486
538
  if callable(match):
487
539
  checker = _ValueChecker(match, match.__name__) # type: ignore
@@ -504,28 +556,25 @@ async def set_and_wait_for_other_value(
504
556
  This function sets a set_signal to a specified set_value and waits for
505
557
  a match_signal to have the match_value.
506
558
 
507
- Parameters
508
- ----------
509
- signal:
510
- The signal to set
511
- set_value:
512
- The value to set it to
513
- match_signal:
514
- The signal to monitor
515
- match_value:
516
- The value to wait for
517
- timeout:
518
- How long to wait for the signal to have the value
519
- set_timeout:
520
- How long to wait for the set to complete
521
- wait_for_set_completion:
522
- This will wait for set completion #More info in how-to docs
523
-
524
- Notes
525
- -----
526
- Example usage::
527
-
528
- 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
+ ```
529
578
  """
530
579
  # Start monitoring before the set to avoid a race condition
531
580
  values_gen = observe_value(match_signal)
@@ -535,25 +584,34 @@ async def set_and_wait_for_other_value(
535
584
 
536
585
  status = set_signal.set(set_value, timeout=set_timeout)
537
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
+
538
596
  # If the value was the same as before no need to wait for it to change
539
- if current_value != match_value:
597
+ if not matcher(current_value):
540
598
 
541
599
  async def _wait_for_value():
542
600
  async for value in values_gen:
543
- if value == match_value:
601
+ if matcher(value):
544
602
  break
545
603
 
546
604
  try:
547
605
  await asyncio.wait_for(_wait_for_value(), timeout)
548
606
  if wait_for_set_completion:
549
607
  await status
550
- return status
551
608
  except asyncio.TimeoutError as e:
552
- raise TimeoutError(
553
- 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"
554
612
  ) from e
555
613
 
556
- return completed_status()
614
+ return status
557
615
 
558
616
 
559
617
  async def set_and_wait_for_value(
@@ -561,37 +619,44 @@ async def set_and_wait_for_value(
561
619
  value: SignalDatatypeT,
562
620
  match_value: SignalDatatypeT | Callable[[SignalDatatypeT], bool] | None = None,
563
621
  timeout: float = DEFAULT_TIMEOUT,
564
- status_timeout: float | None = None,
622
+ set_timeout: float | None = None,
565
623
  wait_for_set_completion: bool = True,
566
624
  ) -> AsyncStatus:
567
- """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.
626
+
627
+ This function sets a set_signal to a specified set_value and waits for
628
+ a match_signal to have the match_value.
568
629
 
569
- Useful for busy record, or other Signals with pattern:
570
- - Set Signal with wait=True and stash the Status
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
571
650
  - Read the same Signal to check the operation has started
572
651
  - Return the Status so calling code can wait for operation to complete
573
-
574
- Parameters
575
- ----------
576
- signal:
577
- The signal to set
578
- value:
579
- The value to set it to
580
- match_value:
581
- The expected value of the signal after the operation.
582
- Used to verify that the set operation was successful.
583
- timeout:
584
- How long to wait for the signal to have the value
585
- status_timeout:
586
- How long the returned Status will wait for the set to complete
587
- wait_for_set_completion:
588
- This will wait for set completion #More info in how-to docs
589
-
590
- Notes
591
- -----
592
- Example usage::
593
-
594
- 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
+ ```
595
660
  """
596
661
  if match_value is None:
597
662
  match_value = value
@@ -601,7 +666,7 @@ async def set_and_wait_for_value(
601
666
  signal,
602
667
  match_value,
603
668
  timeout,
604
- status_timeout,
669
+ set_timeout,
605
670
  wait_for_set_completion,
606
671
  )
607
672
 
@@ -612,20 +677,11 @@ def walk_rw_signals(device: Device, path_prefix: str = "") -> dict[str, SignalRW
612
677
  Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
613
678
  part of saving and loading a device.
614
679
 
615
- Parameters
616
- ----------
617
- device : Device
618
- Ophyd device to retrieve read-write signals from.
619
-
620
- path_prefix : str
621
- For internal use, leave blank when calling the method.
622
-
623
- Returns
624
- -------
625
- SignalRWs : dict
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:
626
683
  A dictionary matching the string attribute path of a SignalRW with the
627
684
  signal itself.
628
-
629
685
  """
630
686
  signals: dict[str, SignalRW[Any]] = {}
631
687
 
@@ -636,3 +692,39 @@ def walk_rw_signals(device: Device, path_prefix: str = "") -> dict[str, SignalRW
636
692
  attr_signals = walk_rw_signals(attr, path_prefix=dot_path + ".")
637
693
  signals.update(attr_signals)
638
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