ophyd-async 0.9.0a2__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 (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 +37 -8
  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 +137 -81
  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 +87 -0
  132. ophyd_async/testing/_utils.py +3 -0
  133. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
  134. ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
  135. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.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.0a1.dist-info/licenses}/LICENSE +0 -0
  151. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,200 @@
1
+ import textwrap
2
+ from dataclasses import dataclass
3
+ from typing import Any, Generic, TypeVar
4
+
5
+ import numpy as np
6
+
7
+ from ophyd_async.core import (
8
+ Array1D,
9
+ DTypeScalar_co,
10
+ StrictEnum,
11
+ )
12
+ from ophyd_async.testing import float_array_value, int_array_value
13
+ from tango import AttrDataFormat, AttrWriteType, DevState
14
+ from tango.server import Device, attribute, command
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ class ExampleStrEnum(StrictEnum):
20
+ A = "AAA"
21
+ B = "BBB"
22
+ C = "CCC"
23
+
24
+
25
+ def int_image_value(
26
+ dtype: type[DTypeScalar_co],
27
+ ):
28
+ # how do we type this?
29
+ array_1d = int_array_value(dtype)
30
+ return np.vstack((array_1d, array_1d))
31
+
32
+
33
+ def float_image_value(
34
+ dtype: type[DTypeScalar_co],
35
+ ):
36
+ # how do we type this?
37
+ array_1d = float_array_value(dtype)
38
+ return np.vstack((array_1d, array_1d))
39
+
40
+
41
+ def _valid_command(dformat: AttrDataFormat, dtype: str):
42
+ if dtype == "DevUChar":
43
+ return False
44
+ if dformat != AttrDataFormat.SCALAR and dtype in ["DevState", "DevEnum"]:
45
+ return False
46
+ return True
47
+
48
+
49
+ @dataclass
50
+ class AttributeData(Generic[T]):
51
+ name: str
52
+ tango_type: str
53
+ initial_scalar: T
54
+ initial_spectrum: Array1D
55
+
56
+
57
+ _all_attribute_definitions = [
58
+ AttributeData(
59
+ "str",
60
+ "DevString",
61
+ "test_string",
62
+ np.array(["one", "two", "three"], dtype=str),
63
+ ),
64
+ AttributeData(
65
+ "bool",
66
+ "DevBoolean",
67
+ True,
68
+ np.array([False, True], dtype=bool),
69
+ ),
70
+ AttributeData("strenum", "DevEnum", 1, np.array([0, 1, 2])),
71
+ AttributeData("int8", "DevShort", 1, int_array_value(np.int8)),
72
+ AttributeData("uint8", "DevUChar", 1, int_array_value(np.uint8)),
73
+ AttributeData("int16", "DevShort", 1, int_array_value(np.int16)),
74
+ AttributeData("uint16", "DevUShort", 1, int_array_value(np.uint16)),
75
+ AttributeData("int32", "DevLong", 1, int_array_value(np.int32)),
76
+ AttributeData("uint32", "DevULong", 1, int_array_value(np.uint32)),
77
+ AttributeData("int64", "DevLong64", 1, int_array_value(np.int64)),
78
+ AttributeData("uint64", "DevULong64", 1, int_array_value(np.uint64)),
79
+ AttributeData("float32", "DevFloat", 1.234, float_array_value(np.float32)),
80
+ AttributeData("float64", "DevDouble", 1.234, float_array_value(np.float64)),
81
+ AttributeData(
82
+ "my_state",
83
+ "DevState",
84
+ DevState.INIT,
85
+ np.array([DevState.INIT, DevState.ON, DevState.MOVING], dtype=DevState),
86
+ ),
87
+ ]
88
+
89
+
90
+ class OneOfEverythingTangoDevice(Device):
91
+ attr_values = {}
92
+ initial_values = {}
93
+
94
+ def _add_attr(self, attr: attribute, initial_value):
95
+ self.attr_values[attr.name] = initial_value
96
+ self.initial_values[attr.name] = initial_value
97
+ self.add_attribute(attr)
98
+ self.set_change_event(attr.name, True, False)
99
+
100
+ def add_scalar_attr(self, name: str, dtype: str, initial_value: Any):
101
+ attr = attribute(
102
+ name=name,
103
+ dtype=dtype,
104
+ dformat=AttrDataFormat.SCALAR,
105
+ access=AttrWriteType.READ_WRITE,
106
+ fget=self.read,
107
+ fset=self.write,
108
+ enum_labels=[e.value for e in ExampleStrEnum],
109
+ )
110
+ self._add_attr(attr, initial_value)
111
+
112
+ def add_array_attrs(self, name: str, dtype: str, initial_value: np.ndarray):
113
+ spectrum_name = f"{name}_spectrum"
114
+ spectrum_attr = attribute(
115
+ name=spectrum_name,
116
+ dtype=dtype,
117
+ dformat=AttrDataFormat.SPECTRUM,
118
+ access=AttrWriteType.READ_WRITE,
119
+ fget=self.read,
120
+ fset=self.write,
121
+ max_dim_x=initial_value.shape[-1],
122
+ enum_labels=[e.value for e in ExampleStrEnum],
123
+ )
124
+ image_name = f"{name}_image"
125
+ image_attr = attribute(
126
+ name=image_name,
127
+ dtype=dtype,
128
+ dformat=AttrDataFormat.IMAGE,
129
+ access=AttrWriteType.READ_WRITE,
130
+ fget=self.read,
131
+ fset=self.write,
132
+ max_dim_x=initial_value.shape[-1],
133
+ max_dim_y=2,
134
+ enum_labels=[e.value for e in ExampleStrEnum],
135
+ )
136
+ self._add_attr(spectrum_attr, initial_value)
137
+ # have image just be 2 of the initial spectrum stacked
138
+ self._add_attr(image_attr, np.vstack((initial_value, initial_value)))
139
+
140
+ def add_scalar_command(self, name: str, dtype: str):
141
+ if _valid_command(AttrDataFormat.SCALAR, dtype):
142
+ self.add_command(
143
+ command(
144
+ f=getattr(self, f"{name}_cmd"),
145
+ dtype_in=dtype,
146
+ dtype_out=dtype,
147
+ dformat_in=AttrDataFormat.SCALAR,
148
+ dformat_out=AttrDataFormat.SCALAR,
149
+ ),
150
+ )
151
+
152
+ def add_spectrum_command(self, name: str, dtype: str):
153
+ if _valid_command(AttrDataFormat.SPECTRUM, dtype):
154
+ self.add_command(
155
+ command(
156
+ f=getattr(self, f"{name}_spectrum_cmd"),
157
+ dtype_in=dtype,
158
+ dtype_out=dtype,
159
+ dformat_in=AttrDataFormat.SPECTRUM,
160
+ dformat_out=AttrDataFormat.SPECTRUM,
161
+ ),
162
+ )
163
+
164
+ def initialize_dynamic_attributes(self):
165
+ for attr_data in _all_attribute_definitions:
166
+ self.add_scalar_attr(
167
+ attr_data.name, attr_data.tango_type, attr_data.initial_scalar
168
+ )
169
+ self.add_array_attrs(
170
+ attr_data.name, attr_data.tango_type, attr_data.initial_spectrum
171
+ )
172
+ self.add_scalar_command(attr_data.name, attr_data.tango_type)
173
+ self.add_spectrum_command(attr_data.name, attr_data.tango_type)
174
+
175
+ @command
176
+ def reset_values(self):
177
+ for attr_name in self.attr_values:
178
+ self.attr_values[attr_name] = self.initial_values[attr_name]
179
+
180
+ def read(self, attr):
181
+ value = self.attr_values[attr.get_name()]
182
+ attr.set_value(value)
183
+
184
+ def write(self, attr):
185
+ new_value = attr.get_write_value()
186
+ self.attr_values[attr.get_name()] = new_value
187
+ self.push_change_event(attr.get_name(), new_value)
188
+
189
+ echo_command_code = textwrap.dedent(
190
+ """\
191
+ def {}(self, arg):
192
+ return arg
193
+ """
194
+ )
195
+
196
+ for attr_data in _all_attribute_definitions:
197
+ if _valid_command(AttrDataFormat.SCALAR, attr_data.tango_type):
198
+ exec(echo_command_code.format(f"{attr_data.name}_cmd"))
199
+ if _valid_command(AttrDataFormat.SPECTRUM, attr_data.tango_type):
200
+ exec(echo_command_code.format(f"{attr_data.name}_spectrum_cmd"))
@@ -1,7 +1,10 @@
1
+ """Utilities for testing devices."""
2
+
1
3
  from . import __pytest_assert_rewrite # noqa: F401
2
4
  from ._assert import (
3
5
  ApproxTable,
4
6
  MonitorQueue,
7
+ StatusWatcher,
5
8
  approx_value,
6
9
  assert_configuration,
7
10
  assert_describe_signal,
@@ -14,7 +17,6 @@ from ._mock_signal_utils import (
14
17
  get_mock,
15
18
  get_mock_put,
16
19
  mock_puts_blocked,
17
- reset_mock_put_calls,
18
20
  set_mock_put_proceeds,
19
21
  set_mock_value,
20
22
  set_mock_values,
@@ -24,24 +26,36 @@ from ._one_of_everything import (
24
26
  ExampleTable,
25
27
  OneOfEverythingDevice,
26
28
  ParentOfEverythingDevice,
29
+ float_array_value,
30
+ int_array_value,
31
+ )
32
+ from ._single_derived import (
33
+ BeamstopPosition,
34
+ Exploder,
35
+ MovableBeamstop,
36
+ ReadOnlyBeamstop,
27
37
  )
28
38
  from ._wait_for_pending import wait_for_pending_wakeups
29
39
 
40
+ # The order of this list determines the order of the documentation,
41
+ # so does not match the alphabetical order of the imports
30
42
  __all__ = [
31
43
  "approx_value",
44
+ # Assert functions
45
+ "assert_value",
46
+ "assert_reading",
32
47
  "assert_configuration",
33
48
  "assert_describe_signal",
34
49
  "assert_emitted",
35
- "assert_reading",
36
- "assert_value",
37
- "callback_on_mock_put",
50
+ # Mocking utilities
38
51
  "get_mock",
52
+ "set_mock_value",
53
+ "set_mock_values",
39
54
  "get_mock_put",
55
+ "callback_on_mock_put",
40
56
  "mock_puts_blocked",
41
- "reset_mock_put_calls",
42
57
  "set_mock_put_proceeds",
43
- "set_mock_value",
44
- "set_mock_values",
58
+ # Wait for pending wakeups
45
59
  "wait_for_pending_wakeups",
46
60
  "ExampleEnum",
47
61
  "ExampleTable",
@@ -49,4 +63,12 @@ __all__ = [
49
63
  "ParentOfEverythingDevice",
50
64
  "MonitorQueue",
51
65
  "ApproxTable",
66
+ "StatusWatcher",
67
+ "int_array_value",
68
+ "float_array_value",
69
+ # Derived examples
70
+ "BeamstopPosition",
71
+ "Exploder",
72
+ "MovableBeamstop",
73
+ "ReadOnlyBeamstop",
52
74
  ]
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
- import time
3
2
  from contextlib import AbstractContextManager
4
3
  from typing import Any
4
+ from unittest.mock import Mock, call
5
5
 
6
6
  import pytest
7
7
  from bluesky.protocols import Reading
@@ -13,89 +13,84 @@ from ophyd_async.core import (
13
13
  SignalDatatypeT,
14
14
  SignalR,
15
15
  Table,
16
+ WatchableAsyncStatus,
17
+ Watcher,
16
18
  )
17
19
 
20
+ from ._utils import T
21
+
18
22
 
19
23
  def approx_value(value: Any):
24
+ """Allow any value to be compared to another in tests.
25
+
26
+ This is needed because numpy arrays give a numpy array back when compared,
27
+ not a bool. This means that you can't ``assert array1==array2``. Numpy
28
+ arrays can be wrapped with `pytest.approx`, but this doesn't work for
29
+ `Table` instances: in this case we use `ApproxTable`.
30
+ """
20
31
  return ApproxTable(value) if isinstance(value, Table) else pytest.approx(value)
21
32
 
22
33
 
23
34
  async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
24
- """Assert a signal's value and compare it an expected signal.
25
-
26
- Parameters
27
- ----------
28
- signal:
29
- signal with get_value.
30
- value:
31
- The expected value from the signal.
32
-
33
- Notes
34
- -----
35
- Example usage::
36
- await assert_value(signal, value)
35
+ """Assert that a Signal has the given value.
37
36
 
37
+ :param signal: Signal with get_value.
38
+ :param value: The expected value from the signal.
38
39
  """
39
40
  actual_value = await signal.get_value()
40
41
  assert approx_value(value) == actual_value
41
42
 
42
43
 
43
44
  async def assert_reading(
44
- readable: AsyncReadable, expected_reading: dict[str, Reading]
45
+ readable: AsyncReadable,
46
+ expected_reading: dict[str, dict[str, Any]],
45
47
  ) -> None:
46
- """Assert readings from readable.
47
-
48
- Parameters
49
- ----------
50
- readable:
51
- Callable with readable.read function that generate readings.
52
-
53
- reading:
54
- The expected readings from the readable.
55
-
56
- Notes
57
- -----
58
- Example usage::
59
- await assert_reading(readable, reading)
48
+ """Assert that a readable Device has the given reading.
60
49
 
50
+ :param readable: Device with an async ``read()`` method to get the reading from.
51
+ :param expected_reading: The expected reading from the readable.
61
52
  """
62
53
  actual_reading = await readable.read()
54
+ _assert_readings_approx_equal(expected_reading, actual_reading)
55
+
56
+
57
+ def _assert_readings_approx_equal(expected, actual):
58
+ assert expected.keys() == actual.keys()
63
59
  approx_expected_reading = {
64
- k: dict(v, value=approx_value(expected_reading[k]["value"]))
65
- for k, v in expected_reading.items()
60
+ k: dict(
61
+ v,
62
+ value=approx_value(v["value"]),
63
+ timestamp=pytest.approx(v["timestamp"], rel=0.1)
64
+ if "timestamp" in v
65
+ else actual[k]["timestamp"],
66
+ alarm_severity=v.get("alarm_severity", actual[k]["alarm_severity"]),
67
+ )
68
+ for k, v in expected.items()
66
69
  }
67
- assert actual_reading == approx_expected_reading
70
+ assert actual == approx_expected_reading
68
71
 
69
72
 
70
73
  async def assert_configuration(
71
74
  configurable: AsyncConfigurable,
72
- configuration: dict[str, Reading],
75
+ configuration: dict[str, dict[str, Any]],
73
76
  ) -> None:
74
- """Assert readings from Configurable.
75
-
76
- Parameters
77
- ----------
78
- configurable:
79
- Configurable with Configurable.read function that generate readings.
80
-
81
- configuration:
82
- The expected readings from configurable.
83
-
84
- Notes
85
- -----
86
- Example usage::
87
- await assert_configuration(configurable configuration)
77
+ """Assert that a configurable Device has the given configuration.
88
78
 
79
+ :param configurable:
80
+ Device with an async ``read_configuration()`` method to get the
81
+ configuration from.
82
+ :param configuration: The expected configuration from the configurable.
89
83
  """
90
84
  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
85
+ _assert_readings_approx_equal(configuration, actual_configuration)
96
86
 
97
87
 
98
88
  async def assert_describe_signal(signal: SignalR, /, **metadata):
89
+ """Assert the describe of a signal matches the expected metadata.
90
+
91
+ :param signal: The signal to describe.
92
+ :param metadata: The expected metadata.
93
+ """
99
94
  actual_describe = await signal.describe()
100
95
  assert list(actual_describe) == [signal.name]
101
96
  (actual_datakey,) = actual_describe.values()
@@ -104,23 +99,18 @@ async def assert_describe_signal(signal: SignalR, /, **metadata):
104
99
 
105
100
 
106
101
  def assert_emitted(docs: dict[str, list[dict]], **numbers: int):
107
- """Assert emitted document generated by running a Bluesky plan
108
-
109
- Parameters
110
- ----------
111
- Doc:
112
- A dictionary
113
-
114
- numbers:
115
- expected emission in kwarg from
116
-
117
- Notes
118
- -----
119
- Example usage::
120
- docs = defaultdict(list)
121
- RE.subscribe(lambda name, doc: docs[name].append(doc))
122
- RE(my_plan())
123
- assert_emitted(docs, start=1, descriptor=1, event=1, stop=1)
102
+ """Assert emitted document generated by running a Bluesky plan.
103
+
104
+ :param docs: A mapping of document type -> list of documents that have been emitted.
105
+ :param numbers: The number of each document type expected.
106
+
107
+ :example:
108
+ ```python
109
+ docs = defaultdict(list)
110
+ RE.subscribe(lambda name, doc: docs[name].append(doc))
111
+ RE(my_plan())
112
+ assert_emitted(docs, start=1, descriptor=1, event=1, stop=1)
113
+ ```
124
114
  """
125
115
  assert list(docs) == list(numbers)
126
116
  actual_numbers = {name: len(d) for name, d in docs.items()}
@@ -128,6 +118,14 @@ def assert_emitted(docs: dict[str, list[dict]], **numbers: int):
128
118
 
129
119
 
130
120
  class ApproxTable:
121
+ """For approximating two tables are equivalent.
122
+
123
+ :param expected: The expected table.
124
+ :param rel: The relative tolerance.
125
+ :param abs: The absolute tolerance.
126
+ :param nan_ok: Whether NaNs are allowed.
127
+ """
128
+
131
129
  def __init__(self, expected: Table, rel=None, abs=None, nan_ok: bool = False):
132
130
  self.expected = expected
133
131
  self.rel = rel
@@ -144,29 +142,23 @@ class ApproxTable:
144
142
 
145
143
 
146
144
  class MonitorQueue(AbstractContextManager):
145
+ """Monitors a `Signal` and stores its updates."""
146
+
147
147
  def __init__(self, signal: SignalR):
148
148
  self.signal = signal
149
149
  self.updates: asyncio.Queue[dict[str, Reading]] = asyncio.Queue()
150
- self.signal.subscribe(self.updates.put_nowait)
151
150
 
152
151
  async def assert_updates(self, expected_value):
153
152
  # 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
153
+ update = await asyncio.wait_for(self.updates.get(), timeout=5)
154
+ await assert_value(self.signal, expected_value)
162
155
  expected_reading = {
163
156
  self.signal.name: {
164
157
  "value": expected_value,
165
- "timestamp": pytest.approx(time.time(), rel=0.1),
166
- "alarm_severity": 0,
167
158
  }
168
159
  }
169
- assert reading == update == expected_reading
160
+ await assert_reading(self.signal, expected_reading)
161
+ _assert_readings_approx_equal(expected_reading, update)
170
162
 
171
163
  def __enter__(self):
172
164
  self.signal.subscribe(self.updates.put_nowait)
@@ -174,3 +166,67 @@ class MonitorQueue(AbstractContextManager):
174
166
 
175
167
  def __exit__(self, exc_type, exc_value, traceback):
176
168
  self.signal.clear_sub(self.updates.put_nowait)
169
+
170
+
171
+ class StatusWatcher(Watcher[T]):
172
+ """Watches an `AsyncStatus`, storing the calls within."""
173
+
174
+ def __init__(self, status: WatchableAsyncStatus) -> None:
175
+ self._event = asyncio.Event()
176
+ self.mock = Mock()
177
+ """Mock that stores watcher updates from the status."""
178
+ status.watch(self)
179
+
180
+ def __call__(
181
+ self,
182
+ current: T | None = None,
183
+ initial: T | None = None,
184
+ target: T | None = None,
185
+ name: str | None = None,
186
+ unit: str | None = None,
187
+ precision: int | None = None,
188
+ fraction: float | None = None,
189
+ time_elapsed: float | None = None,
190
+ time_remaining: float | None = None,
191
+ ) -> Any:
192
+ self.mock(
193
+ current=current,
194
+ initial=initial,
195
+ target=target,
196
+ name=name,
197
+ unit=unit,
198
+ precision=precision,
199
+ fraction=fraction,
200
+ time_elapsed=time_elapsed,
201
+ time_remaining=time_remaining,
202
+ )
203
+ self._event.set()
204
+
205
+ async def wait_for_call(
206
+ self,
207
+ current: T | None = None,
208
+ initial: T | None = None,
209
+ target: T | None = None,
210
+ name: str | None = None,
211
+ unit: str | None = None,
212
+ precision: int | None = None,
213
+ fraction: float | None = None,
214
+ # Any so we can use pytest.approx
215
+ time_elapsed: float | Any = None,
216
+ time_remaining: float | Any = None,
217
+ ):
218
+ await asyncio.wait_for(self._event.wait(), timeout=1)
219
+ assert self.mock.call_count == 1
220
+ assert self.mock.call_args == call(
221
+ current=current,
222
+ initial=initial,
223
+ target=target,
224
+ name=name,
225
+ unit=unit,
226
+ precision=precision,
227
+ fraction=fraction,
228
+ time_elapsed=time_elapsed,
229
+ time_remaining=time_remaining,
230
+ )
231
+ self.mock.reset_mock()
232
+ self._event.clear()