ophyd-async 0.9.0a1__py3-none-any.whl → 0.9.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 (97) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +13 -20
  3. ophyd_async/core/_detector.py +61 -37
  4. ophyd_async/core/_device.py +102 -80
  5. ophyd_async/core/_device_filler.py +17 -8
  6. ophyd_async/core/_flyer.py +2 -2
  7. ophyd_async/core/_readable.py +30 -23
  8. ophyd_async/core/_settings.py +104 -0
  9. ophyd_async/core/_signal.py +55 -17
  10. ophyd_async/core/_signal_backend.py +4 -1
  11. ophyd_async/core/_soft_signal_backend.py +2 -1
  12. ophyd_async/core/_table.py +18 -10
  13. ophyd_async/core/_utils.py +5 -3
  14. ophyd_async/core/_yaml_settings.py +64 -0
  15. ophyd_async/epics/adandor/__init__.py +9 -0
  16. ophyd_async/epics/adandor/_andor.py +45 -0
  17. ophyd_async/epics/adandor/_andor_controller.py +49 -0
  18. ophyd_async/epics/adandor/_andor_io.py +36 -0
  19. ophyd_async/epics/adaravis/__init__.py +3 -1
  20. ophyd_async/epics/adaravis/_aravis.py +23 -37
  21. ophyd_async/epics/adaravis/_aravis_controller.py +13 -22
  22. ophyd_async/epics/adcore/__init__.py +15 -8
  23. ophyd_async/epics/adcore/_core_detector.py +41 -0
  24. ophyd_async/epics/adcore/_core_io.py +35 -10
  25. ophyd_async/epics/adcore/_core_logic.py +98 -86
  26. ophyd_async/epics/adcore/_core_writer.py +219 -0
  27. ophyd_async/epics/adcore/_hdf_writer.py +38 -62
  28. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  29. ophyd_async/epics/adcore/_single_trigger.py +4 -3
  30. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  31. ophyd_async/epics/adcore/_utils.py +2 -1
  32. ophyd_async/epics/adkinetix/_kinetix.py +29 -24
  33. ophyd_async/epics/adkinetix/_kinetix_controller.py +9 -21
  34. ophyd_async/epics/adpilatus/__init__.py +2 -2
  35. ophyd_async/epics/adpilatus/_pilatus.py +27 -39
  36. ophyd_async/epics/adpilatus/_pilatus_controller.py +44 -22
  37. ophyd_async/epics/adsimdetector/__init__.py +3 -3
  38. ophyd_async/epics/adsimdetector/_sim.py +33 -17
  39. ophyd_async/epics/advimba/_vimba.py +23 -23
  40. ophyd_async/epics/advimba/_vimba_controller.py +10 -24
  41. ophyd_async/epics/core/_aioca.py +31 -14
  42. ophyd_async/epics/core/_p4p.py +40 -16
  43. ophyd_async/epics/core/_util.py +1 -1
  44. ophyd_async/epics/motor.py +18 -10
  45. ophyd_async/epics/sim/_ioc.py +29 -0
  46. ophyd_async/epics/{demo → sim}/_mover.py +10 -4
  47. ophyd_async/epics/testing/__init__.py +14 -14
  48. ophyd_async/epics/testing/_example_ioc.py +48 -65
  49. ophyd_async/epics/testing/_utils.py +17 -45
  50. ophyd_async/epics/testing/test_records.db +8 -0
  51. ophyd_async/fastcs/panda/__init__.py +0 -2
  52. ophyd_async/fastcs/panda/_control.py +7 -2
  53. ophyd_async/fastcs/panda/_hdf_panda.py +3 -1
  54. ophyd_async/fastcs/panda/_table.py +4 -1
  55. ophyd_async/plan_stubs/__init__.py +14 -0
  56. ophyd_async/plan_stubs/_ensure_connected.py +11 -17
  57. ophyd_async/plan_stubs/_fly.py +1 -1
  58. ophyd_async/plan_stubs/_nd_attributes.py +7 -5
  59. ophyd_async/plan_stubs/_panda.py +13 -0
  60. ophyd_async/plan_stubs/_settings.py +125 -0
  61. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  62. ophyd_async/sim/__init__.py +19 -0
  63. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
  64. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
  65. ophyd_async/tango/core/_signal.py +3 -1
  66. ophyd_async/tango/core/_tango_transport.py +12 -14
  67. ophyd_async/tango/{demo → sim}/_mover.py +5 -2
  68. ophyd_async/testing/__init__.py +19 -0
  69. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  70. ophyd_async/testing/_assert.py +88 -40
  71. ophyd_async/testing/_mock_signal_utils.py +3 -3
  72. ophyd_async/testing/_one_of_everything.py +126 -0
  73. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/METADATA +2 -2
  74. ophyd_async-0.9.0a2.dist-info/RECORD +129 -0
  75. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/WHEEL +1 -1
  76. ophyd_async/core/_device_save_loader.py +0 -274
  77. ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
  78. ophyd_async/fastcs/panda/_utils.py +0 -16
  79. ophyd_async/sim/demo/__init__.py +0 -19
  80. ophyd_async/sim/testing/__init__.py +0 -0
  81. ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
  82. ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
  83. /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
  84. /ophyd_async/epics/{demo → sim}/_sensor.py +0 -0
  85. /ophyd_async/epics/{demo → sim}/mover.db +0 -0
  86. /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
  87. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
  88. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
  89. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
  90. /ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +0 -0
  91. /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
  92. /ophyd_async/tango/{demo → sim}/_counter.py +0 -0
  93. /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
  94. /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
  95. /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
  96. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/LICENSE +0 -0
  97. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Callable, Mapping
5
+ from typing import Any
6
+
7
+ import bluesky.plan_stubs as bps
8
+ import numpy as np
9
+ from bluesky.utils import MsgGenerator, plan
10
+
11
+ from ophyd_async.core import (
12
+ Device,
13
+ Settings,
14
+ SettingsProvider,
15
+ SignalRW,
16
+ T,
17
+ walk_rw_signals,
18
+ )
19
+ from ophyd_async.core._table import Table
20
+
21
+ from ._wait_for_awaitable import wait_for_awaitable
22
+
23
+
24
+ @plan
25
+ def _get_values_of_signals(
26
+ signals: Mapping[T, SignalRW],
27
+ ) -> MsgGenerator[dict[T, Any]]:
28
+ coros = [sig.get_value() for sig in signals.values()]
29
+ values = yield from wait_for_awaitable(asyncio.gather(*coros))
30
+ named_values = dict(zip(signals, values, strict=True))
31
+ return named_values
32
+
33
+
34
+ @plan
35
+ def get_current_settings(device: Device) -> MsgGenerator[Settings]:
36
+ signals = walk_rw_signals(device)
37
+ named_values = yield from _get_values_of_signals(signals)
38
+ signal_values = {signals[name]: value for name, value in named_values.items()}
39
+ return Settings(device, signal_values)
40
+
41
+
42
+ @plan
43
+ def store_settings(
44
+ provider: SettingsProvider, name: str, device: Device
45
+ ) -> MsgGenerator[None]:
46
+ """Walk a Device for SignalRWs and store their values with a provider associated
47
+ with the given name.
48
+ """
49
+ signals = walk_rw_signals(device)
50
+ named_values = yield from _get_values_of_signals(signals)
51
+ yield from wait_for_awaitable(provider.store(name, named_values))
52
+
53
+
54
+ @plan
55
+ def retrieve_settings(
56
+ provider: SettingsProvider, name: str, device: Device
57
+ ) -> MsgGenerator[Settings]:
58
+ """Retrieve named Settings for a Device from a provider."""
59
+ named_values = yield from wait_for_awaitable(provider.retrieve(name))
60
+ signals = walk_rw_signals(device)
61
+ unknown_names = set(named_values) - set(signals)
62
+ if unknown_names:
63
+ raise NameError(f"Unknown signal names {sorted(unknown_names)}")
64
+ signal_values = {signals[name]: value for name, value in named_values.items()}
65
+ return Settings(device, signal_values)
66
+
67
+
68
+ @plan
69
+ def apply_settings(settings: Settings) -> MsgGenerator[None]:
70
+ """Set every SignalRW to the given value in Settings. If value is None ignore it."""
71
+ signal_values = {
72
+ signal: value for signal, value in settings.items() if value is not None
73
+ }
74
+ if signal_values:
75
+ for signal, value in signal_values.items():
76
+ yield from bps.abs_set(signal, value, group="apply_settings")
77
+ yield from bps.wait("apply_settings")
78
+
79
+
80
+ @plan
81
+ def apply_settings_if_different(
82
+ settings: Settings,
83
+ apply_plan: Callable[[Settings], MsgGenerator[None]],
84
+ current_settings: Settings | None = None,
85
+ ) -> MsgGenerator[None]:
86
+ """Set every SignalRW in settings to its given value if it is different to the
87
+ current value.
88
+
89
+ Parameters
90
+ ----------
91
+ apply_plan:
92
+ A device specific plan which takes the Settings to apply and applies them to
93
+ the Device. Used to add device specific ordering to setting the signals.
94
+ current_settings:
95
+ If given, should be a superset of settings containing the current value of
96
+ the Settings in the Device. If not given it will be created by reading just
97
+ the signals given in settings.
98
+ """
99
+ if current_settings is None:
100
+ # If we aren't give the current settings, then get the
101
+ # values of just the signals we were asked to change.
102
+ # This allows us to use this plan with Settings for a subset
103
+ # of signals in the Device without retrieving them all
104
+ signal_values = yield from _get_values_of_signals(
105
+ {sig: sig for sig in settings}
106
+ )
107
+ current_settings = Settings(settings.device, signal_values)
108
+
109
+ def _is_different(current, required) -> bool:
110
+ if isinstance(current, Table):
111
+ current = current.model_dump()
112
+ if isinstance(required, Table):
113
+ required = required.model_dump()
114
+ return current.keys() != required.keys() or any(
115
+ _is_different(current[k], required[k]) for k in current
116
+ )
117
+ elif isinstance(current, np.ndarray):
118
+ return not np.array_equal(current, required)
119
+ else:
120
+ return current != required
121
+
122
+ settings_to_change, _ = settings.partition(
123
+ lambda sig: _is_different(current_settings[sig], settings[sig])
124
+ )
125
+ yield from apply_plan(settings_to_change)
@@ -0,0 +1,13 @@
1
+ from collections.abc import Awaitable
2
+
3
+ import bluesky.plan_stubs as bps
4
+ from bluesky.utils import MsgGenerator, plan
5
+
6
+ from ophyd_async.core import T
7
+
8
+
9
+ @plan
10
+ def wait_for_awaitable(coro: Awaitable[T]) -> MsgGenerator[T]:
11
+ """Wait for a single awaitable to complete, and return the result."""
12
+ (task,) = yield from bps.wait_for([lambda: coro])
13
+ return task.result()
@@ -0,0 +1,19 @@
1
+ from ._pattern_detector import (
2
+ DATA_PATH,
3
+ SUM_PATH,
4
+ PatternDetector,
5
+ PatternDetectorController,
6
+ PatternDetectorWriter,
7
+ PatternGenerator,
8
+ )
9
+ from ._sim_motor import SimMotor
10
+
11
+ __all__ = [
12
+ "DATA_PATH",
13
+ "SUM_PATH",
14
+ "PatternGenerator",
15
+ "PatternDetector",
16
+ "PatternDetectorController",
17
+ "PatternDetectorWriter",
18
+ "SimMotor",
19
+ ]
@@ -27,8 +27,15 @@ class PatternDetectorController(DetectorController):
27
27
  )
28
28
 
29
29
  async def arm(self):
30
- assert self._trigger_info.livetime
31
- assert self.period
30
+ if not hasattr(self, "_trigger_info"):
31
+ msg = "TriggerInfo information is missing, has 'prepare' been called?"
32
+ raise RuntimeError(msg)
33
+ if not self._trigger_info.livetime:
34
+ msg = "Livetime information is missing in trigger info"
35
+ raise ValueError(msg)
36
+ if not self.period:
37
+ msg = "Period is not set"
38
+ raise ValueError(msg)
32
39
  self.task = asyncio.create_task(
33
40
  self._coroutine_for_image_writing(
34
41
  self._trigger_info.livetime,
@@ -67,11 +67,14 @@ class PatternGenerator:
67
67
 
68
68
  def write_data_to_dataset(self, path: str, data_shape: tuple[int, ...], data):
69
69
  """Write data to named dataset, resizing to fit and flushing after."""
70
- assert self._handle_for_h5_file, "no file has been opened!"
70
+ if not self._handle_for_h5_file:
71
+ msg = "No file has been opened!"
72
+ raise OSError(msg)
73
+
71
74
  dset = self._handle_for_h5_file[path]
72
- assert isinstance(
73
- dset, h5py.Dataset
74
- ), f"Expected {path} to be dataset, got {dset}"
75
+ if not isinstance(dset, h5py.Dataset):
76
+ msg = f"Expected {path} to be a dataset, got {type(dset).__name__}"
77
+ raise TypeError(msg)
75
78
  dset.resize((self.image_counter + 1,) + data_shape)
76
79
  dset[self.image_counter] = data
77
80
  dset.flush()
@@ -114,7 +117,9 @@ class PatternGenerator:
114
117
 
115
118
  self._handle_for_h5_file = h5py.File(self.target_path, "w", libver="latest")
116
119
 
117
- assert self._handle_for_h5_file, "not loaded the file right"
120
+ if not self._handle_for_h5_file:
121
+ msg = f"Problem opening file {self.target_path}"
122
+ raise OSError(msg)
118
123
 
119
124
  self._handle_for_h5_file.create_dataset(
120
125
  name=DATA_PATH,
@@ -184,7 +189,9 @@ class PatternGenerator:
184
189
  # cannot get the full filename the HDF writer will write
185
190
  # until the first frame comes in
186
191
  if not self._hdf_stream_provider:
187
- assert self.target_path, "open file has not been called"
192
+ if self.target_path is None:
193
+ msg = "open file has not been called"
194
+ raise RuntimeError(msg)
188
195
  self._hdf_stream_provider = HDFFile(
189
196
  self.target_path,
190
197
  self._datasets,
@@ -28,6 +28,8 @@ from tango.asyncio import DeviceProxy as AsyncDeviceProxy
28
28
 
29
29
  from ._tango_transport import TangoSignalBackend, get_python_type
30
30
 
31
+ logger = logging.getLogger("ophyd_async")
32
+
31
33
 
32
34
  def make_backend(
33
35
  datatype: type[SignalDatatypeT] | None,
@@ -205,7 +207,7 @@ async def infer_signal_type(
205
207
  if config.in_type == CmdArgType.DevVoid:
206
208
  return SignalX
207
209
  elif config.in_type != config.out_type:
208
- logging.debug("Commands with different in and out dtypes are not supported")
210
+ logger.debug("Commands with different in and out dtypes are not supported")
209
211
  return None
210
212
  else:
211
213
  return SignalRW
@@ -210,11 +210,11 @@ class AttributeProxy(TangoProxy):
210
210
  await asyncio.sleep(A_BIT)
211
211
  if to and (time.time() - start_time > to):
212
212
  raise TimeoutError(
213
- f"{self._name} attr put failed:" f" Timeout"
213
+ f"{self._name} attr put failed: Timeout"
214
214
  ) from exc
215
215
  else:
216
216
  raise RuntimeError(
217
- f"{self._name} device failure:" f" {exc.args[0].desc}"
217
+ f"{self._name} device failure: {exc.args[0].desc}"
218
218
  ) from exc
219
219
 
220
220
  return AsyncStatus(wait_for_reply(rid, timeout))
@@ -422,7 +422,7 @@ class CommandProxy(TangoProxy):
422
422
  raise TimeoutError(f"{self._name} command failed: Timeout") from te
423
423
  except DevFailed as de:
424
424
  raise RuntimeError(
425
- f"{self._name} device" f" failure: {de.args[0].desc}"
425
+ f"{self._name} device failure: {de.args[0].desc}"
426
426
  ) from de
427
427
 
428
428
  else:
@@ -446,8 +446,7 @@ class CommandProxy(TangoProxy):
446
446
  ) from de_exc
447
447
  else:
448
448
  raise RuntimeError(
449
- f"{self._name} device failure:"
450
- f" {de_exc.args[0].desc}"
449
+ f"{self._name} device failure: {de_exc.args[0].desc}"
451
450
  ) from de_exc
452
451
 
453
452
  return AsyncStatus(wait_for_reply(rid, timeout))
@@ -733,22 +732,21 @@ class TangoSignalBackend(SignalBackend[SignalDatatypeT]):
733
732
  " for which polling is disabled."
734
733
  )
735
734
 
735
+ if callback and self.proxies[self.read_trl].has_subscription(): # type: ignore
736
+ msg = "Cannot set a callback when one is already set"
737
+ raise RuntimeError(msg)
738
+
739
+ if self.proxies[self.read_trl].has_subscription(): # type: ignore
740
+ self.proxies[self.read_trl].unsubscribe_callback() # type: ignore
741
+
736
742
  if callback:
737
743
  try:
738
- assert not self.proxies[self.read_trl].has_subscription() # type: ignore
739
744
  self.proxies[self.read_trl].subscribe_callback(callback) # type: ignore
740
- except AssertionError as ae:
741
- raise RuntimeError(
742
- "Cannot set a callback when one" " is already set"
743
- ) from ae
744
745
  except RuntimeError as exc:
745
746
  raise RuntimeError(
746
- f"Cannot set callback" f" for {self.read_trl}. {exc}"
747
+ f"Cannot set callback for {self.read_trl}. {exc}"
747
748
  ) from exc
748
749
 
749
- else:
750
- self.proxies[self.read_trl].unsubscribe_callback() # type: ignore
751
-
752
750
  def set_polling(
753
751
  self,
754
752
  allow_polling: bool = True,
@@ -43,8 +43,11 @@ class TangoMover(TangoReadable, Movable, Stoppable):
43
43
  (old_position, velocity) = await asyncio.gather(
44
44
  self.position.get_value(), self.velocity.get_value()
45
45
  )
46
- if timeout is CALCULATE_TIMEOUT:
47
- assert velocity > 0, "Motor has zero velocity"
46
+ # TODO: check whether Tango does work with negative velocity
47
+ if timeout is CALCULATE_TIMEOUT and velocity == 0:
48
+ msg = "Motor has zero velocity"
49
+ raise ValueError(msg)
50
+ else:
48
51
  timeout = abs(value - old_position) / velocity + DEFAULT_TIMEOUT
49
52
 
50
53
  if not (isinstance(timeout, float) or timeout is None):
@@ -1,5 +1,10 @@
1
+ from . import __pytest_assert_rewrite # noqa: F401
1
2
  from ._assert import (
3
+ ApproxTable,
4
+ MonitorQueue,
5
+ approx_value,
2
6
  assert_configuration,
7
+ assert_describe_signal,
3
8
  assert_emitted,
4
9
  assert_reading,
5
10
  assert_value,
@@ -14,10 +19,18 @@ from ._mock_signal_utils import (
14
19
  set_mock_value,
15
20
  set_mock_values,
16
21
  )
22
+ from ._one_of_everything import (
23
+ ExampleEnum,
24
+ ExampleTable,
25
+ OneOfEverythingDevice,
26
+ ParentOfEverythingDevice,
27
+ )
17
28
  from ._wait_for_pending import wait_for_pending_wakeups
18
29
 
19
30
  __all__ = [
31
+ "approx_value",
20
32
  "assert_configuration",
33
+ "assert_describe_signal",
21
34
  "assert_emitted",
22
35
  "assert_reading",
23
36
  "assert_value",
@@ -30,4 +43,10 @@ __all__ = [
30
43
  "set_mock_value",
31
44
  "set_mock_values",
32
45
  "wait_for_pending_wakeups",
46
+ "ExampleEnum",
47
+ "ExampleTable",
48
+ "OneOfEverythingDevice",
49
+ "ParentOfEverythingDevice",
50
+ "MonitorQueue",
51
+ "ApproxTable",
33
52
  ]
@@ -0,0 +1,4 @@
1
+ import pytest
2
+
3
+ # So that bare asserts give a nice pytest traceback
4
+ pytest.register_assert_rewrite("ophyd_async.testing._assert")
@@ -1,20 +1,23 @@
1
- from collections.abc import Mapping
1
+ import asyncio
2
+ import time
3
+ from contextlib import AbstractContextManager
2
4
  from typing import Any
3
5
 
6
+ import pytest
4
7
  from bluesky.protocols import Reading
8
+ from event_model import DataKey
5
9
 
6
- from ophyd_async.core import AsyncConfigurable, AsyncReadable, SignalDatatypeT, SignalR
10
+ from ophyd_async.core import (
11
+ AsyncConfigurable,
12
+ AsyncReadable,
13
+ SignalDatatypeT,
14
+ SignalR,
15
+ Table,
16
+ )
7
17
 
8
18
 
9
- def _generate_assert_error_msg(name: str, expected_result, actual_result) -> str:
10
- WARNING = "\033[93m"
11
- FAIL = "\033[91m"
12
- ENDC = "\033[0m"
13
- return (
14
- f"Expected {WARNING}{name}{ENDC} to produce"
15
- + f"\n{FAIL}{expected_result}{ENDC}"
16
- + f"\nbut actually got \n{FAIL}{actual_result}{ENDC}"
17
- )
19
+ def approx_value(value: Any):
20
+ return ApproxTable(value) if isinstance(value, Table) else pytest.approx(value)
18
21
 
19
22
 
20
23
  async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
@@ -34,15 +37,11 @@ async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
34
37
 
35
38
  """
36
39
  actual_value = await signal.get_value()
37
- assert actual_value == value, _generate_assert_error_msg(
38
- name=signal.name,
39
- expected_result=value,
40
- actual_result=actual_value,
41
- )
40
+ assert approx_value(value) == actual_value
42
41
 
43
42
 
44
43
  async def assert_reading(
45
- readable: AsyncReadable, expected_reading: Mapping[str, Reading]
44
+ readable: AsyncReadable, expected_reading: dict[str, Reading]
46
45
  ) -> None:
47
46
  """Assert readings from readable.
48
47
 
@@ -61,16 +60,16 @@ async def assert_reading(
61
60
 
62
61
  """
63
62
  actual_reading = await readable.read()
64
- assert expected_reading == actual_reading, _generate_assert_error_msg(
65
- name=readable.name,
66
- expected_result=expected_reading,
67
- actual_result=actual_reading,
68
- )
63
+ approx_expected_reading = {
64
+ k: dict(v, value=approx_value(expected_reading[k]["value"]))
65
+ for k, v in expected_reading.items()
66
+ }
67
+ assert actual_reading == approx_expected_reading
69
68
 
70
69
 
71
70
  async def assert_configuration(
72
71
  configurable: AsyncConfigurable,
73
- configuration: Mapping[str, Reading],
72
+ configuration: dict[str, Reading],
74
73
  ) -> None:
75
74
  """Assert readings from Configurable.
76
75
 
@@ -88,15 +87,23 @@ async def assert_configuration(
88
87
  await assert_configuration(configurable configuration)
89
88
 
90
89
  """
91
- actual_configurable = await configurable.read_configuration()
92
- assert configuration == actual_configurable, _generate_assert_error_msg(
93
- name=configurable.name,
94
- expected_result=configuration,
95
- actual_result=actual_configurable,
96
- )
90
+ actual_configuration = await configurable.read_configuration()
91
+ approx_expected_configuration = {
92
+ k: dict(v, value=approx_value(configuration[k]["value"]))
93
+ for k, v in configuration.items()
94
+ }
95
+ assert actual_configuration == approx_expected_configuration
97
96
 
98
97
 
99
- def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
98
+ async def assert_describe_signal(signal: SignalR, /, **metadata):
99
+ actual_describe = await signal.describe()
100
+ assert list(actual_describe) == [signal.name]
101
+ (actual_datakey,) = actual_describe.values()
102
+ expected_datakey = DataKey(source=signal.source, **metadata)
103
+ assert actual_datakey == expected_datakey
104
+
105
+
106
+ def assert_emitted(docs: dict[str, list[dict]], **numbers: int):
100
107
  """Assert emitted document generated by running a Bluesky plan
101
108
 
102
109
  Parameters
@@ -115,14 +122,55 @@ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
115
122
  RE(my_plan())
116
123
  assert_emitted(docs, start=1, descriptor=1, event=1, stop=1)
117
124
  """
118
- assert list(docs) == list(numbers), _generate_assert_error_msg(
119
- name="documents",
120
- expected_result=list(numbers),
121
- actual_result=list(docs),
122
- )
125
+ assert list(docs) == list(numbers)
123
126
  actual_numbers = {name: len(d) for name, d in docs.items()}
124
- assert actual_numbers == numbers, _generate_assert_error_msg(
125
- name="emitted",
126
- expected_result=numbers,
127
- actual_result=actual_numbers,
128
- )
127
+ assert actual_numbers == numbers
128
+
129
+
130
+ class ApproxTable:
131
+ def __init__(self, expected: Table, rel=None, abs=None, nan_ok: bool = False):
132
+ self.expected = expected
133
+ self.rel = rel
134
+ self.abs = abs
135
+ self.nan_ok = nan_ok
136
+
137
+ def __eq__(self, value):
138
+ approx_fields = {
139
+ k: pytest.approx(v, self.rel, self.abs, self.nan_ok)
140
+ for k, v in self.expected
141
+ }
142
+ expected = type(self.expected).model_construct(**approx_fields) # type: ignore
143
+ return expected == value
144
+
145
+
146
+ class MonitorQueue(AbstractContextManager):
147
+ def __init__(self, signal: SignalR):
148
+ self.signal = signal
149
+ self.updates: asyncio.Queue[dict[str, Reading]] = asyncio.Queue()
150
+ self.signal.subscribe(self.updates.put_nowait)
151
+
152
+ async def assert_updates(self, expected_value):
153
+ # Get an update, value and reading
154
+ expected_type = type(expected_value)
155
+ expected_value = approx_value(expected_value)
156
+ update = await self.updates.get()
157
+ value = await self.signal.get_value()
158
+ reading = await self.signal.read()
159
+ # Check they match what we expected
160
+ assert value == expected_value
161
+ assert type(value) is expected_type
162
+ expected_reading = {
163
+ self.signal.name: {
164
+ "value": expected_value,
165
+ "timestamp": pytest.approx(time.time(), rel=0.1),
166
+ "alarm_severity": 0,
167
+ }
168
+ }
169
+ assert reading == update == expected_reading
170
+
171
+ def __enter__(self):
172
+ self.signal.subscribe(self.updates.put_nowait)
173
+ return self
174
+
175
+ def __exit__(self, exc_type, exc_value, traceback):
176
+ self.signal.clear_sub(self.updates.put_nowait)
@@ -22,9 +22,9 @@ def get_mock(device: Device | Signal) -> Mock:
22
22
  def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend:
23
23
  connector = signal._connector # noqa: SLF001
24
24
  assert isinstance(connector, SignalConnector), f"Expected Signal, got {signal}"
25
- assert isinstance(
26
- connector.backend, MockSignalBackend
27
- ), f"Signal {signal} not connected in mock mode"
25
+ assert isinstance(connector.backend, MockSignalBackend), (
26
+ f"Signal {signal} not connected in mock mode"
27
+ )
28
28
  return connector.backend
29
29
 
30
30
 
@@ -0,0 +1,126 @@
1
+ from collections.abc import Sequence
2
+ from typing import Any
3
+
4
+ import numpy as np
5
+
6
+ from ophyd_async.core import (
7
+ Array1D,
8
+ Device,
9
+ DTypeScalar_co,
10
+ SignalRW,
11
+ StandardReadable,
12
+ StrictEnum,
13
+ Table,
14
+ soft_signal_r_and_setter,
15
+ soft_signal_rw,
16
+ )
17
+ from ophyd_async.core import StandardReadableFormat as Format
18
+ from ophyd_async.core._device import DeviceVector
19
+
20
+
21
+ class ExampleEnum(StrictEnum):
22
+ A = "Aaa"
23
+ B = "Bbb"
24
+ C = "Ccc"
25
+
26
+
27
+ class ExampleTable(Table):
28
+ bool: Array1D[np.bool_]
29
+ int: Array1D[np.int32]
30
+ float: Array1D[np.float64]
31
+ str: Sequence[str]
32
+ enum: Sequence[ExampleEnum]
33
+
34
+
35
+ def int_array_signal(
36
+ dtype: type[DTypeScalar_co], name: str = ""
37
+ ) -> SignalRW[Array1D[DTypeScalar_co]]:
38
+ iinfo = np.iinfo(dtype) # type: ignore
39
+ value = np.array([iinfo.min, iinfo.max, 0, 1, 2, 3, 4], dtype=dtype)
40
+ return soft_signal_rw(Array1D[dtype], value, name)
41
+
42
+
43
+ def float_array_signal(
44
+ dtype: type[DTypeScalar_co], name: str = ""
45
+ ) -> SignalRW[Array1D[DTypeScalar_co]]:
46
+ finfo = np.finfo(dtype) # type: ignore
47
+ value = np.array(
48
+ [
49
+ finfo.min,
50
+ finfo.max,
51
+ finfo.smallest_normal,
52
+ finfo.smallest_subnormal,
53
+ 0,
54
+ 1.234,
55
+ 2.34e5,
56
+ 3.45e-6,
57
+ ],
58
+ dtype=dtype,
59
+ )
60
+ return soft_signal_rw(Array1D[dtype], value, name)
61
+
62
+
63
+ class OneOfEverythingDevice(StandardReadable):
64
+ # make a detector to test assert_configuration
65
+ def __init__(self, name=""):
66
+ # add all signals to configuration
67
+ with self.add_children_as_readables(Format.CONFIG_SIGNAL):
68
+ self.int = soft_signal_rw(int, 1)
69
+ self.float = soft_signal_rw(float, 1.234)
70
+ self.str = soft_signal_rw(str, "test_string")
71
+ self.bool = soft_signal_rw(bool, True)
72
+ self.enum = soft_signal_rw(ExampleEnum, ExampleEnum.B)
73
+ self.int8a = int_array_signal(np.int8)
74
+ self.uint8a = int_array_signal(np.uint8)
75
+ self.int16a = int_array_signal(np.int16)
76
+ self.uint16a = int_array_signal(np.uint16)
77
+ self.int32a = int_array_signal(np.int32)
78
+ self.uint32a = int_array_signal(np.uint32)
79
+ self.int64a = int_array_signal(np.int64)
80
+ self.uint64a = int_array_signal(np.uint64)
81
+ self.float32a = float_array_signal(np.float32)
82
+ self.float64a = float_array_signal(np.float64)
83
+ self.stra = soft_signal_rw(
84
+ Sequence[str],
85
+ ["one", "two", "three"],
86
+ )
87
+ self.enuma = soft_signal_rw(
88
+ Sequence[ExampleEnum],
89
+ [ExampleEnum.A, ExampleEnum.C],
90
+ )
91
+ self.table = soft_signal_rw(
92
+ ExampleTable,
93
+ ExampleTable(
94
+ bool=np.array([False, False, True, True], np.bool_),
95
+ int=np.array([1, 8, -9, 32], np.int32),
96
+ float=np.array([1.8, 8.2, -6, 32.9887], np.float64),
97
+ str=["Hello", "World", "Foo", "Bar"],
98
+ enum=[ExampleEnum.A, ExampleEnum.B, ExampleEnum.A, ExampleEnum.C],
99
+ ),
100
+ )
101
+ self.ndarray = soft_signal_rw(np.ndarray, np.array(([1, 2, 3], [4, 5, 6])))
102
+ super().__init__(name)
103
+
104
+
105
+ async def _get_signal_values(child: Device) -> dict[SignalRW, Any]:
106
+ if isinstance(child, SignalRW):
107
+ return {child: await child.get_value()}
108
+ ret = {}
109
+ for _, c in child.children():
110
+ ret.update(await _get_signal_values(c))
111
+ return ret
112
+
113
+
114
+ class ParentOfEverythingDevice(Device):
115
+ def __init__(self, name=""):
116
+ self.child = OneOfEverythingDevice()
117
+ self.vector = DeviceVector(
118
+ {1: OneOfEverythingDevice(), 3: OneOfEverythingDevice()}
119
+ )
120
+ self.sig_rw = soft_signal_rw(str, "Top level SignalRW")
121
+ self.sig_r, _ = soft_signal_r_and_setter(str, "Top level SignalR")
122
+ self._sig_rw = soft_signal_rw(str, "Top level private SignalRW")
123
+ super().__init__(name=name)
124
+
125
+ async def get_signal_values(self):
126
+ return await _get_signal_values(self)