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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +13 -20
  3. ophyd_async/core/_detector.py +61 -37
  4. ophyd_async/core/_device.py +102 -80
  5. ophyd_async/core/_device_filler.py +17 -8
  6. ophyd_async/core/_flyer.py +2 -2
  7. ophyd_async/core/_readable.py +30 -23
  8. ophyd_async/core/_settings.py +104 -0
  9. ophyd_async/core/_signal.py +55 -17
  10. ophyd_async/core/_signal_backend.py +4 -1
  11. ophyd_async/core/_soft_signal_backend.py +2 -1
  12. ophyd_async/core/_table.py +18 -10
  13. ophyd_async/core/_utils.py +5 -3
  14. ophyd_async/core/_yaml_settings.py +64 -0
  15. ophyd_async/epics/adandor/__init__.py +9 -0
  16. ophyd_async/epics/adandor/_andor.py +45 -0
  17. ophyd_async/epics/adandor/_andor_controller.py +49 -0
  18. ophyd_async/epics/adandor/_andor_io.py +36 -0
  19. ophyd_async/epics/adaravis/__init__.py +3 -1
  20. ophyd_async/epics/adaravis/_aravis.py +23 -37
  21. ophyd_async/epics/adaravis/_aravis_controller.py +13 -22
  22. ophyd_async/epics/adcore/__init__.py +15 -8
  23. ophyd_async/epics/adcore/_core_detector.py +41 -0
  24. ophyd_async/epics/adcore/_core_io.py +35 -10
  25. ophyd_async/epics/adcore/_core_logic.py +98 -86
  26. ophyd_async/epics/adcore/_core_writer.py +219 -0
  27. ophyd_async/epics/adcore/_hdf_writer.py +38 -62
  28. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  29. ophyd_async/epics/adcore/_single_trigger.py +4 -3
  30. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  31. ophyd_async/epics/adcore/_utils.py +2 -1
  32. ophyd_async/epics/adkinetix/_kinetix.py +29 -24
  33. ophyd_async/epics/adkinetix/_kinetix_controller.py +9 -21
  34. ophyd_async/epics/adpilatus/__init__.py +2 -2
  35. ophyd_async/epics/adpilatus/_pilatus.py +27 -39
  36. ophyd_async/epics/adpilatus/_pilatus_controller.py +44 -22
  37. ophyd_async/epics/adsimdetector/__init__.py +3 -3
  38. ophyd_async/epics/adsimdetector/_sim.py +33 -17
  39. ophyd_async/epics/advimba/_vimba.py +23 -23
  40. ophyd_async/epics/advimba/_vimba_controller.py +10 -24
  41. ophyd_async/epics/core/_aioca.py +31 -14
  42. ophyd_async/epics/core/_p4p.py +40 -16
  43. ophyd_async/epics/core/_util.py +1 -1
  44. ophyd_async/epics/motor.py +18 -10
  45. ophyd_async/epics/sim/_ioc.py +29 -0
  46. ophyd_async/epics/{demo → sim}/_mover.py +10 -4
  47. ophyd_async/epics/testing/__init__.py +14 -14
  48. ophyd_async/epics/testing/_example_ioc.py +48 -65
  49. ophyd_async/epics/testing/_utils.py +17 -45
  50. ophyd_async/epics/testing/test_records.db +8 -0
  51. ophyd_async/fastcs/panda/__init__.py +0 -2
  52. ophyd_async/fastcs/panda/_control.py +7 -2
  53. ophyd_async/fastcs/panda/_hdf_panda.py +3 -1
  54. ophyd_async/fastcs/panda/_table.py +4 -1
  55. ophyd_async/plan_stubs/__init__.py +14 -0
  56. ophyd_async/plan_stubs/_ensure_connected.py +11 -17
  57. ophyd_async/plan_stubs/_fly.py +1 -1
  58. ophyd_async/plan_stubs/_nd_attributes.py +7 -5
  59. ophyd_async/plan_stubs/_panda.py +13 -0
  60. ophyd_async/plan_stubs/_settings.py +125 -0
  61. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  62. ophyd_async/sim/__init__.py +19 -0
  63. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
  64. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
  65. ophyd_async/tango/core/_signal.py +3 -1
  66. ophyd_async/tango/core/_tango_transport.py +12 -14
  67. ophyd_async/tango/{demo → sim}/_mover.py +5 -2
  68. ophyd_async/testing/__init__.py +19 -0
  69. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  70. ophyd_async/testing/_assert.py +88 -40
  71. ophyd_async/testing/_mock_signal_utils.py +3 -3
  72. ophyd_async/testing/_one_of_everything.py +126 -0
  73. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/METADATA +2 -2
  74. ophyd_async-0.9.0a2.dist-info/RECORD +129 -0
  75. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/WHEEL +1 -1
  76. ophyd_async/core/_device_save_loader.py +0 -274
  77. ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
  78. ophyd_async/fastcs/panda/_utils.py +0 -16
  79. ophyd_async/sim/demo/__init__.py +0 -19
  80. ophyd_async/sim/testing/__init__.py +0 -0
  81. ophyd_async-0.9.0a1.dist-info/RECORD +0 -119
  82. ophyd_async-0.9.0a1.dist-info/entry_points.txt +0 -2
  83. /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
  84. /ophyd_async/epics/{demo → sim}/_sensor.py +0 -0
  85. /ophyd_async/epics/{demo → sim}/mover.db +0 -0
  86. /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
  87. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
  88. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
  89. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
  90. /ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +0 -0
  91. /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
  92. /ophyd_async/tango/{demo → sim}/_counter.py +0 -0
  93. /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
  94. /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
  95. /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
  96. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/LICENSE +0 -0
  97. {ophyd_async-0.9.0a1.dist-info → ophyd_async-0.9.0a2.dist-info}/top_level.txt +0 -0
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.9.0a1'
15
+ __version__ = version = '0.9.0a2'
16
16
  __version_tuple__ = version_tuple = (0, 9, 0)
@@ -1,22 +1,13 @@
1
1
  from ._detector import (
2
2
  DetectorController,
3
+ DetectorControllerT,
3
4
  DetectorTrigger,
4
5
  DetectorWriter,
5
6
  StandardDetector,
6
7
  TriggerInfo,
7
8
  )
8
- from ._device import Device, DeviceCollector, DeviceConnector, DeviceVector
9
+ from ._device import Device, DeviceConnector, DeviceVector, init_devices
9
10
  from ._device_filler import DeviceFiller
10
- from ._device_save_loader import (
11
- all_at_once,
12
- get_signal_values,
13
- load_device,
14
- load_from_yaml,
15
- save_device,
16
- save_to_yaml,
17
- set_signal_values,
18
- walk_rw_signals,
19
- )
20
11
  from ._flyer import FlyerController, StandardFlyer
21
12
  from ._hdf_dataset import HDFDataset, HDFFile
22
13
  from ._log import config_ophyd_async_logging
@@ -41,6 +32,7 @@ from ._readable import (
41
32
  StandardReadable,
42
33
  StandardReadableFormat,
43
34
  )
35
+ from ._settings import Settings, SettingsProvider
44
36
  from ._signal import (
45
37
  Signal,
46
38
  SignalConnector,
@@ -55,9 +47,11 @@ from ._signal import (
55
47
  soft_signal_r_and_setter,
56
48
  soft_signal_rw,
57
49
  wait_for_value,
50
+ walk_rw_signals,
58
51
  )
59
52
  from ._signal_backend import (
60
53
  Array1D,
54
+ DTypeScalar_co,
61
55
  SignalBackend,
62
56
  SignalDatatype,
63
57
  SignalDatatypeT,
@@ -84,26 +78,20 @@ from ._utils import (
84
78
  in_micros,
85
79
  wait_for_connection,
86
80
  )
81
+ from ._yaml_settings import YamlSettingsProvider
87
82
 
88
83
  __all__ = [
89
84
  "DetectorController",
85
+ "DetectorControllerT",
90
86
  "DetectorTrigger",
91
87
  "DetectorWriter",
92
88
  "StandardDetector",
93
89
  "TriggerInfo",
94
90
  "Device",
95
91
  "DeviceConnector",
96
- "DeviceCollector",
92
+ "init_devices",
97
93
  "DeviceVector",
98
94
  "DeviceFiller",
99
- "all_at_once",
100
- "get_signal_values",
101
- "load_device",
102
- "load_from_yaml",
103
- "save_device",
104
- "save_to_yaml",
105
- "set_signal_values",
106
- "walk_rw_signals",
107
95
  "StandardFlyer",
108
96
  "FlyerController",
109
97
  "HDFDataset",
@@ -128,6 +116,8 @@ __all__ = [
128
116
  "HintedSignal",
129
117
  "StandardReadable",
130
118
  "StandardReadableFormat",
119
+ "Settings",
120
+ "SettingsProvider",
131
121
  "Signal",
132
122
  "SignalConnector",
133
123
  "SignalR",
@@ -141,7 +131,9 @@ __all__ = [
141
131
  "soft_signal_r_and_setter",
142
132
  "soft_signal_rw",
143
133
  "wait_for_value",
134
+ "walk_rw_signals",
144
135
  "Array1D",
136
+ "DTypeScalar_co",
145
137
  "SignalBackend",
146
138
  "make_datakey",
147
139
  "StrictEnum",
@@ -168,4 +160,5 @@ __all__ = [
168
160
  "in_micros",
169
161
  "wait_for_connection",
170
162
  "completed_status",
163
+ "YamlSettingsProvider",
171
164
  ]
@@ -5,10 +5,15 @@ import time
5
5
  from abc import ABC, abstractmethod
6
6
  from collections.abc import AsyncGenerator, AsyncIterator, Callable, Iterator, Sequence
7
7
  from functools import cached_property
8
+ from typing import (
9
+ Generic,
10
+ TypeVar,
11
+ )
8
12
 
9
13
  from bluesky.protocols import (
10
14
  Collectable,
11
15
  Flyable,
16
+ Hints,
12
17
  Preparable,
13
18
  Reading,
14
19
  Stageable,
@@ -87,7 +92,7 @@ class DetectorController(ABC):
87
92
  """For a given exposure, how long should the time between exposures be"""
88
93
 
89
94
  @abstractmethod
90
- async def prepare(self, trigger_info: TriggerInfo):
95
+ async def prepare(self, trigger_info: TriggerInfo) -> None:
91
96
  """
92
97
  Do all necessary steps to prepare the detector for triggers.
93
98
 
@@ -157,6 +162,23 @@ class DetectorWriter(ABC):
157
162
  async def close(self) -> None:
158
163
  """Close writer, blocks until I/O is complete"""
159
164
 
165
+ @property
166
+ def hints(self) -> Hints:
167
+ return {}
168
+
169
+
170
+ # Add type var for controller so we can define
171
+ # StandardDetector[KinetixController, ADWriter] for example
172
+ DetectorControllerT = TypeVar("DetectorControllerT", bound=DetectorController)
173
+ DetectorWriterT = TypeVar("DetectorWriterT", bound=DetectorWriter)
174
+
175
+
176
+ def _ensure_trigger_info_exists(trigger_info: TriggerInfo | None) -> TriggerInfo:
177
+ # make absolute sure we realy have a valid TriggerInfo ... mostly for pylance
178
+ if trigger_info is None:
179
+ raise RuntimeError("Trigger info must be set before calling this method.")
180
+ return trigger_info
181
+
160
182
 
161
183
  class StandardDetector(
162
184
  Device,
@@ -168,6 +190,7 @@ class StandardDetector(
168
190
  Flyable,
169
191
  Collectable,
170
192
  WritesStreamAssets,
193
+ Generic[DetectorControllerT, DetectorWriterT],
171
194
  ):
172
195
  """
173
196
  Useful detector base class for step and fly scanning detectors.
@@ -176,8 +199,8 @@ class StandardDetector(
176
199
 
177
200
  def __init__(
178
201
  self,
179
- controller: DetectorController,
180
- writer: DetectorWriter,
202
+ controller: DetectorControllerT,
203
+ writer: DetectorWriterT,
181
204
  config_sigs: Sequence[SignalR] = (),
182
205
  name: str = "",
183
206
  connector: DeviceConnector | None = None,
@@ -211,19 +234,11 @@ class StandardDetector(
211
234
  self._initial_frame: int = 0
212
235
  super().__init__(name, connector=connector)
213
236
 
214
- @property
215
- def controller(self) -> DetectorController:
216
- return self._controller
217
-
218
- @property
219
- def writer(self) -> DetectorWriter:
220
- return self._writer
221
-
222
237
  @AsyncStatus.wrap
223
238
  async def stage(self) -> None:
224
239
  # Disarm the detector, stop file writing.
225
240
  await self._check_config_sigs()
226
- await asyncio.gather(self.writer.close(), self.controller.disarm())
241
+ await asyncio.gather(self._writer.close(), self._controller.disarm())
227
242
  self._trigger_info = None
228
243
 
229
244
  async def _check_config_sigs(self):
@@ -244,7 +259,7 @@ class StandardDetector(
244
259
  @AsyncStatus.wrap
245
260
  async def unstage(self) -> None:
246
261
  # Stop data writing.
247
- await asyncio.gather(self.writer.close(), self.controller.disarm())
262
+ await asyncio.gather(self._writer.close(), self._controller.disarm())
248
263
 
249
264
  async def read_configuration(self) -> dict[str, Reading]:
250
265
  return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
@@ -271,15 +286,19 @@ class StandardDetector(
271
286
  frame_timeout=None,
272
287
  )
273
288
  )
274
- assert self._trigger_info
275
- assert self._trigger_info.trigger is DetectorTrigger.INTERNAL
289
+
290
+ self._trigger_info = _ensure_trigger_info_exists(self._trigger_info)
291
+ if self._trigger_info.trigger is not DetectorTrigger.INTERNAL:
292
+ msg = "The trigger method can only be called with INTERNAL triggering"
293
+ raise ValueError(msg)
294
+
276
295
  # Arm the detector and wait for it to finish.
277
- indices_written = await self.writer.get_indices_written()
278
- await self.controller.arm()
279
- await self.controller.wait_for_idle()
296
+ indices_written = await self._writer.get_indices_written()
297
+ await self._controller.arm()
298
+ await self._controller.wait_for_idle()
280
299
  end_observation = indices_written + 1
281
300
 
282
- async for index in self.writer.observe_indices_written(
301
+ async for index in self._writer.observe_indices_written(
283
302
  DEFAULT_TIMEOUT
284
303
  + (self._trigger_info.livetime or 0)
285
304
  + (self._trigger_info.deadtime or 0)
@@ -303,28 +322,29 @@ class StandardDetector(
303
322
  Args:
304
323
  value: TriggerInfo describing how to trigger the detector
305
324
  """
306
- if value.trigger != DetectorTrigger.INTERNAL:
307
- assert (
308
- value.deadtime
309
- ), "Deadtime must be supplied when in externally triggered mode"
310
- if value.deadtime:
311
- required = self.controller.get_deadtime(value.livetime)
312
- assert required <= value.deadtime, (
313
- f"Detector {self.controller} needs at least {required}s deadtime, "
314
- f"but trigger logic provides only {value.deadtime}s"
325
+ if value.trigger != DetectorTrigger.INTERNAL and not value.deadtime:
326
+ msg = "Deadtime must be supplied when in externally triggered mode"
327
+ raise ValueError(msg)
328
+ required_deadtime = self._controller.get_deadtime(value.livetime)
329
+ if value.deadtime and required_deadtime > value.deadtime:
330
+ msg = (
331
+ f"Detector {self._controller} needs at least {required_deadtime}s "
332
+ f"deadtime, but trigger logic provides only {value.deadtime}s"
315
333
  )
334
+ raise ValueError(msg)
335
+
316
336
  self._trigger_info = value
317
337
  self._number_of_triggers_iter = iter(
318
338
  self._trigger_info.number_of_triggers
319
339
  if isinstance(self._trigger_info.number_of_triggers, list)
320
340
  else [self._trigger_info.number_of_triggers]
321
341
  )
322
- self._initial_frame = await self.writer.get_indices_written()
342
+ self._initial_frame = await self._writer.get_indices_written()
323
343
  self._describe, _ = await asyncio.gather(
324
- self.writer.open(value.multiplier), self.controller.prepare(value)
344
+ self._writer.open(value.multiplier), self._controller.prepare(value)
325
345
  )
326
346
  if value.trigger != DetectorTrigger.INTERNAL:
327
- await self.controller.arm()
347
+ await self._controller.arm()
328
348
  self._fly_start = time.monotonic()
329
349
 
330
350
  @AsyncStatus.wrap
@@ -342,8 +362,8 @@ class StandardDetector(
342
362
 
343
363
  @WatchableAsyncStatus.wrap
344
364
  async def complete(self):
345
- assert self._trigger_info
346
- indices_written = self.writer.observe_indices_written(
365
+ self._trigger_info = _ensure_trigger_info_exists(self._trigger_info)
366
+ indices_written = self._writer.observe_indices_written(
347
367
  self._trigger_info.frame_timeout
348
368
  or (
349
369
  DEFAULT_TIMEOUT
@@ -372,7 +392,7 @@ class StandardDetector(
372
392
  self._completable_frames = 0
373
393
  self._frames_to_complete = 0
374
394
  self._number_of_triggers_iter = None
375
- await self.controller.wait_for_idle()
395
+ await self._controller.wait_for_idle()
376
396
 
377
397
  async def describe_collect(self) -> dict[str, DataKey]:
378
398
  return self._describe
@@ -384,9 +404,13 @@ class StandardDetector(
384
404
  # The index is optional, and provided for fly scans, however this needs to be
385
405
  # retrieved for step scans.
386
406
  if index is None:
387
- index = await self.writer.get_indices_written()
388
- async for doc in self.writer.collect_stream_docs(index):
407
+ index = await self._writer.get_indices_written()
408
+ async for doc in self._writer.collect_stream_docs(index):
389
409
  yield doc
390
410
 
391
411
  async def get_index(self) -> int:
392
- return await self.writer.get_indices_written()
412
+ return await self._writer.get_indices_written()
413
+
414
+ @property
415
+ def hints(self) -> Hints:
416
+ return self._writer.hints
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import sys
5
- from collections.abc import Coroutine, Iterator, Mapping, MutableMapping
5
+ from collections.abc import Awaitable, Callable, Iterator, Mapping, MutableMapping
6
6
  from functools import cached_property
7
7
  from logging import LoggerAdapter, getLogger
8
8
  from typing import Any, TypeVar
@@ -158,10 +158,12 @@ class Device(HasName):
158
158
  timeout:
159
159
  Time to wait before failing with a TimeoutError.
160
160
  """
161
- assert hasattr(self, "_connector"), (
162
- f"{self}: doesn't have attribute `_connector`,"
163
- " did you call `super().__init__` in your `__init__` method?"
164
- )
161
+ if not hasattr(self, "_connector"):
162
+ msg = (
163
+ f"{self}: doesn't have attribute `_connector`,"
164
+ " did you call `super().__init__` in your `__init__` method?"
165
+ )
166
+ raise RuntimeError(msg)
165
167
  if mock:
166
168
  # Always connect in mock mode serially
167
169
  if isinstance(mock, LazyMock):
@@ -182,7 +184,9 @@ class Device(HasName):
182
184
  self._mock = None
183
185
  coro = self._connector.connect_real(self, timeout, force_reconnect)
184
186
  self._connect_task = asyncio.create_task(coro)
185
- assert self._connect_task, "Connect task not created, this shouldn't happen"
187
+ if not self._connect_task:
188
+ msg = "Connect task not created, this shouldn't happen"
189
+ raise RuntimeError(msg)
186
190
  # Wait for it to complete
187
191
  await self._connect_task
188
192
 
@@ -206,7 +210,7 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
206
210
 
207
211
  In the below example, foos becomes a dictionary on the parent device
208
212
  at runtime, so parent.foos[2] returns a FooDevice. For example usage see
209
- :class:`~ophyd_async.epics.demo.DynamicSensorGroup`
213
+ :class:`~ophyd_async.epics.sim.DynamicSensorGroup`
210
214
  """
211
215
 
212
216
  def __init__(
@@ -232,8 +236,12 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
232
236
  def __setitem__(self, key: int, value: DeviceT) -> None:
233
237
  # Check the types on entry to dict to make sure we can't accidentally
234
238
  # make a non-integer named child
235
- assert isinstance(key, int), f"Expected int, got {key}"
236
- assert isinstance(value, Device), f"Expected Device, got {value}"
239
+ if not isinstance(key, int):
240
+ msg = f"Expected int, got {key}"
241
+ raise TypeError(msg)
242
+ if not isinstance(value, Device):
243
+ msg = f"Expected Device, got {value}"
244
+ raise TypeError(msg)
237
245
  self._children[key] = value
238
246
  value.parent = self
239
247
 
@@ -254,94 +262,49 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
254
262
  return hash(id(self))
255
263
 
256
264
 
257
- class DeviceCollector:
258
- """Collector of top level Device instances to be used as a context manager
259
-
260
- Parameters
261
- ----------
262
- set_name:
263
- If True, call ``device.set_name(variable_name)`` on all collected
264
- Devices
265
- child_name_separator:
266
- Use this as a separator if we call ``set_name``.
267
- connect:
268
- If True, call ``device.connect(mock)`` in parallel on all
269
- collected Devices
270
- mock:
271
- If True, connect Signals in simulation mode
272
- timeout:
273
- How long to wait for connect before logging an exception
274
-
275
- Notes
276
- -----
277
- Example usage::
278
-
279
- [async] with DeviceCollector():
280
- t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
281
- t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
282
- # Names and connects devices here
283
- assert t1x.comm.velocity.source
284
- assert t1x.name == "t1x"
265
+ class DeviceProcessor:
266
+ """Sync/Async Context Manager that finds all the Devices declared within it.
285
267
 
268
+ Used in `init_devices`
286
269
  """
287
270
 
288
- def __init__(
289
- self,
290
- set_name=True,
291
- child_name_separator: str = "-",
292
- connect=True,
293
- mock=False,
294
- timeout: float = 10.0,
295
- ):
296
- self._set_name = set_name
297
- self._child_name_separator = child_name_separator
298
- self._connect = connect
299
- self._mock = mock
300
- self._timeout = timeout
301
- self._names_on_enter: set[str] = set()
302
- self._objects_on_exit: dict[str, Any] = {}
303
-
304
- def _caller_locals(self):
271
+ def __init__(self, process_devices: Callable[[dict[str, Device]], Awaitable[None]]):
272
+ self._process_devices = process_devices
273
+ self._locals_on_enter: dict[str, Any] = {}
274
+ self._locals_on_exit: dict[str, Any] = {}
275
+
276
+ def _caller_locals(self) -> dict[str, Any]:
305
277
  """Walk up until we find a stack frame that doesn't have us as self"""
306
278
  try:
307
279
  raise ValueError
308
280
  except ValueError:
309
281
  _, _, tb = sys.exc_info()
310
- assert tb, "Can't get traceback, this shouldn't happen"
282
+ if not tb:
283
+ msg = "Can't get traceback, this shouldn't happen"
284
+ raise RuntimeError(msg) # noqa: B904
311
285
  caller_frame = tb.tb_frame
312
286
  while caller_frame.f_locals.get("self", None) is self:
313
287
  caller_frame = caller_frame.f_back
314
- assert (
315
- caller_frame
316
- ), "No previous frame to the one with self in it, this shouldn't happen"
317
- return caller_frame.f_locals
288
+ if not caller_frame:
289
+ msg = (
290
+ "No previous frame to the one with self in it, "
291
+ "this shouldn't happen"
292
+ )
293
+ raise RuntimeError( # noqa: B904
294
+ msg
295
+ )
296
+ return caller_frame.f_locals.copy()
318
297
 
319
- def __enter__(self) -> DeviceCollector:
298
+ def __enter__(self) -> DeviceProcessor:
320
299
  # Stash the names that were defined before we were called
321
- self._names_on_enter = set(self._caller_locals())
300
+ self._locals_on_enter = self._caller_locals()
322
301
  return self
323
302
 
324
- async def __aenter__(self) -> DeviceCollector:
303
+ async def __aenter__(self) -> DeviceProcessor:
325
304
  return self.__enter__()
326
305
 
327
- async def _on_exit(self) -> None:
328
- # Name and kick off connect for devices
329
- connect_coroutines: dict[str, Coroutine] = {}
330
- for name, obj in self._objects_on_exit.items():
331
- if name not in self._names_on_enter and isinstance(obj, Device):
332
- if self._set_name and not obj.name:
333
- obj.set_name(name, child_name_separator=self._child_name_separator)
334
- if self._connect:
335
- connect_coroutines[name] = obj.connect(
336
- self._mock, timeout=self._timeout
337
- )
338
-
339
- # Connect to all the devices
340
- if connect_coroutines:
341
- await wait_for_connection(**connect_coroutines)
342
-
343
306
  async def __aexit__(self, type, value, traceback):
344
- self._objects_on_exit = self._caller_locals()
307
+ self._locals_on_exit = self._caller_locals()
345
308
  await self._on_exit()
346
309
 
347
310
  def __exit__(self, type_, value, traceback):
@@ -350,7 +313,7 @@ class DeviceCollector:
350
313
  "Cannot use DeviceConnector inside a plan, instead use "
351
314
  "`yield from ophyd_async.plan_stubs.ensure_connected(device)`"
352
315
  )
353
- self._objects_on_exit = self._caller_locals()
316
+ self._locals_on_exit = self._caller_locals()
354
317
  try:
355
318
  fut = call_in_bluesky_event_loop(self._on_exit())
356
319
  except RuntimeError as e:
@@ -360,3 +323,62 @@ class DeviceCollector:
360
323
  "user/explanations/event-loop-choice.html for more info."
361
324
  ) from e
362
325
  return fut
326
+
327
+ async def _on_exit(self) -> None:
328
+ # Find all the devices
329
+ devices = {
330
+ name: obj
331
+ for name, obj in self._locals_on_exit.items()
332
+ if isinstance(obj, Device) and self._locals_on_enter.get(name) is not obj
333
+ }
334
+ # Call the provided process function on them
335
+ await self._process_devices(devices)
336
+
337
+
338
+ def init_devices(
339
+ set_name=True,
340
+ child_name_separator: str = "-",
341
+ connect=True,
342
+ mock=False,
343
+ timeout: float = 10.0,
344
+ ) -> DeviceProcessor:
345
+ """Auto initialise top level Device instances to be used as a context manager
346
+
347
+ Parameters
348
+ ----------
349
+ set_name:
350
+ If True, call ``device.set_name(variable_name)`` on all Devices
351
+ created within the context manager that have an empty ``name``
352
+ child_name_separator:
353
+ Use this as a separator if we call ``set_name``.
354
+ connect:
355
+ If True, call ``device.connect(mock, timeout)`` in parallel on all
356
+ Devices created within the context manager
357
+ mock:
358
+ If True, connect Signals in mock mode
359
+ timeout:
360
+ How long to wait for connect before logging an exception
361
+
362
+ Notes
363
+ -----
364
+ Example usage::
365
+
366
+ [async] with init_devices():
367
+ t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
368
+ t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
369
+ # Names and connects devices here
370
+ assert t1x.name == "t1x"
371
+ """
372
+
373
+ async def process_devices(devices: dict[str, Device]):
374
+ if set_name:
375
+ for name, device in devices.items():
376
+ if not device.name:
377
+ device.set_name(name, child_name_separator=child_name_separator)
378
+ if connect:
379
+ coros = {
380
+ name: device.connect(mock, timeout) for name, device in devices.items()
381
+ }
382
+ await wait_for_connection(**coros)
383
+
384
+ return DeviceProcessor(process_devices)
@@ -39,6 +39,13 @@ def _logical(name: UniqueName) -> LogicalName:
39
39
  return LogicalName(name.rstrip("_"))
40
40
 
41
41
 
42
+ def _check_device_annotation(annotation: Any) -> DeviceAnnotation:
43
+ if not isinstance(annotation, DeviceAnnotation):
44
+ msg = f"Annotation {annotation} is not a DeviceAnnotation"
45
+ raise TypeError(msg)
46
+ return annotation
47
+
48
+
42
49
  @runtime_checkable
43
50
  class DeviceAnnotation(Protocol):
44
51
  @abstractmethod
@@ -150,8 +157,8 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
150
157
  yield backend, extras
151
158
  signal = child_type(backend)
152
159
  for anno in extras:
153
- assert isinstance(anno, DeviceAnnotation), anno
154
- anno(self._device, signal)
160
+ device_annotation = _check_device_annotation(annotation=anno)
161
+ device_annotation(self._device, signal)
155
162
  setattr(self._device, name, signal)
156
163
  dest = self._filled_backends if filled else self._unfilled_backends
157
164
  dest[_logical(name)] = (backend, child_type)
@@ -167,15 +174,17 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
167
174
  yield connector, extras
168
175
  device = child_type(connector=connector)
169
176
  for anno in extras:
170
- assert isinstance(anno, DeviceAnnotation), anno
171
- anno(self._device, device)
177
+ device_annotation = _check_device_annotation(annotation=anno)
178
+ device_annotation(self._device, device)
172
179
  setattr(self._device, name, device)
173
180
  dest = self._filled_connectors if filled else self._unfilled_connectors
174
181
  dest[_logical(name)] = connector
175
182
 
176
183
  def create_device_vector_entries_to_mock(self, num: int):
177
184
  for name, cls in self._vector_device_type.items():
178
- assert cls, "Shouldn't happen"
185
+ if not cls:
186
+ msg = "Malformed device vector"
187
+ raise TypeError(msg)
179
188
  for i in range(1, num + 1):
180
189
  if issubclass(cls, Signal):
181
190
  self.fill_child_signal(name, cls, i)
@@ -254,9 +263,9 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
254
263
  # We need to add a new entry to a DeviceVector
255
264
  vector = self._ensure_device_vector(name)
256
265
  vector_device_type = self._vector_device_type[name] or device_type
257
- assert issubclass(
258
- vector_device_type, Device
259
- ), f"{vector_device_type} is not a Device"
266
+ if not issubclass(vector_device_type, Device):
267
+ msg = f"{vector_device_type} is not a Device"
268
+ raise TypeError(msg)
260
269
  connector = self._device_connector_factory()
261
270
  vector[vector_index] = vector_device_type(connector=connector)
262
271
  elif child := getattr(self._device, name, None):
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Generic
2
+ from typing import Any, Generic
3
3
 
4
4
  from bluesky.protocols import Flyable, Preparable, Stageable
5
5
 
@@ -10,7 +10,7 @@ from ._utils import T
10
10
 
11
11
  class FlyerController(ABC, Generic[T]):
12
12
  @abstractmethod
13
- async def prepare(self, value: T):
13
+ async def prepare(self, value: T) -> Any:
14
14
  """Move to the start of the flyscan"""
15
15
 
16
16
  @abstractmethod