ophyd-async 0.3a2__py3-none-any.whl → 0.3a3__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 (40) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +11 -1
  3. ophyd_async/core/detector.py +8 -9
  4. ophyd_async/core/flyer.py +2 -2
  5. ophyd_async/core/signal.py +102 -7
  6. ophyd_async/core/signal_backend.py +2 -2
  7. ophyd_async/core/sim_signal_backend.py +6 -6
  8. ophyd_async/core/standard_readable.py +211 -24
  9. ophyd_async/epics/_backend/_aioca.py +6 -6
  10. ophyd_async/epics/_backend/_p4p.py +17 -11
  11. ophyd_async/epics/areadetector/__init__.py +4 -0
  12. ophyd_async/epics/areadetector/aravis.py +7 -9
  13. ophyd_async/epics/areadetector/controllers/__init__.py +2 -1
  14. ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
  15. ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
  16. ophyd_async/epics/areadetector/drivers/__init__.py +6 -0
  17. ophyd_async/epics/areadetector/drivers/kinetix_driver.py +24 -0
  18. ophyd_async/epics/areadetector/drivers/vimba_driver.py +58 -0
  19. ophyd_async/epics/areadetector/kinetix.py +46 -0
  20. ophyd_async/epics/areadetector/pilatus.py +7 -12
  21. ophyd_async/epics/areadetector/single_trigger_det.py +14 -6
  22. ophyd_async/epics/areadetector/vimba.py +43 -0
  23. ophyd_async/epics/areadetector/writers/hdf_writer.py +6 -3
  24. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +1 -0
  25. ophyd_async/epics/demo/__init__.py +19 -22
  26. ophyd_async/epics/motion/motor.py +11 -12
  27. ophyd_async/epics/signal/signal.py +1 -1
  28. ophyd_async/log.py +130 -0
  29. ophyd_async/panda/writers/_hdf_writer.py +3 -3
  30. ophyd_async/protocols.py +26 -3
  31. ophyd_async/sim/demo/sim_motor.py +11 -9
  32. ophyd_async/sim/pattern_generator.py +4 -4
  33. ophyd_async/sim/sim_pattern_detector_writer.py +2 -2
  34. ophyd_async/sim/sim_pattern_generator.py +2 -2
  35. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a3.dist-info}/METADATA +20 -3
  36. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a3.dist-info}/RECORD +40 -33
  37. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a3.dist-info}/LICENSE +0 -0
  38. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a3.dist-info}/WHEEL +0 -0
  39. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a3.dist-info}/entry_points.txt +0 -0
  40. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a3.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.3a2'
15
+ __version__ = version = '0.3a3'
16
16
  __version_tuple__ = version_tuple = (0, 3)
@@ -30,6 +30,10 @@ from .signal import (
30
30
  SignalRW,
31
31
  SignalW,
32
32
  SignalX,
33
+ assert_configuration,
34
+ assert_emitted,
35
+ assert_reading,
36
+ assert_value,
33
37
  observe_value,
34
38
  set_and_wait_for_value,
35
39
  set_sim_callback,
@@ -41,7 +45,7 @@ from .signal import (
41
45
  )
42
46
  from .signal_backend import SignalBackend
43
47
  from .sim_signal_backend import SimSignalBackend
44
- from .standard_readable import StandardReadable
48
+ from .standard_readable import ConfigSignal, HintedSignal, StandardReadable
45
49
  from .utils import (
46
50
  DEFAULT_TIMEOUT,
47
51
  Callback,
@@ -84,6 +88,8 @@ __all__ = [
84
88
  "ShapeProvider",
85
89
  "StaticDirectoryProvider",
86
90
  "StandardReadable",
91
+ "ConfigSignal",
92
+ "HintedSignal",
87
93
  "TriggerInfo",
88
94
  "TriggerLogic",
89
95
  "HardwareTriggeredFlyable",
@@ -103,4 +109,8 @@ __all__ = [
103
109
  "walk_rw_signals",
104
110
  "load_device",
105
111
  "save_device",
112
+ "assert_reading",
113
+ "assert_value",
114
+ "assert_configuration",
115
+ "assert_emitted",
106
116
  ]
@@ -19,7 +19,7 @@ from typing import (
19
19
 
20
20
  from bluesky.protocols import (
21
21
  Collectable,
22
- Descriptor,
22
+ DataKey,
23
23
  Flyable,
24
24
  Preparable,
25
25
  Reading,
@@ -33,7 +33,6 @@ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
33
33
 
34
34
  from .async_status import AsyncStatus
35
35
  from .device import Device
36
- from .signal import SignalR
37
36
  from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
38
37
 
39
38
  T = TypeVar("T")
@@ -110,7 +109,7 @@ class DetectorWriter(ABC):
110
109
  (e.g. an HDF5 file)"""
111
110
 
112
111
  @abstractmethod
113
- async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
112
+ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
114
113
  """Open writer and wait for it to be ready for data.
115
114
 
116
115
  Args:
@@ -161,7 +160,7 @@ class StandardDetector(
161
160
  self,
162
161
  controller: DetectorControl,
163
162
  writer: DetectorWriter,
164
- config_sigs: Sequence[SignalR] = (),
163
+ config_sigs: Sequence[AsyncReadable] = (),
165
164
  name: str = "",
166
165
  writer_timeout: float = DEFAULT_TIMEOUT,
167
166
  ) -> None:
@@ -181,7 +180,7 @@ class StandardDetector(
181
180
  """
182
181
  self._controller = controller
183
182
  self._writer = writer
184
- self._describe: Dict[str, Descriptor] = {}
183
+ self._describe: Dict[str, DataKey] = {}
185
184
  self._config_sigs = list(config_sigs)
186
185
  self._frame_writing_timeout = writer_timeout
187
186
  # For prepare
@@ -214,7 +213,7 @@ class StandardDetector(
214
213
  async def _check_config_sigs(self):
215
214
  """Checks configuration signals are named and connected."""
216
215
  for signal in self._config_sigs:
217
- if signal._name == "":
216
+ if signal.name == "":
218
217
  raise Exception(
219
218
  "config signal must be named before it is passed to the detector"
220
219
  )
@@ -234,14 +233,14 @@ class StandardDetector(
234
233
  async def read_configuration(self) -> Dict[str, Reading]:
235
234
  return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
236
235
 
237
- async def describe_configuration(self) -> Dict[str, Descriptor]:
236
+ async def describe_configuration(self) -> Dict[str, DataKey]:
238
237
  return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
239
238
 
240
239
  async def read(self) -> Dict[str, Reading]:
241
240
  # All data is in StreamResources, not Events, so nothing to output here
242
241
  return {}
243
242
 
244
- async def describe(self) -> Dict[str, Descriptor]:
243
+ async def describe(self) -> Dict[str, DataKey]:
245
244
  return self._describe
246
245
 
247
246
  @AsyncStatus.wrap
@@ -330,7 +329,7 @@ class StandardDetector(
330
329
  assert self._fly_status, "Kickoff not run"
331
330
  return await self._fly_status
332
331
 
333
- async def describe_collect(self) -> Dict[str, Descriptor]:
332
+ async def describe_collect(self) -> Dict[str, DataKey]:
334
333
  return self._describe
335
334
 
336
335
  async def collect_asset_docs(
ophyd_async/core/flyer.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Dict, Generic, Sequence, TypeVar
3
3
 
4
- from bluesky.protocols import Descriptor, Flyable, Preparable, Reading, Stageable
4
+ from bluesky.protocols import DataKey, Flyable, Preparable, Reading, Stageable
5
5
 
6
6
  from .async_status import AsyncStatus
7
7
  from .device import Device
@@ -74,7 +74,7 @@ class HardwareTriggeredFlyable(
74
74
  async def complete(self) -> None:
75
75
  await self._trigger_logic.complete()
76
76
 
77
- async def describe_configuration(self) -> Dict[str, Descriptor]:
77
+ async def describe_configuration(self) -> Dict[str, DataKey]:
78
78
  return await merge_gathered_dicts(
79
79
  [sig.describe() for sig in self._configuration_signals]
80
80
  )
@@ -2,19 +2,29 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import functools
5
- from typing import AsyncGenerator, Callable, Dict, Generic, Optional, Tuple, Type, Union
5
+ from typing import (
6
+ Any,
7
+ AsyncGenerator,
8
+ Callable,
9
+ Dict,
10
+ Generic,
11
+ Mapping,
12
+ Optional,
13
+ Tuple,
14
+ Type,
15
+ Union,
16
+ )
6
17
 
7
18
  from bluesky.protocols import (
8
- Descriptor,
19
+ DataKey,
9
20
  Locatable,
10
21
  Location,
11
22
  Movable,
12
23
  Reading,
13
- Stageable,
14
24
  Subscribable,
15
25
  )
16
26
 
17
- from ophyd_async.protocols import AsyncReadable
27
+ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable
18
28
 
19
29
  from .async_status import AsyncStatus
20
30
  from .device import Device
@@ -128,7 +138,7 @@ class _SignalCache(Generic[T]):
128
138
  return self._staged or bool(self._listeners)
129
139
 
130
140
 
131
- class SignalR(Signal[T], AsyncReadable, Stageable, Subscribable):
141
+ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
132
142
  """Signal that can be read from and monitored"""
133
143
 
134
144
  _cache: Optional[_SignalCache] = None
@@ -161,9 +171,9 @@ class SignalR(Signal[T], AsyncReadable, Stageable, Subscribable):
161
171
  return {self.name: await self._backend_or_cache(cached).get_reading()}
162
172
 
163
173
  @_add_timeout
164
- async def describe(self) -> Dict[str, Descriptor]:
174
+ async def describe(self) -> Dict[str, DataKey]:
165
175
  """Return a single item dict with the descriptor in it"""
166
- return {self.name: await self._backend.get_descriptor(self.source)}
176
+ return {self.name: await self._backend.get_datakey(self.source)}
167
177
 
168
178
  @_add_timeout
169
179
  async def get_value(self, cached: Optional[bool] = None) -> T:
@@ -272,6 +282,91 @@ def soft_signal_r_and_backend(
272
282
  return (signal, backend)
273
283
 
274
284
 
285
+ async def assert_value(signal: SignalR[T], value: Any) -> None:
286
+ """Assert a signal's value and compare it an expected signal.
287
+
288
+ Parameters
289
+ ----------
290
+ signal:
291
+ signal with get_value.
292
+ value:
293
+ The expected value from the signal.
294
+
295
+ Notes
296
+ -----
297
+ Example usage::
298
+ await assert_value(signal, value)
299
+
300
+ """
301
+ assert await signal.get_value() == value
302
+
303
+
304
+ async def assert_reading(
305
+ readable: AsyncReadable, reading: Mapping[str, Reading]
306
+ ) -> None:
307
+ """Assert readings from readable.
308
+
309
+ Parameters
310
+ ----------
311
+ readable:
312
+ Callable with readable.read function that generate readings.
313
+
314
+ reading:
315
+ The expected readings from the readable.
316
+
317
+ Notes
318
+ -----
319
+ Example usage::
320
+ await assert_reading(readable, reading)
321
+
322
+ """
323
+ assert await readable.read() == reading
324
+
325
+
326
+ async def assert_configuration(
327
+ configurable: AsyncConfigurable,
328
+ configuration: Mapping[str, Reading],
329
+ ) -> None:
330
+ """Assert readings from Configurable.
331
+
332
+ Parameters
333
+ ----------
334
+ configurable:
335
+ Configurable with Configurable.read function that generate readings.
336
+
337
+ configuration:
338
+ The expected readings from configurable.
339
+
340
+ Notes
341
+ -----
342
+ Example usage::
343
+ await assert_configuration(configurable configuration)
344
+
345
+ """
346
+ assert await configurable.read_configuration() == configuration
347
+
348
+
349
+ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
350
+ """Assert emitted document generated by running a Bluesky plan
351
+
352
+ Parameters
353
+ ----------
354
+ Doc:
355
+ A dictionary
356
+
357
+ numbers:
358
+ expected emission in kwarg from
359
+
360
+ Notes
361
+ -----
362
+ Example usage::
363
+ assert_emitted(docs, start=1, descriptor=1,
364
+ resource=1, datum=1, event=1, stop=1)
365
+ """
366
+ assert list(docs) == list(numbers)
367
+ assert {name: len(d) for name, d in docs.items()} == numbers
368
+
369
+
275
370
  async def observe_value(signal: SignalR[T], timeout=None) -> AsyncGenerator[T, None]:
276
371
  """Subscribe to the value of a signal so it can be iterated from.
277
372
 
@@ -1,7 +1,7 @@
1
1
  from abc import abstractmethod
2
2
  from typing import Generic, Optional, Type
3
3
 
4
- from bluesky.protocols import Descriptor, Reading
4
+ from bluesky.protocols import DataKey, Reading
5
5
 
6
6
  from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
7
7
 
@@ -27,7 +27,7 @@ class SignalBackend(Generic[T]):
27
27
  """Put a value to the PV, if wait then wait for completion for up to timeout"""
28
28
 
29
29
  @abstractmethod
30
- async def get_descriptor(self, source: str) -> Descriptor:
30
+ async def get_datakey(self, source: str) -> DataKey:
31
31
  """Metadata like source, dtype, shape, precision, units"""
32
32
 
33
33
  @abstractmethod
@@ -9,7 +9,7 @@ from enum import Enum
9
9
  from typing import Any, Dict, Generic, Optional, Type, Union, cast, get_origin
10
10
 
11
11
  import numpy as np
12
- from bluesky.protocols import Descriptor, Dtype, Reading
12
+ from bluesky.protocols import DataKey, Dtype, Reading
13
13
 
14
14
  from .signal_backend import SignalBackend
15
15
  from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype
@@ -36,7 +36,7 @@ class SimConverter(Generic[T]):
36
36
  alarm_severity=-1 if severity > 2 else severity,
37
37
  )
38
38
 
39
- def descriptor(self, source: str, value) -> Descriptor:
39
+ def get_datakey(self, source: str, value) -> DataKey:
40
40
  dtype = type(value)
41
41
  if np.issubdtype(dtype, np.integer):
42
42
  dtype = int
@@ -56,7 +56,7 @@ class SimConverter(Generic[T]):
56
56
 
57
57
 
58
58
  class SimArrayConverter(SimConverter):
59
- def descriptor(self, source: str, value) -> Descriptor:
59
+ def get_datakey(self, source: str, value) -> DataKey:
60
60
  return {"source": source, "dtype": "array", "shape": [len(value)]}
61
61
 
62
62
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
@@ -79,7 +79,7 @@ class SimEnumConverter(SimConverter):
79
79
  else:
80
80
  return self.enum_class(value)
81
81
 
82
- def descriptor(self, source: str, value) -> Descriptor:
82
+ def get_datakey(self, source: str, value) -> DataKey:
83
83
  choices = [e.value for e in self.enum_class]
84
84
  return {"source": source, "dtype": "string", "shape": [], "choices": choices} # type: ignore
85
85
 
@@ -164,8 +164,8 @@ class SimSignalBackend(SignalBackend[T]):
164
164
  if self.callback:
165
165
  self.callback(reading, self._value)
166
166
 
167
- async def get_descriptor(self, source: str) -> Descriptor:
168
- return self.converter.descriptor(source, self._value)
167
+ async def get_datakey(self, source: str) -> DataKey:
168
+ return self.converter.get_datakey(source, self._value)
169
169
 
170
170
  async def get_reading(self) -> Reading:
171
171
  return self.converter.reading(self._value, self._timestamp, self._severity)
@@ -1,16 +1,34 @@
1
- from typing import Dict, Sequence, Tuple
1
+ import warnings
2
+ from contextlib import contextmanager
3
+ from typing import (
4
+ Callable,
5
+ Dict,
6
+ Generator,
7
+ Optional,
8
+ Sequence,
9
+ Tuple,
10
+ Type,
11
+ Union,
12
+ )
2
13
 
3
- from bluesky.protocols import Descriptor, Reading, Stageable
14
+ from bluesky.protocols import DataKey, HasHints, Hints, Reading
4
15
 
5
- from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
16
+ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageable
6
17
 
7
18
  from .async_status import AsyncStatus
8
- from .device import Device
19
+ from .device import Device, DeviceVector
9
20
  from .signal import SignalR
10
21
  from .utils import merge_gathered_dicts
11
22
 
23
+ ReadableChild = Union[AsyncReadable, AsyncConfigurable, AsyncStageable, HasHints]
24
+ ReadableChildWrapper = Union[
25
+ Callable[[ReadableChild], ReadableChild], Type["ConfigSignal"], Type["HintedSignal"]
26
+ ]
12
27
 
13
- class StandardReadable(Device, AsyncReadable, AsyncConfigurable, Stageable):
28
+
29
+ class StandardReadable(
30
+ Device, AsyncReadable, AsyncConfigurable, AsyncStageable, HasHints
31
+ ):
14
32
  """Device that owns its children and provides useful default behavior.
15
33
 
16
34
  - When its name is set it renames child Devices
@@ -18,9 +36,12 @@ class StandardReadable(Device, AsyncReadable, AsyncConfigurable, Stageable):
18
36
  - These signals will be subscribed for read() between stage() and unstage()
19
37
  """
20
38
 
21
- _read_signals: Tuple[SignalR, ...] = ()
22
- _configuration_signals: Tuple[SignalR, ...] = ()
23
- _read_uncached_signals: Tuple[SignalR, ...] = ()
39
+ # These must be immutable types to avoid accidental sharing between
40
+ # different instances of the class
41
+ _readables: Tuple[AsyncReadable, ...] = ()
42
+ _configurables: Tuple[AsyncConfigurable, ...] = ()
43
+ _stageables: Tuple[AsyncStageable, ...] = ()
44
+ _has_hints: Tuple[HasHints, ...] = ()
24
45
 
25
46
  def set_readable_signals(
26
47
  self,
@@ -38,37 +59,203 @@ class StandardReadable(Device, AsyncReadable, AsyncConfigurable, Stageable):
38
59
  read_uncached:
39
60
  Signals to make up :meth:`~StandardReadable.read` that won't be cached
40
61
  """
41
- self._read_signals = tuple(read)
42
- self._configuration_signals = tuple(config)
43
- self._read_uncached_signals = tuple(read_uncached)
62
+ warnings.warn(
63
+ DeprecationWarning(
64
+ "Migrate to `add_children_as_readables` context manager or "
65
+ "`add_readables` method"
66
+ )
67
+ )
68
+ self.add_readables(read, wrapper=HintedSignal)
69
+ self.add_readables(config, wrapper=ConfigSignal)
70
+ self.add_readables(read_uncached, wrapper=HintedSignal.uncached)
44
71
 
45
72
  @AsyncStatus.wrap
46
73
  async def stage(self) -> None:
47
- for sig in self._read_signals + self._configuration_signals:
74
+ for sig in self._stageables:
48
75
  await sig.stage().task
49
76
 
50
77
  @AsyncStatus.wrap
51
78
  async def unstage(self) -> None:
52
- for sig in self._read_signals + self._configuration_signals:
79
+ for sig in self._stageables:
53
80
  await sig.unstage().task
54
81
 
55
- async def describe_configuration(self) -> Dict[str, Descriptor]:
82
+ async def describe_configuration(self) -> Dict[str, DataKey]:
56
83
  return await merge_gathered_dicts(
57
- [sig.describe() for sig in self._configuration_signals]
84
+ [sig.describe_configuration() for sig in self._configurables]
58
85
  )
59
86
 
60
87
  async def read_configuration(self) -> Dict[str, Reading]:
61
88
  return await merge_gathered_dicts(
62
- [sig.read() for sig in self._configuration_signals]
89
+ [sig.read_configuration() for sig in self._configurables]
63
90
  )
64
91
 
65
- async def describe(self) -> Dict[str, Descriptor]:
66
- return await merge_gathered_dicts(
67
- [sig.describe() for sig in self._read_signals + self._read_uncached_signals]
68
- )
92
+ async def describe(self) -> Dict[str, DataKey]:
93
+ return await merge_gathered_dicts([sig.describe() for sig in self._readables])
69
94
 
70
95
  async def read(self) -> Dict[str, Reading]:
71
- return await merge_gathered_dicts(
72
- [sig.read() for sig in self._read_signals]
73
- + [sig.read(cached=False) for sig in self._read_uncached_signals]
74
- )
96
+ return await merge_gathered_dicts([sig.read() for sig in self._readables])
97
+
98
+ @property
99
+ def hints(self) -> Hints:
100
+ hints: Hints = {}
101
+ for new_hint in self._has_hints:
102
+ # Merge the existing and new hints, based on the type of the value.
103
+ # This avoids default dict merge behaviour that overrides the values;
104
+ # we want to combine them when they are Sequences, and ensure they are
105
+ # identical when string values.
106
+ for key, value in new_hint.hints.items():
107
+ if isinstance(value, str):
108
+ if key in hints:
109
+ assert (
110
+ hints[key] == value # type: ignore[literal-required]
111
+ ), f"Hints key {key} value may not be overridden"
112
+ else:
113
+ hints[key] = value # type: ignore[literal-required]
114
+ elif isinstance(value, Sequence):
115
+ if key in hints:
116
+ for new_val in value:
117
+ assert (
118
+ new_val not in hints[key] # type: ignore[literal-required]
119
+ ), f"Hint {key} {new_val} overrides existing hint"
120
+ hints[key] = ( # type: ignore[literal-required]
121
+ hints[key] + value # type: ignore[literal-required]
122
+ )
123
+ else:
124
+ hints[key] = value # type: ignore[literal-required]
125
+ else:
126
+ raise TypeError(
127
+ f"{new_hint.name}: Unknown type for value '{value}' "
128
+ f" for key '{key}'"
129
+ )
130
+
131
+ return hints
132
+
133
+ @contextmanager
134
+ def add_children_as_readables(
135
+ self,
136
+ wrapper: Optional[ReadableChildWrapper] = None,
137
+ ) -> Generator[None, None, None]:
138
+ """Context manager to wrap adding Devices
139
+
140
+ Add Devices to this class instance inside the Context Manager to automatically
141
+ add them to the correct fields, based on the Device's interfaces.
142
+
143
+ The provided wrapper class will be applied to all Devices and can be used to
144
+ specify their behaviour.
145
+
146
+ Parameters
147
+ ----------
148
+ wrapper:
149
+ Wrapper class to apply to all Devices created inside the context manager.
150
+
151
+ See Also
152
+ --------
153
+ :func:`~StandardReadable.add_readables`
154
+ :class:`ConfigSignal`
155
+ :class:`HintedSignal`
156
+ :meth:`HintedSignal.uncached`
157
+ """
158
+
159
+ dict_copy = self.__dict__.copy()
160
+
161
+ yield
162
+
163
+ # Set symmetric difference operator gives all newly added keys
164
+ new_keys = dict_copy.keys() ^ self.__dict__.keys()
165
+ new_values = [self.__dict__[key] for key in new_keys]
166
+
167
+ flattened_values = []
168
+ for value in new_values:
169
+ if isinstance(value, DeviceVector):
170
+ children = value.children()
171
+ flattened_values.extend([x[1] for x in children])
172
+ else:
173
+ flattened_values.append(value)
174
+
175
+ new_devices = list(filter(lambda x: isinstance(x, Device), flattened_values))
176
+ self.add_readables(new_devices, wrapper)
177
+
178
+ def add_readables(
179
+ self,
180
+ devices: Sequence[Device],
181
+ wrapper: Optional[ReadableChildWrapper] = None,
182
+ ) -> None:
183
+ """Add the given devices to the lists of known Devices
184
+
185
+ Add the provided Devices to the relevant fields, based on the Signal's
186
+ interfaces.
187
+
188
+ The provided wrapper class will be applied to all Devices and can be used to
189
+ specify their behaviour.
190
+
191
+ Parameters
192
+ ----------
193
+ devices:
194
+ The devices to be added
195
+ wrapper:
196
+ Wrapper class to apply to all Devices created inside the context manager.
197
+
198
+ See Also
199
+ --------
200
+ :func:`~StandardReadable.add_children_as_readables`
201
+ :class:`ConfigSignal`
202
+ :class:`HintedSignal`
203
+ :meth:`HintedSignal.uncached`
204
+ """
205
+
206
+ for readable in devices:
207
+ obj = readable
208
+ if wrapper:
209
+ obj = wrapper(readable)
210
+
211
+ if isinstance(obj, AsyncReadable):
212
+ self._readables += (obj,)
213
+
214
+ if isinstance(obj, AsyncConfigurable):
215
+ self._configurables += (obj,)
216
+
217
+ if isinstance(obj, AsyncStageable):
218
+ self._stageables += (obj,)
219
+
220
+ if isinstance(obj, HasHints):
221
+ self._has_hints += (obj,)
222
+
223
+
224
+ class ConfigSignal(AsyncConfigurable):
225
+ def __init__(self, signal: ReadableChild) -> None:
226
+ assert isinstance(signal, SignalR), f"Expected signal, got {signal}"
227
+ self.signal = signal
228
+
229
+ async def read_configuration(self) -> Dict[str, Reading]:
230
+ return await self.signal.read()
231
+
232
+ async def describe_configuration(self) -> Dict[str, DataKey]:
233
+ return await self.signal.describe()
234
+
235
+
236
+ class HintedSignal(HasHints, AsyncReadable):
237
+ def __init__(self, signal: ReadableChild, allow_cache: bool = True) -> None:
238
+ assert isinstance(signal, SignalR), f"Expected signal, got {signal}"
239
+ self.signal = signal
240
+ self.cached = None if allow_cache else allow_cache
241
+ if allow_cache:
242
+ self.stage = signal.stage
243
+ self.unstage = signal.unstage
244
+
245
+ async def read(self) -> Dict[str, Reading]:
246
+ return await self.signal.read(cached=self.cached)
247
+
248
+ async def describe(self) -> Dict[str, DataKey]:
249
+ return await self.signal.describe()
250
+
251
+ @property
252
+ def name(self) -> str:
253
+ return self.signal.name
254
+
255
+ @property
256
+ def hints(self) -> Hints:
257
+ return {"fields": [self.signal.name]}
258
+
259
+ @classmethod
260
+ def uncached(cls, signal: ReadableChild) -> "HintedSignal":
261
+ return cls(signal, allow_cache=False)
@@ -15,7 +15,7 @@ from aioca import (
15
15
  caput,
16
16
  )
17
17
  from aioca.types import AugmentedValue, Dbr, Format
18
- from bluesky.protocols import Descriptor, Dtype, Reading
18
+ from bluesky.protocols import DataKey, Dtype, Reading
19
19
  from epicscorelibs.ca import dbr
20
20
 
21
21
  from ophyd_async.core import (
@@ -58,7 +58,7 @@ class CaConverter:
58
58
  "alarm_severity": -1 if value.severity > 2 else value.severity,
59
59
  }
60
60
 
61
- def descriptor(self, source: str, value: AugmentedValue) -> Descriptor:
61
+ def get_datakey(self, source: str, value: AugmentedValue) -> DataKey:
62
62
  return {"source": source, "dtype": dbr_to_dtype[value.datatype], "shape": []}
63
63
 
64
64
 
@@ -73,7 +73,7 @@ class CaLongStrConverter(CaConverter):
73
73
 
74
74
 
75
75
  class CaArrayConverter(CaConverter):
76
- def descriptor(self, source: str, value: AugmentedValue) -> Descriptor:
76
+ def get_datakey(self, source: str, value: AugmentedValue) -> DataKey:
77
77
  return {"source": source, "dtype": "array", "shape": [len(value)]}
78
78
 
79
79
 
@@ -90,7 +90,7 @@ class CaEnumConverter(CaConverter):
90
90
  def value(self, value: AugmentedValue):
91
91
  return self.enum_class(value)
92
92
 
93
- def descriptor(self, source: str, value: AugmentedValue) -> Descriptor:
93
+ def get_datakey(self, source: str, value: AugmentedValue) -> DataKey:
94
94
  choices = [e.value for e in self.enum_class]
95
95
  return {"source": source, "dtype": "string", "shape": [], "choices": choices}
96
96
 
@@ -218,9 +218,9 @@ class CaSignalBackend(SignalBackend[T]):
218
218
  timeout=None,
219
219
  )
220
220
 
221
- async def get_descriptor(self, source: str) -> Descriptor:
221
+ async def get_datakey(self, source: str) -> DataKey:
222
222
  value = await self._caget(FORMAT_CTRL)
223
- return self.converter.descriptor(source, value)
223
+ return self.converter.get_datakey(source, value)
224
224
 
225
225
  async def get_reading(self) -> Reading:
226
226
  value = await self._caget(FORMAT_TIME)