ophyd-async 0.8.0a5__py3-none-any.whl → 0.9.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 (64) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +4 -26
  3. ophyd_async/core/_detector.py +9 -9
  4. ophyd_async/core/_device.py +27 -8
  5. ophyd_async/core/_protocol.py +0 -28
  6. ophyd_async/core/_signal.py +111 -136
  7. ophyd_async/core/_table.py +9 -4
  8. ophyd_async/core/_utils.py +11 -2
  9. ophyd_async/epics/adaravis/_aravis_controller.py +8 -8
  10. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  11. ophyd_async/epics/adcore/_core_io.py +21 -21
  12. ophyd_async/epics/adcore/_core_logic.py +6 -3
  13. ophyd_async/epics/adcore/_hdf_writer.py +6 -3
  14. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  15. ophyd_async/epics/adcore/_utils.py +35 -35
  16. ophyd_async/epics/adkinetix/_kinetix_controller.py +7 -7
  17. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  18. ophyd_async/epics/adpilatus/_pilatus.py +3 -3
  19. ophyd_async/epics/adpilatus/_pilatus_controller.py +4 -4
  20. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  21. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  22. ophyd_async/epics/advimba/_vimba_controller.py +14 -14
  23. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  24. ophyd_async/epics/core/_p4p.py +19 -0
  25. ophyd_async/epics/core/_pvi_connector.py +4 -2
  26. ophyd_async/epics/core/_signal.py +9 -2
  27. ophyd_async/epics/core/_util.py +9 -0
  28. ophyd_async/epics/demo/_mover.py +2 -2
  29. ophyd_async/epics/demo/_sensor.py +2 -2
  30. ophyd_async/epics/eiger/_eiger_controller.py +10 -5
  31. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  32. ophyd_async/epics/motor.py +8 -5
  33. ophyd_async/epics/testing/__init__.py +24 -0
  34. ophyd_async/epics/testing/_example_ioc.py +107 -0
  35. ophyd_async/epics/testing/_utils.py +78 -0
  36. ophyd_async/epics/testing/test_records.db +158 -0
  37. ophyd_async/epics/testing/test_records_pva.db +177 -0
  38. ophyd_async/fastcs/core.py +2 -2
  39. ophyd_async/fastcs/panda/_block.py +9 -9
  40. ophyd_async/fastcs/panda/_control.py +2 -2
  41. ophyd_async/fastcs/panda/_hdf_panda.py +4 -1
  42. ophyd_async/fastcs/panda/_trigger.py +7 -7
  43. ophyd_async/plan_stubs/_fly.py +1 -1
  44. ophyd_async/sim/demo/_sim_motor.py +34 -32
  45. ophyd_async/tango/__init__.py +0 -43
  46. ophyd_async/tango/{signal → core}/__init__.py +7 -2
  47. ophyd_async/tango/{base_devices → core}/_base_device.py +38 -64
  48. ophyd_async/tango/{signal → core}/_signal.py +13 -3
  49. ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
  50. ophyd_async/tango/{signal → core}/_tango_transport.py +1 -1
  51. ophyd_async/tango/demo/_counter.py +6 -7
  52. ophyd_async/tango/demo/_mover.py +8 -7
  53. ophyd_async/testing/__init__.py +33 -0
  54. ophyd_async/testing/_assert.py +128 -0
  55. ophyd_async/{core → testing}/_mock_signal_utils.py +12 -8
  56. ophyd_async/testing/_wait_for_pending.py +22 -0
  57. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/METADATA +49 -47
  58. ophyd_async-0.9.0a1.dist-info/RECORD +119 -0
  59. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/WHEEL +1 -1
  60. ophyd_async/tango/base_devices/__init__.py +0 -4
  61. ophyd_async-0.8.0a5.dist-info/RECORD +0 -112
  62. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/LICENSE +0 -0
  63. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/entry_points.txt +0 -0
  64. {ophyd_async-0.8.0a5.dist-info → ophyd_async-0.9.0a1.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.8.0a5'
16
- __version_tuple__ = version_tuple = (0, 8, 0)
15
+ __version__ = version = '0.9.0a1'
16
+ __version_tuple__ = version_tuple = (0, 9, 0)
@@ -21,16 +21,6 @@ from ._flyer import FlyerController, StandardFlyer
21
21
  from ._hdf_dataset import HDFDataset, HDFFile
22
22
  from ._log import config_ophyd_async_logging
23
23
  from ._mock_signal_backend import MockSignalBackend
24
- from ._mock_signal_utils import (
25
- callback_on_mock_put,
26
- get_mock,
27
- get_mock_put,
28
- mock_puts_blocked,
29
- reset_mock_put_calls,
30
- set_mock_put_proceeds,
31
- set_mock_value,
32
- set_mock_values,
33
- )
34
24
  from ._protocol import AsyncConfigurable, AsyncReadable, AsyncStageable
35
25
  from ._providers import (
36
26
  AutoIncrementFilenameProvider,
@@ -53,14 +43,12 @@ from ._readable import (
53
43
  )
54
44
  from ._signal import (
55
45
  Signal,
46
+ SignalConnector,
56
47
  SignalR,
57
48
  SignalRW,
58
49
  SignalW,
59
50
  SignalX,
60
- assert_configuration,
61
- assert_emitted,
62
- assert_reading,
63
- assert_value,
51
+ observe_signals_value,
64
52
  observe_value,
65
53
  set_and_wait_for_other_value,
66
54
  set_and_wait_for_value,
@@ -122,14 +110,6 @@ __all__ = [
122
110
  "HDFFile",
123
111
  "config_ophyd_async_logging",
124
112
  "MockSignalBackend",
125
- "callback_on_mock_put",
126
- "get_mock",
127
- "get_mock_put",
128
- "mock_puts_blocked",
129
- "reset_mock_put_calls",
130
- "set_mock_put_proceeds",
131
- "set_mock_value",
132
- "set_mock_values",
133
113
  "AsyncConfigurable",
134
114
  "AsyncReadable",
135
115
  "AsyncStageable",
@@ -149,15 +129,13 @@ __all__ = [
149
129
  "StandardReadable",
150
130
  "StandardReadableFormat",
151
131
  "Signal",
132
+ "SignalConnector",
152
133
  "SignalR",
153
134
  "SignalRW",
154
135
  "SignalW",
155
136
  "SignalX",
156
- "assert_configuration",
157
- "assert_emitted",
158
- "assert_reading",
159
- "assert_value",
160
137
  "observe_value",
138
+ "observe_signals_value",
161
139
  "set_and_wait_for_value",
162
140
  "set_and_wait_for_other_value",
163
141
  "soft_signal_r_and_setter",
@@ -30,13 +30,13 @@ class DetectorTrigger(StrictEnum):
30
30
  """Type of mechanism for triggering a detector to take frames"""
31
31
 
32
32
  #: Detector generates internal trigger for given rate
33
- internal = "internal"
33
+ INTERNAL = "internal"
34
34
  #: Expect a series of arbitrary length trigger signals
35
- edge_trigger = "edge_trigger"
35
+ EDGE_TRIGGER = "edge_trigger"
36
36
  #: Expect a series of constant width external gate signals
37
- constant_gate = "constant_gate"
37
+ CONSTANT_GATE = "constant_gate"
38
38
  #: Expect a series of variable width external gate signals
39
- variable_gate = "variable_gate"
39
+ VARIABLE_GATE = "variable_gate"
40
40
 
41
41
 
42
42
  class TriggerInfo(BaseModel):
@@ -53,7 +53,7 @@ class TriggerInfo(BaseModel):
53
53
  #: - 3 times for final flat field images
54
54
  number_of_triggers: NonNegativeInt | list[NonNegativeInt]
55
55
  #: Sort of triggers that will be sent
56
- trigger: DetectorTrigger = Field(default=DetectorTrigger.internal)
56
+ trigger: DetectorTrigger = Field(default=DetectorTrigger.INTERNAL)
57
57
  #: What is the minimum deadtime between triggers
58
58
  deadtime: float | None = Field(default=None, ge=0)
59
59
  #: What is the maximum high time of the triggers
@@ -265,14 +265,14 @@ class StandardDetector(
265
265
  await self.prepare(
266
266
  TriggerInfo(
267
267
  number_of_triggers=1,
268
- trigger=DetectorTrigger.internal,
268
+ trigger=DetectorTrigger.INTERNAL,
269
269
  deadtime=None,
270
270
  livetime=None,
271
271
  frame_timeout=None,
272
272
  )
273
273
  )
274
274
  assert self._trigger_info
275
- assert self._trigger_info.trigger is DetectorTrigger.internal
275
+ assert self._trigger_info.trigger is DetectorTrigger.INTERNAL
276
276
  # Arm the detector and wait for it to finish.
277
277
  indices_written = await self.writer.get_indices_written()
278
278
  await self.controller.arm()
@@ -303,7 +303,7 @@ class StandardDetector(
303
303
  Args:
304
304
  value: TriggerInfo describing how to trigger the detector
305
305
  """
306
- if value.trigger != DetectorTrigger.internal:
306
+ if value.trigger != DetectorTrigger.INTERNAL:
307
307
  assert (
308
308
  value.deadtime
309
309
  ), "Deadtime must be supplied when in externally triggered mode"
@@ -323,7 +323,7 @@ class StandardDetector(
323
323
  self._describe, _ = await asyncio.gather(
324
324
  self.writer.open(value.multiplier), self.controller.prepare(value)
325
325
  )
326
- if value.trigger != DetectorTrigger.internal:
326
+ if value.trigger != DetectorTrigger.INTERNAL:
327
327
  await self.controller.arm()
328
328
  self._fly_start = time.monotonic()
329
329
 
@@ -10,7 +10,6 @@ from typing import Any, TypeVar
10
10
  from bluesky.protocols import HasName
11
11
  from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
12
12
 
13
- from ._protocol import Connectable
14
13
  from ._utils import DEFAULT_TIMEOUT, LazyMock, NotConnected, wait_for_connection
15
14
 
16
15
 
@@ -61,7 +60,7 @@ class DeviceConnector:
61
60
  await wait_for_connection(**coros)
62
61
 
63
62
 
64
- class Device(HasName, Connectable):
63
+ class Device(HasName):
65
64
  """Common base class for all Ophyd Async Devices."""
66
65
 
67
66
  _name: str = ""
@@ -71,13 +70,16 @@ class Device(HasName, Connectable):
71
70
  _connect_task: asyncio.Task | None = None
72
71
  # The mock if we have connected in mock mode
73
72
  _mock: LazyMock | None = None
73
+ # The separator to use when making child names
74
+ _child_name_separator: str = "-"
74
75
 
75
76
  def __init__(
76
77
  self, name: str = "", connector: DeviceConnector | None = None
77
78
  ) -> None:
78
79
  self._connector = connector or DeviceConnector()
79
80
  self._connector.create_children_from_annotations(self)
80
- self.set_name(name)
81
+ if name:
82
+ self.set_name(name)
81
83
 
82
84
  @property
83
85
  def name(self) -> str:
@@ -97,21 +99,30 @@ class Device(HasName, Connectable):
97
99
  getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
98
100
  )
99
101
 
100
- def set_name(self, name: str):
102
+ def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
101
103
  """Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
102
104
 
103
105
  Parameters
104
106
  ----------
105
107
  name:
106
108
  New name to set
109
+ child_name_separator:
110
+ Use this as a separator instead of "-". Use "_" instead to make the same
111
+ names as the equivalent ophyd sync device.
107
112
  """
108
113
  self._name = name
114
+ if child_name_separator:
115
+ self._child_name_separator = child_name_separator
109
116
  # Ensure logger is recreated after a name change
110
117
  if "log" in self.__dict__:
111
118
  del self.log
112
- for child_name, child in self.children():
113
- child_name = f"{self.name}-{child_name.strip('_')}" if self.name else ""
114
- child.set_name(child_name)
119
+ for attr_name, child in self.children():
120
+ child_name = (
121
+ f"{self.name}{self._child_name_separator}{attr_name}"
122
+ if self.name
123
+ else ""
124
+ )
125
+ child.set_name(child_name, child_name_separator=self._child_name_separator)
115
126
 
116
127
  def __setattr__(self, name: str, value: Any) -> None:
117
128
  # Bear in mind that this function is called *a lot*, so
@@ -147,6 +158,10 @@ class Device(HasName, Connectable):
147
158
  timeout:
148
159
  Time to wait before failing with a TimeoutError.
149
160
  """
161
+ assert hasattr(self, "_connector"), (
162
+ f"{self}: doesn't have attribute `_connector`,"
163
+ " did you call `super().__init__` in your `__init__` method?"
164
+ )
150
165
  if mock:
151
166
  # Always connect in mock mode serially
152
167
  if isinstance(mock, LazyMock):
@@ -247,6 +262,8 @@ class DeviceCollector:
247
262
  set_name:
248
263
  If True, call ``device.set_name(variable_name)`` on all collected
249
264
  Devices
265
+ child_name_separator:
266
+ Use this as a separator if we call ``set_name``.
250
267
  connect:
251
268
  If True, call ``device.connect(mock)`` in parallel on all
252
269
  collected Devices
@@ -271,11 +288,13 @@ class DeviceCollector:
271
288
  def __init__(
272
289
  self,
273
290
  set_name=True,
291
+ child_name_separator: str = "-",
274
292
  connect=True,
275
293
  mock=False,
276
294
  timeout: float = 10.0,
277
295
  ):
278
296
  self._set_name = set_name
297
+ self._child_name_separator = child_name_separator
279
298
  self._connect = connect
280
299
  self._mock = mock
281
300
  self._timeout = timeout
@@ -311,7 +330,7 @@ class DeviceCollector:
311
330
  for name, obj in self._objects_on_exit.items():
312
331
  if name not in self._names_on_enter and isinstance(obj, Device):
313
332
  if self._set_name and not obj.name:
314
- obj.set_name(name)
333
+ obj.set_name(name, child_name_separator=self._child_name_separator)
315
334
  if self._connect:
316
335
  connect_coroutines[name] = obj.connect(
317
336
  self._mock, timeout=self._timeout
@@ -13,38 +13,10 @@ 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
-
18
16
  if TYPE_CHECKING:
19
- from unittest.mock import Mock
20
-
21
17
  from ._status import AsyncStatus
22
18
 
23
19
 
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
-
48
20
  @runtime_checkable
49
21
  class AsyncReadable(HasName, Protocol):
50
22
  @abstractmethod
@@ -2,8 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import functools
5
- from collections.abc import AsyncGenerator, Awaitable, Callable, Mapping
6
- from typing import Any, Generic, cast
5
+ import time
6
+ from collections.abc import AsyncGenerator, Awaitable, Callable
7
+ from typing import Generic, cast
7
8
 
8
9
  from bluesky.protocols import (
9
10
  Locatable,
@@ -17,7 +18,6 @@ from event_model import DataKey
17
18
  from ._device import Device, DeviceConnector
18
19
  from ._mock_signal_backend import MockSignalBackend
19
20
  from ._protocol import (
20
- AsyncConfigurable,
21
21
  AsyncReadable,
22
22
  AsyncStageable,
23
23
  Reading,
@@ -28,7 +28,7 @@ from ._signal_backend import (
28
28
  SignalDatatypeV,
29
29
  )
30
30
  from ._soft_signal_backend import SoftSignalBackend
31
- from ._status import AsyncStatus
31
+ from ._status import AsyncStatus, completed_status
32
32
  from ._utils import (
33
33
  CALCULATE_TIMEOUT,
34
34
  DEFAULT_TIMEOUT,
@@ -122,7 +122,7 @@ class _SignalCache(Generic[SignalDatatypeT]):
122
122
 
123
123
  def _callback(self, reading: Reading[SignalDatatypeT]):
124
124
  self._signal.log.debug(
125
- f"Updated subscription: reading of source {self._signal.source} changed"
125
+ f"Updated subscription: reading of source {self._signal.source} changed "
126
126
  f"from {self._reading} to {reading}"
127
127
  )
128
128
  self._reading = reading
@@ -301,137 +301,70 @@ def soft_signal_r_and_setter(
301
301
  return (signal, backend.set_value)
302
302
 
303
303
 
304
- def _generate_assert_error_msg(name: str, expected_result, actual_result) -> str:
305
- WARNING = "\033[93m"
306
- FAIL = "\033[91m"
307
- ENDC = "\033[0m"
308
- return (
309
- f"Expected {WARNING}{name}{ENDC} to produce"
310
- + f"\n{FAIL}{expected_result}{ENDC}"
311
- + f"\nbut actually got \n{FAIL}{actual_result}{ENDC}"
312
- )
313
-
314
-
315
- async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
316
- """Assert a signal's value and compare it an expected signal.
304
+ async def observe_value(
305
+ signal: SignalR[SignalDatatypeT],
306
+ timeout: float | None = None,
307
+ done_status: Status | None = None,
308
+ done_timeout: float | None = None,
309
+ ) -> AsyncGenerator[SignalDatatypeT, None]:
310
+ """Subscribe to the value of a signal so it can be iterated from.
317
311
 
318
312
  Parameters
319
313
  ----------
320
314
  signal:
321
- signal with get_value.
322
- value:
323
- The expected value from the signal.
324
-
325
- Notes
326
- -----
327
- Example usage::
328
- await assert_value(signal, value)
329
-
330
- """
331
- actual_value = await signal.get_value()
332
- assert actual_value == value, _generate_assert_error_msg(
333
- name=signal.name,
334
- expected_result=value,
335
- actual_result=actual_value,
336
- )
337
-
338
-
339
- async def assert_reading(
340
- readable: AsyncReadable, expected_reading: Mapping[str, Reading]
341
- ) -> None:
342
- """Assert readings from readable.
343
-
344
- Parameters
345
- ----------
346
- readable:
347
- Callable with readable.read function that generate readings.
348
-
349
- reading:
350
- The expected readings from the readable.
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
351
327
 
352
328
  Notes
353
329
  -----
354
- Example usage::
355
- await assert_reading(readable, reading)
356
-
357
- """
358
- actual_reading = await readable.read()
359
- assert expected_reading == actual_reading, _generate_assert_error_msg(
360
- name=readable.name,
361
- expected_result=expected_reading,
362
- actual_result=actual_reading,
363
- )
364
-
365
-
366
- async def assert_configuration(
367
- configurable: AsyncConfigurable,
368
- configuration: Mapping[str, Reading],
369
- ) -> None:
370
- """Assert readings from Configurable.
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.
371
333
 
372
- Parameters
373
- ----------
374
- configurable:
375
- Configurable with Configurable.read function that generate readings.
376
-
377
- configuration:
378
- The expected readings from configurable.
379
-
380
- Notes
381
- -----
382
334
  Example usage::
383
- await assert_configuration(configurable configuration)
384
335
 
336
+ async for value in observe_value(sig):
337
+ do_something_with(value)
385
338
  """
386
- actual_configurable = await configurable.read_configuration()
387
- assert configuration == actual_configurable, _generate_assert_error_msg(
388
- name=configurable.name,
389
- expected_result=configuration,
390
- actual_result=actual_configurable,
391
- )
392
-
393
339
 
394
- def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
395
- """Assert emitted document generated by running a Bluesky plan
396
-
397
- Parameters
398
- ----------
399
- Doc:
400
- A dictionary
340
+ async for _, value in observe_signals_value(
341
+ signal,
342
+ timeout=timeout,
343
+ done_status=done_status,
344
+ done_timeout=done_timeout,
345
+ ):
346
+ yield value
401
347
 
402
- numbers:
403
- expected emission in kwarg from
404
348
 
405
- Notes
406
- -----
407
- Example usage::
408
- assert_emitted(docs, start=1, descriptor=1,
409
- resource=1, datum=1, event=1, stop=1)
410
- """
411
- assert list(docs) == list(numbers), _generate_assert_error_msg(
412
- name="documents",
413
- expected_result=list(numbers),
414
- actual_result=list(docs),
415
- )
416
- actual_numbers = {name: len(d) for name, d in docs.items()}
417
- assert actual_numbers == numbers, _generate_assert_error_msg(
418
- name="emitted",
419
- expected_result=numbers,
420
- actual_result=actual_numbers,
421
- )
349
+ def _get_iteration_timeout(
350
+ timeout: float | None, overall_deadline: float | None
351
+ ) -> float | None:
352
+ overall_deadline = overall_deadline - time.monotonic() if overall_deadline else None
353
+ return min([x for x in [overall_deadline, timeout] if x is not None], default=None)
422
354
 
423
355
 
424
- async def observe_value(
425
- signal: SignalR[SignalDatatypeT],
356
+ async def observe_signals_value(
357
+ *signals: SignalR[SignalDatatypeT],
426
358
  timeout: float | None = None,
427
359
  done_status: Status | None = None,
428
- ) -> AsyncGenerator[SignalDatatypeT, None]:
360
+ done_timeout: float | None = None,
361
+ ) -> AsyncGenerator[tuple[SignalR[SignalDatatypeT], SignalDatatypeT], None]:
429
362
  """Subscribe to the value of a signal so it can be iterated from.
430
363
 
431
364
  Parameters
432
365
  ----------
433
- signal:
434
- Call subscribe_value on this at the start, and clear_sub on it at the
366
+ signals:
367
+ Call subscribe_value on all the signals at the start, and clear_sub on it at the
435
368
  end
436
369
  timeout:
437
370
  If given, how long to wait for each updated value in seconds. If an update
@@ -439,36 +372,57 @@ async def observe_value(
439
372
  done_status:
440
373
  If this status is complete, stop observing and make the iterator return.
441
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
442
379
 
443
380
  Notes
444
381
  -----
445
382
  Example usage::
446
383
 
447
- async for value in observe_value(sig):
448
- do_something_with(value)
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)
449
389
  """
390
+ q: asyncio.Queue[tuple[SignalR[SignalDatatypeT], SignalDatatypeT] | Status] = (
391
+ asyncio.Queue()
392
+ )
393
+
394
+ cbs: dict[SignalR, Callback] = {}
395
+ for signal in signals:
450
396
 
451
- q: asyncio.Queue[SignalDatatypeT | Status] = asyncio.Queue()
397
+ def queue_value(value: SignalDatatypeT, signal=signal):
398
+ q.put_nowait((signal, value))
399
+
400
+ cbs[signal] = queue_value
401
+ signal.subscribe_value(queue_value)
452
402
 
453
403
  if done_status is not None:
454
404
  done_status.add_callback(q.put_nowait)
455
-
456
- signal.subscribe_value(q.put_nowait)
405
+ overall_deadline = time.monotonic() + done_timeout if done_timeout else None
457
406
  try:
458
407
  while True:
459
- # yield here in case something else is filling the queue
460
- # like in test_observe_value_times_out_with_no_external_task()
461
- await asyncio.sleep(0)
462
- item = await asyncio.wait_for(q.get(), timeout)
408
+ if overall_deadline and time.monotonic() >= overall_deadline:
409
+ raise asyncio.TimeoutError(
410
+ f"observe_value was still observing signals "
411
+ f"{[signal.source for signal in signals]} after "
412
+ f"timeout {done_timeout}s"
413
+ )
414
+ iteration_timeout = _get_iteration_timeout(timeout, overall_deadline)
415
+ item = await asyncio.wait_for(q.get(), iteration_timeout)
463
416
  if done_status and item is done_status:
464
417
  if exc := done_status.exception():
465
418
  raise exc
466
419
  else:
467
420
  break
468
421
  else:
469
- yield cast(SignalDatatypeT, item)
422
+ yield cast(tuple[SignalR[SignalDatatypeT], SignalDatatypeT], item)
470
423
  finally:
471
- signal.clear_sub(q.put_nowait)
424
+ for signal, cb in cbs.items():
425
+ signal.clear_sub(cb)
472
426
 
473
427
 
474
428
  class _ValueChecker(Generic[SignalDatatypeT]):
@@ -533,15 +487,16 @@ async def wait_for_value(
533
487
  async def set_and_wait_for_other_value(
534
488
  set_signal: SignalW[SignalDatatypeT],
535
489
  set_value: SignalDatatypeT,
536
- read_signal: SignalR[SignalDatatypeV],
537
- read_value: SignalDatatypeV,
490
+ match_signal: SignalR[SignalDatatypeV],
491
+ match_value: SignalDatatypeV | Callable[[SignalDatatypeV], bool],
538
492
  timeout: float = DEFAULT_TIMEOUT,
539
493
  set_timeout: float | None = None,
494
+ wait_for_set_completion: bool = True,
540
495
  ) -> AsyncStatus:
541
496
  """Set a signal and monitor another signal until it has the specified value.
542
497
 
543
498
  This function sets a set_signal to a specified set_value and waits for
544
- a read_signal to have the read_value.
499
+ a match_signal to have the match_value.
545
500
 
546
501
  Parameters
547
502
  ----------
@@ -549,14 +504,16 @@ async def set_and_wait_for_other_value(
549
504
  The signal to set
550
505
  set_value:
551
506
  The value to set it to
552
- read_signal:
507
+ match_signal:
553
508
  The signal to monitor
554
- read_value:
509
+ match_value:
555
510
  The value to wait for
556
511
  timeout:
557
512
  How long to wait for the signal to have the value
558
513
  set_timeout:
559
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
560
517
 
561
518
  Notes
562
519
  -----
@@ -565,7 +522,7 @@ async def set_and_wait_for_other_value(
565
522
  set_and_wait_for_value(device.acquire, 1, device.acquire_rbv, 1)
566
523
  """
567
524
  # Start monitoring before the set to avoid a race condition
568
- values_gen = observe_value(read_signal)
525
+ values_gen = observe_value(match_signal)
569
526
 
570
527
  # Get the initial value from the monitor to make sure we've created it
571
528
  current_value = await anext(values_gen)
@@ -573,28 +530,33 @@ async def set_and_wait_for_other_value(
573
530
  status = set_signal.set(set_value, timeout=set_timeout)
574
531
 
575
532
  # If the value was the same as before no need to wait for it to change
576
- if current_value != read_value:
533
+ if current_value != match_value:
577
534
 
578
535
  async def _wait_for_value():
579
536
  async for value in values_gen:
580
- if value == read_value:
537
+ if value == match_value:
581
538
  break
582
539
 
583
540
  try:
584
541
  await asyncio.wait_for(_wait_for_value(), timeout)
542
+ if wait_for_set_completion:
543
+ await status
544
+ return status
585
545
  except asyncio.TimeoutError as e:
586
546
  raise TimeoutError(
587
- f"{read_signal.name} didn't match {read_value} in {timeout}s"
547
+ f"{match_signal.name} didn't match {match_value} in {timeout}s"
588
548
  ) from e
589
549
 
590
- return status
550
+ return completed_status()
591
551
 
592
552
 
593
553
  async def set_and_wait_for_value(
594
554
  signal: SignalRW[SignalDatatypeT],
595
555
  value: SignalDatatypeT,
556
+ match_value: SignalDatatypeT | Callable[[SignalDatatypeT], bool] | None = None,
596
557
  timeout: float = DEFAULT_TIMEOUT,
597
558
  status_timeout: float | None = None,
559
+ wait_for_set_completion: bool = True,
598
560
  ) -> AsyncStatus:
599
561
  """Set a signal and monitor it until it has that value.
600
562
 
@@ -609,10 +571,15 @@ async def set_and_wait_for_value(
609
571
  The signal to set
610
572
  value:
611
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.
612
577
  timeout:
613
578
  How long to wait for the signal to have the value
614
579
  status_timeout:
615
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
616
583
 
617
584
  Notes
618
585
  -----
@@ -620,6 +587,14 @@ async def set_and_wait_for_value(
620
587
 
621
588
  set_and_wait_for_value(device.acquire, 1)
622
589
  """
590
+ if match_value is None:
591
+ match_value = value
623
592
  return await set_and_wait_for_other_value(
624
- signal, value, signal, value, timeout, status_timeout
593
+ signal,
594
+ value,
595
+ signal,
596
+ match_value,
597
+ timeout,
598
+ status_timeout,
599
+ wait_for_set_completion,
625
600
  )