ophyd-async 0.9.0a1__py3-none-any.whl → 0.10.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +102 -74
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +158 -153
  8. ophyd_async/core/_device.py +143 -115
  9. ophyd_async/core/_device_filler.py +82 -9
  10. ophyd_async/core/_flyer.py +16 -7
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +74 -58
  17. ophyd_async/core/_settings.py +113 -0
  18. ophyd_async/core/_signal.py +304 -174
  19. ophyd_async/core/_signal_backend.py +60 -14
  20. ophyd_async/core/_soft_signal_backend.py +18 -12
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +54 -17
  23. ophyd_async/core/_utils.py +101 -52
  24. ophyd_async/core/_yaml_settings.py +66 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/__init__.py +9 -0
  27. ophyd_async/epics/adandor/_andor.py +45 -0
  28. ophyd_async/epics/adandor/_andor_controller.py +51 -0
  29. ophyd_async/epics/adandor/_andor_io.py +34 -0
  30. ophyd_async/epics/adaravis/__init__.py +8 -1
  31. ophyd_async/epics/adaravis/_aravis.py +23 -41
  32. ophyd_async/epics/adaravis/_aravis_controller.py +23 -55
  33. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  34. ophyd_async/epics/adcore/__init__.py +36 -14
  35. ophyd_async/epics/adcore/_core_detector.py +81 -0
  36. ophyd_async/epics/adcore/_core_io.py +145 -95
  37. ophyd_async/epics/adcore/_core_logic.py +179 -88
  38. ophyd_async/epics/adcore/_core_writer.py +223 -0
  39. ophyd_async/epics/adcore/_hdf_writer.py +51 -92
  40. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  41. ophyd_async/epics/adcore/_single_trigger.py +6 -5
  42. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  43. ophyd_async/epics/adcore/_utils.py +3 -2
  44. ophyd_async/epics/adkinetix/__init__.py +2 -1
  45. ophyd_async/epics/adkinetix/_kinetix.py +32 -27
  46. ophyd_async/epics/adkinetix/_kinetix_controller.py +11 -21
  47. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  48. ophyd_async/epics/adpilatus/__init__.py +7 -2
  49. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  50. ophyd_async/epics/adpilatus/_pilatus_controller.py +25 -22
  51. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  52. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  53. ophyd_async/epics/adsimdetector/_sim.py +22 -16
  54. ophyd_async/epics/adsimdetector/_sim_controller.py +9 -43
  55. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  56. ophyd_async/epics/advimba/__init__.py +10 -1
  57. ophyd_async/epics/advimba/_vimba.py +26 -25
  58. ophyd_async/epics/advimba/_vimba_controller.py +12 -24
  59. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  60. ophyd_async/epics/core/_aioca.py +66 -30
  61. ophyd_async/epics/core/_epics_connector.py +4 -0
  62. ophyd_async/epics/core/_epics_device.py +2 -0
  63. ophyd_async/epics/core/_p4p.py +50 -18
  64. ophyd_async/epics/core/_pvi_connector.py +65 -8
  65. ophyd_async/epics/core/_signal.py +51 -51
  66. ophyd_async/epics/core/_util.py +5 -5
  67. ophyd_async/epics/demo/__init__.py +11 -49
  68. ophyd_async/epics/demo/__main__.py +31 -0
  69. ophyd_async/epics/demo/_ioc.py +32 -0
  70. ophyd_async/epics/demo/_motor.py +82 -0
  71. ophyd_async/epics/demo/_point_detector.py +42 -0
  72. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  73. ophyd_async/epics/demo/_stage.py +15 -0
  74. ophyd_async/epics/demo/{mover.db → motor.db} +2 -1
  75. ophyd_async/epics/demo/point_detector.db +59 -0
  76. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  77. ophyd_async/epics/eiger/_eiger.py +1 -3
  78. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  79. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  80. ophyd_async/epics/eiger/_odin_io.py +1 -2
  81. ophyd_async/epics/motor.py +83 -38
  82. ophyd_async/epics/signal.py +4 -1
  83. ophyd_async/epics/testing/__init__.py +14 -14
  84. ophyd_async/epics/testing/_example_ioc.py +68 -73
  85. ophyd_async/epics/testing/_utils.py +19 -44
  86. ophyd_async/epics/testing/test_records.db +16 -0
  87. ophyd_async/epics/testing/test_records_pva.db +17 -16
  88. ophyd_async/fastcs/__init__.py +1 -0
  89. ophyd_async/fastcs/core.py +6 -0
  90. ophyd_async/fastcs/odin/__init__.py +1 -0
  91. ophyd_async/fastcs/panda/__init__.py +8 -8
  92. ophyd_async/fastcs/panda/_block.py +29 -9
  93. ophyd_async/fastcs/panda/_control.py +12 -2
  94. ophyd_async/fastcs/panda/_hdf_panda.py +5 -1
  95. ophyd_async/fastcs/panda/_table.py +13 -7
  96. ophyd_async/fastcs/panda/_trigger.py +23 -9
  97. ophyd_async/fastcs/panda/_writer.py +27 -30
  98. ophyd_async/plan_stubs/__init__.py +16 -0
  99. ophyd_async/plan_stubs/_ensure_connected.py +12 -17
  100. ophyd_async/plan_stubs/_fly.py +3 -5
  101. ophyd_async/plan_stubs/_nd_attributes.py +9 -5
  102. ophyd_async/plan_stubs/_panda.py +14 -0
  103. ophyd_async/plan_stubs/_settings.py +152 -0
  104. ophyd_async/plan_stubs/_utils.py +3 -0
  105. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  106. ophyd_async/sim/__init__.py +29 -0
  107. ophyd_async/sim/__main__.py +43 -0
  108. ophyd_async/sim/_blob_detector.py +33 -0
  109. ophyd_async/sim/_blob_detector_controller.py +48 -0
  110. ophyd_async/sim/_blob_detector_writer.py +105 -0
  111. ophyd_async/sim/_mirror_horizontal.py +46 -0
  112. ophyd_async/sim/_mirror_vertical.py +74 -0
  113. ophyd_async/sim/_motor.py +233 -0
  114. ophyd_async/sim/_pattern_generator.py +124 -0
  115. ophyd_async/sim/_point_detector.py +86 -0
  116. ophyd_async/sim/_stage.py +19 -0
  117. ophyd_async/tango/__init__.py +1 -0
  118. ophyd_async/tango/core/__init__.py +6 -1
  119. ophyd_async/tango/core/_base_device.py +41 -33
  120. ophyd_async/tango/core/_converters.py +81 -0
  121. ophyd_async/tango/core/_signal.py +21 -33
  122. ophyd_async/tango/core/_tango_readable.py +2 -19
  123. ophyd_async/tango/core/_tango_transport.py +148 -74
  124. ophyd_async/tango/core/_utils.py +47 -0
  125. ophyd_async/tango/demo/_counter.py +2 -0
  126. ophyd_async/tango/demo/_detector.py +2 -0
  127. ophyd_async/tango/demo/_mover.py +10 -6
  128. ophyd_async/tango/demo/_tango/_servers.py +4 -0
  129. ophyd_async/tango/testing/__init__.py +6 -0
  130. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  131. ophyd_async/testing/__init__.py +48 -7
  132. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  133. ophyd_async/testing/_assert.py +200 -96
  134. ophyd_async/testing/_mock_signal_utils.py +59 -73
  135. ophyd_async/testing/_one_of_everything.py +146 -0
  136. ophyd_async/testing/_single_derived.py +87 -0
  137. ophyd_async/testing/_utils.py +3 -0
  138. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/METADATA +25 -26
  139. ophyd_async-0.10.0a1.dist-info/RECORD +149 -0
  140. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/WHEEL +1 -1
  141. ophyd_async/core/_device_save_loader.py +0 -274
  142. ophyd_async/epics/demo/_mover.py +0 -95
  143. ophyd_async/epics/demo/_sensor.py +0 -37
  144. ophyd_async/epics/demo/sensor.db +0 -19
  145. ophyd_async/fastcs/panda/_utils.py +0 -16
  146. ophyd_async/sim/demo/__init__.py +0 -19
  147. ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -13
  148. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -42
  149. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -62
  150. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -41
  151. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -207
  152. ophyd_async/sim/demo/_sim_motor.py +0 -107
  153. ophyd_async/sim/testing/__init__.py +0 -0
  154. ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
  155. ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
  156. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info/licenses}/LICENSE +0 -0
  157. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.10.0a1.dist-info}/top_level.txt +0 -0
@@ -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,5 +1,13 @@
1
+ """Utilities for testing devices."""
2
+
3
+ from . import __pytest_assert_rewrite # noqa: F401
1
4
  from ._assert import (
5
+ ApproxTable,
6
+ MonitorQueue,
7
+ StatusWatcher,
8
+ approx_value,
2
9
  assert_configuration,
10
+ assert_describe_signal,
3
11
  assert_emitted,
4
12
  assert_reading,
5
13
  assert_value,
@@ -9,25 +17,58 @@ from ._mock_signal_utils import (
9
17
  get_mock,
10
18
  get_mock_put,
11
19
  mock_puts_blocked,
12
- reset_mock_put_calls,
13
20
  set_mock_put_proceeds,
14
21
  set_mock_value,
15
22
  set_mock_values,
16
23
  )
24
+ from ._one_of_everything import (
25
+ ExampleEnum,
26
+ ExampleTable,
27
+ OneOfEverythingDevice,
28
+ ParentOfEverythingDevice,
29
+ float_array_value,
30
+ int_array_value,
31
+ )
32
+ from ._single_derived import (
33
+ BeamstopPosition,
34
+ Exploder,
35
+ MovableBeamstop,
36
+ ReadOnlyBeamstop,
37
+ )
17
38
  from ._wait_for_pending import wait_for_pending_wakeups
18
39
 
40
+ # The order of this list determines the order of the documentation,
41
+ # so does not match the alphabetical order of the imports
19
42
  __all__ = [
43
+ "approx_value",
44
+ # Assert functions
45
+ "assert_value",
46
+ "assert_reading",
20
47
  "assert_configuration",
48
+ "assert_describe_signal",
21
49
  "assert_emitted",
22
- "assert_reading",
23
- "assert_value",
24
- "callback_on_mock_put",
50
+ # Mocking utilities
25
51
  "get_mock",
52
+ "set_mock_value",
53
+ "set_mock_values",
26
54
  "get_mock_put",
55
+ "callback_on_mock_put",
27
56
  "mock_puts_blocked",
28
- "reset_mock_put_calls",
29
57
  "set_mock_put_proceeds",
30
- "set_mock_value",
31
- "set_mock_values",
58
+ # Wait for pending wakeups
32
59
  "wait_for_pending_wakeups",
60
+ "ExampleEnum",
61
+ "ExampleTable",
62
+ "OneOfEverythingDevice",
63
+ "ParentOfEverythingDevice",
64
+ "MonitorQueue",
65
+ "ApproxTable",
66
+ "StatusWatcher",
67
+ "int_array_value",
68
+ "float_array_value",
69
+ # Derived examples
70
+ "BeamstopPosition",
71
+ "Exploder",
72
+ "MovableBeamstop",
73
+ "ReadOnlyBeamstop",
33
74
  ]
@@ -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,128 +1,232 @@
1
- from collections.abc import Mapping
1
+ import asyncio
2
+ from contextlib import AbstractContextManager
2
3
  from typing import Any
4
+ from unittest.mock import Mock, call
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
+ WatchableAsyncStatus,
17
+ Watcher,
18
+ )
7
19
 
20
+ from ._utils import T
8
21
 
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
- )
18
22
 
23
+ def approx_value(value: Any):
24
+ """Allow any value to be compared to another in tests.
19
25
 
20
- async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
21
- """Assert a signal's value and compare it an expected signal.
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
+ """
31
+ return ApproxTable(value) if isinstance(value, Table) else pytest.approx(value)
22
32
 
23
- Parameters
24
- ----------
25
- signal:
26
- signal with get_value.
27
- value:
28
- The expected value from the signal.
29
33
 
30
- Notes
31
- -----
32
- Example usage::
33
- await assert_value(signal, value)
34
+ async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
35
+ """Assert that a Signal has the given value.
34
36
 
37
+ :param signal: Signal with get_value.
38
+ :param value: The expected value from the signal.
35
39
  """
36
40
  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
- )
41
+ assert approx_value(value) == actual_value
42
42
 
43
43
 
44
44
  async def assert_reading(
45
- readable: AsyncReadable, expected_reading: Mapping[str, Reading]
45
+ readable: AsyncReadable,
46
+ expected_reading: dict[str, dict[str, Any]],
46
47
  ) -> None:
47
- """Assert readings from readable.
48
-
49
- Parameters
50
- ----------
51
- readable:
52
- Callable with readable.read function that generate readings.
53
-
54
- reading:
55
- The expected readings from the readable.
56
-
57
- Notes
58
- -----
59
- Example usage::
60
- await assert_reading(readable, reading)
48
+ """Assert that a readable Device has the given reading.
61
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.
62
52
  """
63
53
  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
- )
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()
59
+ approx_expected_reading = {
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()
69
+ }
70
+ assert actual == approx_expected_reading
69
71
 
70
72
 
71
73
  async def assert_configuration(
72
74
  configurable: AsyncConfigurable,
73
- configuration: Mapping[str, Reading],
75
+ configuration: dict[str, dict[str, Any]],
74
76
  ) -> None:
75
- """Assert readings from Configurable.
77
+ """Assert that a configurable Device has the given configuration.
76
78
 
77
- Parameters
78
- ----------
79
- configurable:
80
- Configurable with Configurable.read function that generate readings.
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.
83
+ """
84
+ actual_configuration = await configurable.read_configuration()
85
+ _assert_readings_approx_equal(configuration, actual_configuration)
81
86
 
82
- configuration:
83
- The expected readings from configurable.
84
87
 
85
- Notes
86
- -----
87
- Example usage::
88
- await assert_configuration(configurable configuration)
88
+ async def assert_describe_signal(signal: SignalR, /, **metadata):
89
+ """Assert the describe of a signal matches the expected metadata.
89
90
 
91
+ :param signal: The signal to describe.
92
+ :param metadata: The expected metadata.
90
93
  """
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
- )
97
-
98
-
99
- def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
100
- """Assert emitted document generated by running a Bluesky plan
101
-
102
- Parameters
103
- ----------
104
- Doc:
105
- A dictionary
106
-
107
- numbers:
108
- expected emission in kwarg from
109
-
110
- Notes
111
- -----
112
- Example usage::
113
- docs = defaultdict(list)
114
- RE.subscribe(lambda name, doc: docs[name].append(doc))
115
- RE(my_plan())
116
- assert_emitted(docs, start=1, descriptor=1, event=1, stop=1)
94
+ actual_describe = await signal.describe()
95
+ assert list(actual_describe) == [signal.name]
96
+ (actual_datakey,) = actual_describe.values()
97
+ expected_datakey = DataKey(source=signal.source, **metadata)
98
+ assert actual_datakey == expected_datakey
99
+
100
+
101
+ def assert_emitted(docs: dict[str, list[dict]], **numbers: int):
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
+ ```
117
114
  """
118
- assert list(docs) == list(numbers), _generate_assert_error_msg(
119
- name="documents",
120
- expected_result=list(numbers),
121
- actual_result=list(docs),
122
- )
115
+ assert list(docs) == list(numbers)
123
116
  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
- )
117
+ assert actual_numbers == numbers
118
+
119
+
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
+
129
+ def __init__(self, expected: Table, rel=None, abs=None, nan_ok: bool = False):
130
+ self.expected = expected
131
+ self.rel = rel
132
+ self.abs = abs
133
+ self.nan_ok = nan_ok
134
+
135
+ def __eq__(self, value):
136
+ approx_fields = {
137
+ k: pytest.approx(v, self.rel, self.abs, self.nan_ok)
138
+ for k, v in self.expected
139
+ }
140
+ expected = type(self.expected).model_construct(**approx_fields) # type: ignore
141
+ return expected == value
142
+
143
+
144
+ class MonitorQueue(AbstractContextManager):
145
+ """Monitors a `Signal` and stores its updates."""
146
+
147
+ def __init__(self, signal: SignalR):
148
+ self.signal = signal
149
+ self.updates: asyncio.Queue[dict[str, Reading]] = asyncio.Queue()
150
+
151
+ async def assert_updates(self, expected_value):
152
+ # Get an update, value and reading
153
+ update = await asyncio.wait_for(self.updates.get(), timeout=5)
154
+ await assert_value(self.signal, expected_value)
155
+ expected_reading = {
156
+ self.signal.name: {
157
+ "value": expected_value,
158
+ }
159
+ }
160
+ await assert_reading(self.signal, expected_reading)
161
+ _assert_readings_approx_equal(expected_reading, update)
162
+
163
+ def __enter__(self):
164
+ self.signal.subscribe(self.updates.put_nowait)
165
+ return self
166
+
167
+ def __exit__(self, exc_type, exc_value, traceback):
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()