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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. ophyd_async/__init__.py +5 -8
  2. ophyd_async/_docs_parser.py +12 -0
  3. ophyd_async/_version.py +9 -4
  4. ophyd_async/core/__init__.py +97 -62
  5. ophyd_async/core/_derived_signal.py +271 -0
  6. ophyd_async/core/_derived_signal_backend.py +300 -0
  7. ophyd_async/core/_detector.py +106 -125
  8. ophyd_async/core/_device.py +69 -63
  9. ophyd_async/core/_device_filler.py +65 -1
  10. ophyd_async/core/_flyer.py +14 -5
  11. ophyd_async/core/_hdf_dataset.py +29 -22
  12. ophyd_async/core/_log.py +14 -23
  13. ophyd_async/core/_mock_signal_backend.py +11 -3
  14. ophyd_async/core/_protocol.py +65 -45
  15. ophyd_async/core/_providers.py +28 -9
  16. ophyd_async/core/_readable.py +44 -35
  17. ophyd_async/core/_settings.py +36 -27
  18. ophyd_async/core/_signal.py +262 -170
  19. ophyd_async/core/_signal_backend.py +56 -13
  20. ophyd_async/core/_soft_signal_backend.py +16 -11
  21. ophyd_async/core/_status.py +72 -24
  22. ophyd_async/core/_table.py +41 -11
  23. ophyd_async/core/_utils.py +96 -49
  24. ophyd_async/core/_yaml_settings.py +2 -0
  25. ophyd_async/epics/__init__.py +1 -0
  26. ophyd_async/epics/adandor/_andor.py +2 -2
  27. ophyd_async/epics/adandor/_andor_controller.py +4 -2
  28. ophyd_async/epics/adandor/_andor_io.py +2 -4
  29. ophyd_async/epics/adaravis/__init__.py +5 -0
  30. ophyd_async/epics/adaravis/_aravis.py +4 -8
  31. ophyd_async/epics/adaravis/_aravis_controller.py +20 -43
  32. ophyd_async/epics/adaravis/_aravis_io.py +13 -28
  33. ophyd_async/epics/adcore/__init__.py +23 -8
  34. ophyd_async/epics/adcore/_core_detector.py +42 -2
  35. ophyd_async/epics/adcore/_core_io.py +124 -99
  36. ophyd_async/epics/adcore/_core_logic.py +106 -27
  37. ophyd_async/epics/adcore/_core_writer.py +12 -8
  38. ophyd_async/epics/adcore/_hdf_writer.py +21 -38
  39. ophyd_async/epics/adcore/_single_trigger.py +2 -2
  40. ophyd_async/epics/adcore/_utils.py +2 -2
  41. ophyd_async/epics/adkinetix/__init__.py +2 -1
  42. ophyd_async/epics/adkinetix/_kinetix.py +3 -3
  43. ophyd_async/epics/adkinetix/_kinetix_controller.py +4 -2
  44. ophyd_async/epics/adkinetix/_kinetix_io.py +12 -13
  45. ophyd_async/epics/adpilatus/__init__.py +5 -0
  46. ophyd_async/epics/adpilatus/_pilatus.py +1 -1
  47. ophyd_async/epics/adpilatus/_pilatus_controller.py +5 -24
  48. ophyd_async/epics/adpilatus/_pilatus_io.py +11 -9
  49. ophyd_async/epics/adsimdetector/__init__.py +8 -1
  50. ophyd_async/epics/adsimdetector/_sim.py +4 -14
  51. ophyd_async/epics/adsimdetector/_sim_controller.py +17 -0
  52. ophyd_async/epics/adsimdetector/_sim_io.py +10 -0
  53. ophyd_async/epics/advimba/__init__.py +10 -1
  54. ophyd_async/epics/advimba/_vimba.py +3 -2
  55. ophyd_async/epics/advimba/_vimba_controller.py +4 -2
  56. ophyd_async/epics/advimba/_vimba_io.py +23 -28
  57. ophyd_async/epics/core/_aioca.py +35 -16
  58. ophyd_async/epics/core/_epics_connector.py +4 -0
  59. ophyd_async/epics/core/_epics_device.py +2 -0
  60. ophyd_async/epics/core/_p4p.py +10 -2
  61. ophyd_async/epics/core/_pvi_connector.py +65 -8
  62. ophyd_async/epics/core/_signal.py +51 -51
  63. ophyd_async/epics/core/_util.py +4 -4
  64. ophyd_async/epics/demo/__init__.py +16 -0
  65. ophyd_async/epics/demo/__main__.py +31 -0
  66. ophyd_async/epics/demo/_ioc.py +32 -0
  67. ophyd_async/epics/demo/_motor.py +82 -0
  68. ophyd_async/epics/demo/_point_detector.py +42 -0
  69. ophyd_async/epics/demo/_point_detector_channel.py +22 -0
  70. ophyd_async/epics/demo/_stage.py +15 -0
  71. ophyd_async/epics/{sim/mover.db → demo/motor.db} +2 -1
  72. ophyd_async/epics/demo/point_detector.db +59 -0
  73. ophyd_async/epics/demo/point_detector_channel.db +21 -0
  74. ophyd_async/epics/eiger/_eiger.py +1 -3
  75. ophyd_async/epics/eiger/_eiger_controller.py +11 -4
  76. ophyd_async/epics/eiger/_eiger_io.py +2 -0
  77. ophyd_async/epics/eiger/_odin_io.py +1 -2
  78. ophyd_async/epics/motor.py +65 -28
  79. ophyd_async/epics/signal.py +4 -1
  80. ophyd_async/epics/testing/_example_ioc.py +21 -9
  81. ophyd_async/epics/testing/_utils.py +3 -0
  82. ophyd_async/epics/testing/test_records.db +8 -0
  83. ophyd_async/epics/testing/test_records_pva.db +17 -16
  84. ophyd_async/fastcs/__init__.py +1 -0
  85. ophyd_async/fastcs/core.py +6 -0
  86. ophyd_async/fastcs/odin/__init__.py +1 -0
  87. ophyd_async/fastcs/panda/__init__.py +8 -6
  88. ophyd_async/fastcs/panda/_block.py +29 -9
  89. ophyd_async/fastcs/panda/_control.py +5 -0
  90. ophyd_async/fastcs/panda/_hdf_panda.py +2 -0
  91. ophyd_async/fastcs/panda/_table.py +9 -6
  92. ophyd_async/fastcs/panda/_trigger.py +23 -9
  93. ophyd_async/fastcs/panda/_writer.py +27 -30
  94. ophyd_async/plan_stubs/__init__.py +2 -0
  95. ophyd_async/plan_stubs/_ensure_connected.py +1 -0
  96. ophyd_async/plan_stubs/_fly.py +2 -4
  97. ophyd_async/plan_stubs/_nd_attributes.py +2 -0
  98. ophyd_async/plan_stubs/_panda.py +1 -0
  99. ophyd_async/plan_stubs/_settings.py +43 -16
  100. ophyd_async/plan_stubs/_utils.py +3 -0
  101. ophyd_async/plan_stubs/_wait_for_awaitable.py +1 -1
  102. ophyd_async/sim/__init__.py +24 -14
  103. ophyd_async/sim/__main__.py +43 -0
  104. ophyd_async/sim/_blob_detector.py +33 -0
  105. ophyd_async/sim/_blob_detector_controller.py +48 -0
  106. ophyd_async/sim/_blob_detector_writer.py +105 -0
  107. ophyd_async/sim/_mirror_horizontal.py +46 -0
  108. ophyd_async/sim/_mirror_vertical.py +74 -0
  109. ophyd_async/sim/_motor.py +233 -0
  110. ophyd_async/sim/_pattern_generator.py +124 -0
  111. ophyd_async/sim/_point_detector.py +86 -0
  112. ophyd_async/sim/_stage.py +19 -0
  113. ophyd_async/tango/__init__.py +1 -0
  114. ophyd_async/tango/core/__init__.py +6 -1
  115. ophyd_async/tango/core/_base_device.py +41 -33
  116. ophyd_async/tango/core/_converters.py +81 -0
  117. ophyd_async/tango/core/_signal.py +18 -32
  118. ophyd_async/tango/core/_tango_readable.py +2 -19
  119. ophyd_async/tango/core/_tango_transport.py +136 -60
  120. ophyd_async/tango/core/_utils.py +47 -0
  121. ophyd_async/tango/{sim → demo}/_counter.py +2 -0
  122. ophyd_async/tango/{sim → demo}/_detector.py +2 -0
  123. ophyd_async/tango/{sim → demo}/_mover.py +5 -4
  124. ophyd_async/tango/{sim → demo}/_tango/_servers.py +4 -0
  125. ophyd_async/tango/testing/__init__.py +6 -0
  126. ophyd_async/tango/testing/_one_of_everything.py +200 -0
  127. ophyd_async/testing/__init__.py +29 -7
  128. ophyd_async/testing/_assert.py +145 -83
  129. ophyd_async/testing/_mock_signal_utils.py +56 -70
  130. ophyd_async/testing/_one_of_everything.py +41 -21
  131. ophyd_async/testing/_single_derived.py +89 -0
  132. ophyd_async/testing/_utils.py +3 -0
  133. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/METADATA +25 -26
  134. ophyd_async-0.10.0a2.dist-info/RECORD +149 -0
  135. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/WHEEL +1 -1
  136. ophyd_async/epics/sim/__init__.py +0 -54
  137. ophyd_async/epics/sim/_ioc.py +0 -29
  138. ophyd_async/epics/sim/_mover.py +0 -101
  139. ophyd_async/epics/sim/_sensor.py +0 -37
  140. ophyd_async/epics/sim/sensor.db +0 -19
  141. ophyd_async/sim/_pattern_detector/__init__.py +0 -13
  142. ophyd_async/sim/_pattern_detector/_pattern_detector.py +0 -42
  143. ophyd_async/sim/_pattern_detector/_pattern_detector_controller.py +0 -69
  144. ophyd_async/sim/_pattern_detector/_pattern_detector_writer.py +0 -41
  145. ophyd_async/sim/_pattern_detector/_pattern_generator.py +0 -214
  146. ophyd_async/sim/_sim_motor.py +0 -107
  147. ophyd_async-0.9.0a2.dist-info/RECORD +0 -129
  148. /ophyd_async/tango/{sim → demo}/__init__.py +0 -0
  149. /ophyd_async/tango/{sim → demo}/_tango/__init__.py +0 -0
  150. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info/licenses}/LICENSE +0 -0
  151. {ophyd_async-0.9.0a2.dist-info → ophyd_async-0.10.0a2.dist-info}/top_level.txt +0 -0
@@ -16,7 +16,7 @@ from typing import (
16
16
  )
17
17
 
18
18
  from ._device import Device, DeviceConnector, DeviceVector
19
- from ._signal import Signal, SignalX
19
+ from ._signal import Ignore, Signal, SignalX
20
20
  from ._signal_backend import SignalBackend, SignalDatatype
21
21
  from ._utils import get_origin_class
22
22
 
@@ -33,6 +33,7 @@ def _get_datatype(annotation: Any) -> type | None:
33
33
  args = get_args(annotation)
34
34
  if len(args) == 1 and get_origin_class(args[0]):
35
35
  return args[0]
36
+ return None
36
37
 
37
38
 
38
39
  def _logical(name: UniqueName) -> LogicalName:
@@ -53,6 +54,13 @@ class DeviceAnnotation(Protocol):
53
54
 
54
55
 
55
56
  class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
57
+ """For filling signals on introspected devices.
58
+
59
+ :param device: The device to fill.
60
+ :param signal_backend_factory: A callable that returns a SignalBackend.
61
+ :param device_connector_factory: A callable that returns a DeviceConnector.
62
+ """
63
+
56
64
  def __init__(
57
65
  self,
58
66
  device: Device,
@@ -68,6 +76,7 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
68
76
  self._extras: dict[UniqueName, Sequence[Any]] = {}
69
77
  self._signal_datatype: dict[LogicalName, type | None] = {}
70
78
  self._vector_device_type: dict[LogicalName, type[Device] | None] = {}
79
+ self.ignored_signals: set[str] = set()
71
80
  # Backends and Connectors stored ready for the connection phase
72
81
  self._unfilled_backends: dict[
73
82
  LogicalName, tuple[SignalBackendT, type[Signal]]
@@ -108,6 +117,8 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
108
117
  # Get hints with Annotated for wrapping signals and backends
109
118
  extra_hints = get_type_hints(cls, include_extras=True)
110
119
  for attr_name, annotation in hints.items():
120
+ if annotation is Ignore:
121
+ self.ignored_signals.add(attr_name)
111
122
  name = UniqueName(attr_name)
112
123
  origin = get_origin_class(annotation)
113
124
  if (
@@ -138,6 +149,7 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
138
149
  self._uncreated_devices[name] = origin
139
150
 
140
151
  def check_created(self):
152
+ """Check that all Signals and Devices declared in annotations are created."""
141
153
  uncreated = sorted(set(self._uncreated_signals).union(self._uncreated_devices))
142
154
  if uncreated:
143
155
  raise RuntimeError(
@@ -148,6 +160,21 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
148
160
  self,
149
161
  filled=True,
150
162
  ) -> Iterator[tuple[SignalBackendT, list[Any]]]:
163
+ """Create all Signals from annotations.
164
+
165
+ :param filled:
166
+ If True then the Signals created should be considered already filled
167
+ with connection data. If False then `fill_child_signal` needs
168
+ calling at device connection time before the signal can be
169
+ connected.
170
+ :yields: `(backend, extras)`
171
+ The `SignalBackend` that has been created for this Signal, and the
172
+ list of extra annotations that could be used to customize it. For
173
+ example an `EpicsDeviceConnector` consumes `PvSuffix` extras to set the
174
+ write_pv of the backend. Any unhandled extras should be left on the
175
+ list so this class can handle them, e.g. `StandardReadableFormat`
176
+ instances.
177
+ """
151
178
  for name in list(self._uncreated_signals):
152
179
  child_type = self._uncreated_signals.pop(name)
153
180
  backend = self._signal_backend_factory(
@@ -167,6 +194,17 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
167
194
  self,
168
195
  filled=True,
169
196
  ) -> Iterator[tuple[DeviceConnectorT, list[Any]]]:
197
+ """Create all Signals from annotations.
198
+
199
+ :param filled:
200
+ If True then the Devices created should be considered already filled
201
+ with connection data. If False then `fill_child_device` needs
202
+ calling at parent device connection time before the child Device can
203
+ be connected.
204
+ :yields: `(connector, extras)`
205
+ The `DeviceConnector` that has been created for this Signal, and the list of
206
+ extra annotations that could be used to customize it.
207
+ """
170
208
  for name in list(self._uncreated_devices):
171
209
  child_type = self._uncreated_devices.pop(name)
172
210
  connector = self._device_connector_factory()
@@ -181,6 +219,10 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
181
219
  dest[_logical(name)] = connector
182
220
 
183
221
  def create_device_vector_entries_to_mock(self, num: int):
222
+ """Create num entries for each `DeviceVector`.
223
+
224
+ This is used when the Device is being connected in mock mode.
225
+ """
184
226
  for name, cls in self._vector_device_type.items():
185
227
  if not cls:
186
228
  msg = "Malformed device vector"
@@ -194,6 +236,11 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
194
236
  self._raise(name, f"Can't make {cls}")
195
237
 
196
238
  def check_filled(self, source: str):
239
+ """Check that all the created Signals and Devices are filled.
240
+
241
+ :param source: The source of the data that should have done the filling, for
242
+ reporting as an error message
243
+ """
197
244
  unfilled = sorted(set(self._unfilled_connectors).union(self._unfilled_backends))
198
245
  if unfilled:
199
246
  raise RuntimeError(
@@ -216,6 +263,15 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
216
263
  signal_type: type[Signal],
217
264
  vector_index: int | None = None,
218
265
  ) -> SignalBackendT:
266
+ """Mark a Signal as filled, and return its backend for filling.
267
+
268
+ :param name:
269
+ The name without trailing underscore, the name in the control system
270
+ :param signal_type:
271
+ One of the types `SignalR`, `SignalW`, `SignalRW` or `SignalX`
272
+ :param vector_index: If the child is in a `DeviceVector` then what index is it
273
+ :return: The SignalBackend for the filled Signal.
274
+ """
219
275
  name = cast(LogicalName, name)
220
276
  if name in self._unfilled_backends:
221
277
  # We made it above
@@ -251,6 +307,14 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
251
307
  device_type: type[Device] = Device,
252
308
  vector_index: int | None = None,
253
309
  ) -> DeviceConnectorT:
310
+ """Mark a Device as filled, and return its connector for filling.
311
+
312
+ :param name:
313
+ The name without trailing underscore, the name in the control system
314
+ :param device_type: The `Device` subclass to be created
315
+ :param vector_index: If the child is in a `DeviceVector` then what index is it
316
+ :return: The DeviceConnector for the filled Device.
317
+ """
254
318
  name = cast(LogicalName, name)
255
319
  if name in self._unfilled_connectors:
256
320
  # We made it above
@@ -9,21 +9,26 @@ from ._utils import T
9
9
 
10
10
 
11
11
  class FlyerController(ABC, Generic[T]):
12
+ """Base class for controlling 'flyable' devices.
13
+
14
+ [`bluesky.protocols.Flyable`](#bluesky.protocols.Flyable).
15
+ """
16
+
12
17
  @abstractmethod
13
18
  async def prepare(self, value: T) -> Any:
14
- """Move to the start of the flyscan"""
19
+ """Move to the start of the flyscan."""
15
20
 
16
21
  @abstractmethod
17
22
  async def kickoff(self):
18
- """Start the flyscan"""
23
+ """Start the flyscan."""
19
24
 
20
25
  @abstractmethod
21
26
  async def complete(self):
22
- """Block until the flyscan is done"""
27
+ """Block until the flyscan is done."""
23
28
 
24
29
  @abstractmethod
25
30
  async def stop(self):
26
- """Stop flying and wait everything to be stopped"""
31
+ """Stop flying and wait everything to be stopped."""
27
32
 
28
33
 
29
34
  class StandardFlyer(
@@ -33,6 +38,11 @@ class StandardFlyer(
33
38
  Flyable,
34
39
  Generic[T],
35
40
  ):
41
+ """Base class for 'flyable' devices.
42
+
43
+ [`bluesky.protocols.Flyable`](#bluesky.protocols.Flyable).
44
+ """
45
+
36
46
  def __init__(
37
47
  self,
38
48
  trigger_logic: FlyerController[T],
@@ -54,7 +64,6 @@ class StandardFlyer(
54
64
  await self._trigger_logic.stop()
55
65
 
56
66
  def prepare(self, value: T) -> AsyncStatus:
57
- """Setup trajectories"""
58
67
  return AsyncStatus(self._prepare(value))
59
68
 
60
69
  async def _prepare(self, value: T) -> None:
@@ -1,5 +1,4 @@
1
- from collections.abc import Iterator, Sequence
2
- from dataclasses import dataclass, field
1
+ from collections.abc import Iterator
3
2
  from pathlib import Path
4
3
  from urllib.parse import urlunparse
5
4
 
@@ -10,44 +9,53 @@ from event_model import (
10
9
  StreamRange,
11
10
  StreamResource,
12
11
  )
12
+ from pydantic import BaseModel, Field
13
13
 
14
14
 
15
- @dataclass
16
- class HDFDataset:
15
+ class HDFDatasetDescription(BaseModel):
16
+ """A description of the type and shape of a dataset in an HDF file."""
17
+
17
18
  data_key: str
19
+ """The data_key that will appear in the event descriptor,
20
+ e.g. det or det.data"""
21
+
18
22
  dataset: str
19
- shape: Sequence[int] = field(default_factory=tuple)
23
+ """The dataset name within the HDF file,
24
+ e.g. /entry/data/data or /entry/instrument/NDAttributes/sum"""
25
+
26
+ shape: tuple[int, ...] = Field(default_factory=tuple)
27
+ """The shape of a single event's data in the HDF file,
28
+ e.g. (1, 768, 1024) for arrays or () for scalars"""
29
+
20
30
  dtype_numpy: str = ""
31
+ """The numpy dtype for this field,
32
+ e.g. <i2 or <f8"""
33
+
34
+ chunk_shape: tuple[int, ...]
35
+ """The explicit chunk size written to disk"""
36
+
21
37
  multiplier: int = 1
22
- swmr: bool = False
23
- # Represents explicit chunk size written to disk.
24
- chunk_shape: tuple[int, ...] = ()
38
+ """Won't be used soon."""
25
39
 
26
40
 
27
41
  SLICE_NAME = "AD_HDF5_SWMR_SLICE"
28
42
 
29
43
 
30
- class HDFFile:
31
- """
32
- :param full_file_name: Absolute path to the file to be written
33
- :param datasets: Datasets to write into the file
44
+ class HDFDocumentComposer:
45
+ """A helper class to make stream resource and datums for HDF datasets.
46
+
47
+ :param full_file_name: Absolute path to the file that has been written
48
+ :param datasets: Descriptions of each of the datasets that will appear in the file
34
49
  """
35
50
 
36
51
  def __init__(
37
52
  self,
38
53
  full_file_name: Path,
39
- datasets: list[HDFDataset],
54
+ datasets: list[HDFDatasetDescription],
40
55
  hostname: str = "localhost",
41
56
  ) -> None:
42
57
  self._last_emitted = 0
43
58
  self._hostname = hostname
44
-
45
- if len(datasets) == 0:
46
- self._bundles = []
47
- return None
48
-
49
- bundler_composer = ComposeStreamResource()
50
-
51
59
  uri = urlunparse(
52
60
  (
53
61
  "file",
@@ -58,7 +66,7 @@ class HDFFile:
58
66
  None,
59
67
  )
60
68
  )
61
-
69
+ bundler_composer = ComposeStreamResource()
62
70
  self._bundles: list[ComposeStreamResourceBundle] = [
63
71
  bundler_composer(
64
72
  mimetype="application/x-hdf5",
@@ -66,7 +74,6 @@ class HDFFile:
66
74
  data_key=ds.data_key,
67
75
  parameters={
68
76
  "dataset": ds.dataset,
69
- "swmr": ds.swmr,
70
77
  "multiplier": ds.multiplier,
71
78
  "chunk_shape": ds.chunk_shape,
72
79
  },
ophyd_async/core/_log.py CHANGED
@@ -63,31 +63,23 @@ def config_ophyd_async_logging(
63
63
  color=True,
64
64
  level="WARNING",
65
65
  ):
66
- """
67
- Set a new handler on the ``logging.getLogger('ophyd_async')`` logger.
66
+ """Set a new handler on the ``logging.getLogger('ophyd_async')`` logger.
67
+
68
68
  If this is called more than once, the handler from the previous invocation
69
69
  is removed (if still present) and replaced.
70
70
 
71
- Parameters
72
- ----------
73
- file : object with ``write`` method or filename string
74
- Default is ``sys.stdout``.
75
- fmt : Overall logging format
76
- datefmt : string
77
- Date format. Default is ``'%H:%M:%S'``.
78
- color : boolean
79
- Use ANSI color codes. True by default.
80
- level : str or int
81
- Python logging level, given as string or corresponding integer.
82
- Default is 'WARNING'.
83
-
84
- Returns
85
- -------
86
- handler : logging.Handler
87
- The handler, which has already been added to the 'ophyd_async' logger.
88
-
89
- Examples
90
- --------
71
+
72
+ :param file:
73
+ object with ``write`` method or filename string. Default is `sys.stdout`.
74
+ :param fmt: str Overall logging format
75
+ :param datefmt: str Date format. Default is `'%H:%M:%S'`.
76
+ :param color: bool Use ANSI color codes. True by default.
77
+ :param level: str or int Python logging level, given as string or
78
+ corresponding integer. Default is 'WARNING'.
79
+
80
+ :returns: The handler, which has already been added to the 'ophyd_async' logger.
81
+
82
+ :examples:
91
83
  Log to a file.
92
84
 
93
85
  config_ophyd_async_logging(file='/tmp/what_is_happening.txt')
@@ -104,7 +96,6 @@ def config_ophyd_async_logging(
104
96
  Increase verbosity: show level DEBUG or higher.
105
97
 
106
98
  config_ophyd_async_logging(level='DEBUG')
107
-
108
99
  """
109
100
  global current_handler
110
101
 
@@ -3,8 +3,10 @@ from collections.abc import Callable
3
3
  from functools import cached_property
4
4
  from unittest.mock import AsyncMock
5
5
 
6
- from bluesky.protocols import Descriptor, Reading
6
+ from bluesky.protocols import Reading
7
+ from event_model import DataKey
7
8
 
9
+ from ._derived_signal_backend import DerivedSignalBackend
8
10
  from ._signal_backend import SignalBackend, SignalDatatypeT
9
11
  from ._soft_signal_backend import SoftSignalBackend
10
12
  from ._utils import Callback, LazyMock
@@ -23,7 +25,7 @@ class MockSignalBackend(SignalBackend[SignalDatatypeT]):
23
25
 
24
26
  self.initial_backend = initial_backend
25
27
 
26
- if isinstance(self.initial_backend, SoftSignalBackend):
28
+ if isinstance(self.initial_backend, SoftSignalBackend | DerivedSignalBackend):
27
29
  # Backend is already a SoftSignalBackend, so use it
28
30
  self.soft_backend = self.initial_backend
29
31
  else:
@@ -38,11 +40,13 @@ class MockSignalBackend(SignalBackend[SignalDatatypeT]):
38
40
 
39
41
  @cached_property
40
42
  def put_mock(self) -> AsyncMock:
43
+ """Return the mock that will track calls to `put()`."""
41
44
  put_mock = AsyncMock(name="put", spec=Callable)
42
45
  self.mock().attach_mock(put_mock, "put")
43
46
  return put_mock
44
47
 
45
48
  def set_value(self, value: SignalDatatypeT):
49
+ """Set the value of the signal."""
46
50
  self.soft_backend.set_value(value)
47
51
 
48
52
  def source(self, name: str, read: bool) -> str:
@@ -53,6 +57,10 @@ class MockSignalBackend(SignalBackend[SignalDatatypeT]):
53
57
 
54
58
  @cached_property
55
59
  def put_proceeds(self) -> asyncio.Event:
60
+ """Return an Event that will block `put()` until set.
61
+
62
+ The Event is initially set, but can be unset to block `put()`.
63
+ """
56
64
  put_proceeds = asyncio.Event()
57
65
  put_proceeds.set()
58
66
  return put_proceeds
@@ -72,7 +80,7 @@ class MockSignalBackend(SignalBackend[SignalDatatypeT]):
72
80
  async def get_setpoint(self) -> SignalDatatypeT:
73
81
  return await self.soft_backend.get_setpoint()
74
82
 
75
- async def get_datakey(self, source: str) -> Descriptor:
83
+ async def get_datakey(self, source: str) -> DataKey:
76
84
  return await self.soft_backend.get_datakey(source)
77
85
 
78
86
  def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None:
@@ -10,67 +10,66 @@ from typing import (
10
10
  runtime_checkable,
11
11
  )
12
12
 
13
- from bluesky.protocols import HasName, Reading
13
+ from bluesky.protocols import HasName, Location, Reading, T_co
14
14
  from event_model import DataKey
15
15
 
16
+ from ._utils import T
17
+
16
18
  if TYPE_CHECKING:
17
19
  from ._status import AsyncStatus
18
20
 
19
21
 
20
22
  @runtime_checkable
21
23
  class AsyncReadable(HasName, Protocol):
24
+ """Async implementations of the sync [](#bluesky.protocols.Readable)."""
25
+
22
26
  @abstractmethod
23
27
  async def read(self) -> dict[str, Reading]:
24
- """Return an OrderedDict mapping string field name(s) to dictionaries
25
- of values and timestamps and optional per-point metadata.
26
-
27
- Example return value:
28
+ """Return value, timestamp, optional per-point metadata for each field name.
28
29
 
29
- .. code-block:: python
30
+ For example:
30
31
 
31
- OrderedDict(('channel1',
32
- {'value': 5, 'timestamp': 1472493713.271991}),
33
- ('channel2',
34
- {'value': 16, 'timestamp': 1472493713.539238}))
32
+ {
33
+ "channel1": {"value": 5, "timestamp": 1472493713.271991},
34
+ "channel2": {"value": 16, "timestamp": 1472493713.539238},
35
+ }
35
36
  """
36
37
 
37
38
  @abstractmethod
38
39
  async def describe(self) -> dict[str, DataKey]:
39
- """Return an OrderedDict with exactly the same keys as the ``read``
40
- method, here mapped to per-scan metadata about each field.
40
+ """Return per-scan metadata for each field name in `read()`.
41
41
 
42
- Example return value:
42
+ For example:
43
43
 
44
- .. code-block:: python
45
-
46
- OrderedDict(('channel1',
47
- {'source': 'XF23-ID:SOME_PV_NAME',
48
- 'dtype': 'number',
49
- 'shape': []}),
50
- ('channel2',
51
- {'source': 'XF23-ID:SOME_PV_NAME',
52
- 'dtype': 'number',
53
- 'shape': []}))
44
+ {
45
+ "channel1": {"source": "SOME_PV1", "dtype": "number", "shape": []},
46
+ "channel2": {"source": "SOME_PV2", "dtype": "number", "shape": []},
47
+ }
54
48
  """
55
49
 
56
50
 
57
51
  @runtime_checkable
58
52
  class AsyncConfigurable(HasName, Protocol):
53
+ """Async implementation of the sync [](#bluesky.protocols.Configurable)."""
54
+
59
55
  @abstractmethod
60
56
  async def read_configuration(self) -> dict[str, Reading]:
61
- """Same API as ``read`` but for slow-changing fields related to configuration.
62
- e.g., exposure time. These will typically be read only once per run.
57
+ """Return value, timestamp, optional per-point metadata for each field name.
58
+
59
+ Same API as [](#AsyncReadable.read) but for slow-changing fields related to
60
+ configuration. e.g., exposure time. These will typically be read only
61
+ once per run.
63
62
  """
64
63
 
65
64
  @abstractmethod
66
65
  async def describe_configuration(self) -> dict[str, DataKey]:
67
- """Same API as ``describe``, but corresponding to the keys in
68
- ``read_configuration``.
69
- """
66
+ """Return per-scan metadata for each field name in `read_configuration()`."""
70
67
 
71
68
 
72
69
  @runtime_checkable
73
70
  class AsyncPausable(Protocol):
71
+ """Async implementation of the sync [](#bluesky.protocols.Pausable)."""
72
+
74
73
  @abstractmethod
75
74
  async def pause(self) -> None:
76
75
  """Perform device-specific work when the RunEngine pauses."""
@@ -82,20 +81,40 @@ class AsyncPausable(Protocol):
82
81
 
83
82
  @runtime_checkable
84
83
  class AsyncStageable(Protocol):
84
+ """Async implementation of the sync [](#bluesky.protocols.Stageable)."""
85
+
85
86
  @abstractmethod
86
87
  def stage(self) -> AsyncStatus:
87
- """An optional hook for "setting up" the device for acquisition.
88
+ """Set up the device for acquisition.
88
89
 
89
- It should return a ``Status`` that is marked done when the device is
90
- done staging.
90
+ :return: An `AsyncStatus` that is marked done when the device is done staging.
91
91
  """
92
92
 
93
93
  @abstractmethod
94
94
  def unstage(self) -> AsyncStatus:
95
- """A hook for "cleaning up" the device after acquisition.
95
+ """Clean up the device after acquisition.
96
+
97
+ :return: An `AsyncStatus` that is marked done when the device is done unstaging.
98
+ """
99
+
100
+
101
+ @runtime_checkable
102
+ class AsyncMovable(Protocol[T_co]):
103
+ @abstractmethod
104
+ def set(self, value: T_co) -> AsyncStatus:
105
+ """Return a ``Status`` that is marked done when the device is done moving."""
96
106
 
97
- It should return a ``Status`` that is marked done when the device is finished
98
- unstaging.
107
+
108
+ @runtime_checkable
109
+ class AsyncLocatable(AsyncMovable[T], Protocol):
110
+ @abstractmethod
111
+ async def locate(self) -> Location[T]:
112
+ """Return the current location of a Device.
113
+
114
+ While a ``Readable`` reports many values, a ``Movable`` will have the
115
+ concept of location. This is where the Device currently is, and where it
116
+ was last requested to move to. This protocol formalizes how to get the
117
+ location from a ``Movable``.
99
118
  """
100
119
 
101
120
 
@@ -103,16 +122,17 @@ C = TypeVar("C", contravariant=True)
103
122
 
104
123
 
105
124
  class Watcher(Protocol, Generic[C]):
106
- @staticmethod
125
+ """Protocol for watching changes in values."""
126
+
107
127
  def __call__(
108
- *,
109
- current: C,
110
- initial: C,
111
- target: C,
112
- name: str | None,
113
- unit: str | None,
114
- precision: float | None,
115
- fraction: float | None,
116
- time_elapsed: float | None,
117
- time_remaining: float | None,
128
+ self,
129
+ current: C | None = None,
130
+ initial: C | None = None,
131
+ target: C | None = None,
132
+ name: str | None = None,
133
+ unit: str | None = None,
134
+ precision: int | None = None,
135
+ fraction: float | None = None,
136
+ time_elapsed: float | None = None,
137
+ time_remaining: float | None = None,
118
138
  ) -> Any: ...