ophyd-async 0.8.0a6__py3-none-any.whl → 0.9.0__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 (110) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +15 -46
  3. ophyd_async/core/_detector.py +68 -44
  4. ophyd_async/core/_device.py +120 -79
  5. ophyd_async/core/_device_filler.py +17 -8
  6. ophyd_async/core/_flyer.py +2 -2
  7. ophyd_async/core/_protocol.py +0 -28
  8. ophyd_async/core/_readable.py +30 -23
  9. ophyd_async/core/_settings.py +104 -0
  10. ophyd_async/core/_signal.py +91 -151
  11. ophyd_async/core/_signal_backend.py +4 -1
  12. ophyd_async/core/_soft_signal_backend.py +2 -1
  13. ophyd_async/core/_table.py +18 -10
  14. ophyd_async/core/_utils.py +30 -5
  15. ophyd_async/core/_yaml_settings.py +64 -0
  16. ophyd_async/epics/adandor/__init__.py +9 -0
  17. ophyd_async/epics/adandor/_andor.py +45 -0
  18. ophyd_async/epics/adandor/_andor_controller.py +49 -0
  19. ophyd_async/epics/adandor/_andor_io.py +36 -0
  20. ophyd_async/epics/adaravis/__init__.py +3 -1
  21. ophyd_async/epics/adaravis/_aravis.py +23 -37
  22. ophyd_async/epics/adaravis/_aravis_controller.py +21 -30
  23. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  24. ophyd_async/epics/adcore/__init__.py +15 -8
  25. ophyd_async/epics/adcore/_core_detector.py +41 -0
  26. ophyd_async/epics/adcore/_core_io.py +56 -31
  27. ophyd_async/epics/adcore/_core_logic.py +99 -86
  28. ophyd_async/epics/adcore/_core_writer.py +219 -0
  29. ophyd_async/epics/adcore/_hdf_writer.py +33 -59
  30. ophyd_async/epics/adcore/_jpeg_writer.py +26 -0
  31. ophyd_async/epics/adcore/_single_trigger.py +5 -4
  32. ophyd_async/epics/adcore/_tiff_writer.py +26 -0
  33. ophyd_async/epics/adcore/_utils.py +37 -36
  34. ophyd_async/epics/adkinetix/_kinetix.py +29 -24
  35. ophyd_async/epics/adkinetix/_kinetix_controller.py +15 -27
  36. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  37. ophyd_async/epics/adpilatus/__init__.py +2 -2
  38. ophyd_async/epics/adpilatus/_pilatus.py +28 -40
  39. ophyd_async/epics/adpilatus/_pilatus_controller.py +47 -25
  40. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  41. ophyd_async/epics/adsimdetector/__init__.py +3 -3
  42. ophyd_async/epics/adsimdetector/_sim.py +33 -17
  43. ophyd_async/epics/advimba/_vimba.py +23 -23
  44. ophyd_async/epics/advimba/_vimba_controller.py +21 -35
  45. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  46. ophyd_async/epics/core/_aioca.py +52 -21
  47. ophyd_async/epics/core/_p4p.py +59 -16
  48. ophyd_async/epics/core/_pvi_connector.py +4 -2
  49. ophyd_async/epics/core/_signal.py +9 -2
  50. ophyd_async/epics/core/_util.py +10 -1
  51. ophyd_async/epics/eiger/_eiger_controller.py +4 -4
  52. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  53. ophyd_async/epics/motor.py +26 -15
  54. ophyd_async/epics/sim/_ioc.py +29 -0
  55. ophyd_async/epics/{demo → sim}/_mover.py +12 -6
  56. ophyd_async/epics/{demo → sim}/_sensor.py +2 -2
  57. ophyd_async/epics/testing/__init__.py +14 -14
  58. ophyd_async/epics/testing/_example_ioc.py +53 -67
  59. ophyd_async/epics/testing/_utils.py +17 -45
  60. ophyd_async/epics/testing/test_records.db +22 -0
  61. ophyd_async/fastcs/core.py +2 -2
  62. ophyd_async/fastcs/panda/__init__.py +0 -2
  63. ophyd_async/fastcs/panda/_block.py +9 -9
  64. ophyd_async/fastcs/panda/_control.py +9 -4
  65. ophyd_async/fastcs/panda/_hdf_panda.py +7 -2
  66. ophyd_async/fastcs/panda/_table.py +4 -1
  67. ophyd_async/fastcs/panda/_trigger.py +7 -7
  68. ophyd_async/plan_stubs/__init__.py +14 -0
  69. ophyd_async/plan_stubs/_ensure_connected.py +11 -17
  70. ophyd_async/plan_stubs/_fly.py +2 -2
  71. ophyd_async/plan_stubs/_nd_attributes.py +7 -5
  72. ophyd_async/plan_stubs/_panda.py +13 -0
  73. ophyd_async/plan_stubs/_settings.py +125 -0
  74. ophyd_async/plan_stubs/_wait_for_awaitable.py +13 -0
  75. ophyd_async/sim/__init__.py +19 -0
  76. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_controller.py +9 -2
  77. ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_generator.py +13 -6
  78. ophyd_async/sim/{demo/_sim_motor.py → _sim_motor.py} +34 -32
  79. ophyd_async/tango/core/_signal.py +3 -1
  80. ophyd_async/tango/core/_tango_transport.py +13 -15
  81. ophyd_async/tango/{demo → sim}/_mover.py +5 -2
  82. ophyd_async/testing/__init__.py +52 -0
  83. ophyd_async/testing/__pytest_assert_rewrite.py +4 -0
  84. ophyd_async/testing/_assert.py +176 -0
  85. ophyd_async/{core → testing}/_mock_signal_utils.py +15 -11
  86. ophyd_async/testing/_one_of_everything.py +126 -0
  87. ophyd_async/testing/_wait_for_pending.py +22 -0
  88. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/METADATA +4 -2
  89. ophyd_async-0.9.0.dist-info/RECORD +129 -0
  90. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/WHEEL +1 -1
  91. ophyd_async/core/_device_save_loader.py +0 -274
  92. ophyd_async/epics/adsimdetector/_sim_controller.py +0 -51
  93. ophyd_async/fastcs/panda/_utils.py +0 -16
  94. ophyd_async/sim/demo/__init__.py +0 -19
  95. ophyd_async/sim/testing/__init__.py +0 -0
  96. ophyd_async-0.8.0a6.dist-info/RECORD +0 -116
  97. ophyd_async-0.8.0a6.dist-info/entry_points.txt +0 -2
  98. /ophyd_async/epics/{demo → sim}/__init__.py +0 -0
  99. /ophyd_async/epics/{demo → sim}/mover.db +0 -0
  100. /ophyd_async/epics/{demo → sim}/sensor.db +0 -0
  101. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/__init__.py +0 -0
  102. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector.py +0 -0
  103. /ophyd_async/sim/{demo/_pattern_detector → _pattern_detector}/_pattern_detector_writer.py +0 -0
  104. /ophyd_async/tango/{demo → sim}/__init__.py +0 -0
  105. /ophyd_async/tango/{demo → sim}/_counter.py +0 -0
  106. /ophyd_async/tango/{demo → sim}/_detector.py +0 -0
  107. /ophyd_async/tango/{demo → sim}/_tango/__init__.py +0 -0
  108. /ophyd_async/tango/{demo → sim}/_tango/_servers.py +0 -0
  109. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/LICENSE +0 -0
  110. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0.dist-info}/top_level.txt +0 -0
@@ -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
@@ -10,7 +10,6 @@ from typing import Any, TypeVar
10
10
  from bluesky.protocols import HasName
11
11
  from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
12
12
 
13
- from ._protocol import Connectable
14
13
  from ._utils import DEFAULT_TIMEOUT, LazyMock, NotConnected, wait_for_connection
15
14
 
16
15
 
@@ -61,7 +60,7 @@ class DeviceConnector:
61
60
  await wait_for_connection(**coros)
62
61
 
63
62
 
64
- class Device(HasName, Connectable):
63
+ class Device(HasName):
65
64
  """Common base class for all Ophyd Async Devices."""
66
65
 
67
66
  _name: str = ""
@@ -71,13 +70,16 @@ class Device(HasName, Connectable):
71
70
  _connect_task: asyncio.Task | None = None
72
71
  # The mock if we have connected in mock mode
73
72
  _mock: LazyMock | None = None
73
+ # The separator to use when making child names
74
+ _child_name_separator: str = "-"
74
75
 
75
76
  def __init__(
76
77
  self, name: str = "", connector: DeviceConnector | None = None
77
78
  ) -> None:
78
79
  self._connector = connector or DeviceConnector()
79
80
  self._connector.create_children_from_annotations(self)
80
- self.set_name(name)
81
+ if name:
82
+ self.set_name(name)
81
83
 
82
84
  @property
83
85
  def name(self) -> str:
@@ -97,21 +99,30 @@ class Device(HasName, Connectable):
97
99
  getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
98
100
  )
99
101
 
100
- def set_name(self, name: str):
102
+ def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
101
103
  """Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
102
104
 
103
105
  Parameters
104
106
  ----------
105
107
  name:
106
108
  New name to set
109
+ child_name_separator:
110
+ Use this as a separator instead of "-". Use "_" instead to make the same
111
+ names as the equivalent ophyd sync device.
107
112
  """
108
113
  self._name = name
114
+ if child_name_separator:
115
+ self._child_name_separator = child_name_separator
109
116
  # Ensure logger is recreated after a name change
110
117
  if "log" in self.__dict__:
111
118
  del self.log
112
- for child_name, child in self.children():
113
- child_name = f"{self.name}-{child_name.strip('_')}" if self.name else ""
114
- child.set_name(child_name)
119
+ for attr_name, child in self.children():
120
+ child_name = (
121
+ f"{self.name}{self._child_name_separator}{attr_name}"
122
+ if self.name
123
+ else ""
124
+ )
125
+ child.set_name(child_name, child_name_separator=self._child_name_separator)
115
126
 
116
127
  def __setattr__(self, name: str, value: Any) -> None:
117
128
  # Bear in mind that this function is called *a lot*, so
@@ -147,6 +158,12 @@ class Device(HasName, Connectable):
147
158
  timeout:
148
159
  Time to wait before failing with a TimeoutError.
149
160
  """
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)
150
167
  if mock:
151
168
  # Always connect in mock mode serially
152
169
  if isinstance(mock, LazyMock):
@@ -167,7 +184,9 @@ class Device(HasName, Connectable):
167
184
  self._mock = None
168
185
  coro = self._connector.connect_real(self, timeout, force_reconnect)
169
186
  self._connect_task = asyncio.create_task(coro)
170
- 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)
171
190
  # Wait for it to complete
172
191
  await self._connect_task
173
192
 
@@ -191,7 +210,7 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
191
210
 
192
211
  In the below example, foos becomes a dictionary on the parent device
193
212
  at runtime, so parent.foos[2] returns a FooDevice. For example usage see
194
- :class:`~ophyd_async.epics.demo.DynamicSensorGroup`
213
+ :class:`~ophyd_async.epics.sim.DynamicSensorGroup`
195
214
  """
196
215
 
197
216
  def __init__(
@@ -217,8 +236,12 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
217
236
  def __setitem__(self, key: int, value: DeviceT) -> None:
218
237
  # Check the types on entry to dict to make sure we can't accidentally
219
238
  # make a non-integer named child
220
- assert isinstance(key, int), f"Expected int, got {key}"
221
- 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)
222
245
  self._children[key] = value
223
246
  value.parent = self
224
247
 
@@ -239,90 +262,49 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
239
262
  return hash(id(self))
240
263
 
241
264
 
242
- class DeviceCollector:
243
- """Collector of top level Device instances to be used as a context manager
244
-
245
- Parameters
246
- ----------
247
- set_name:
248
- If True, call ``device.set_name(variable_name)`` on all collected
249
- Devices
250
- connect:
251
- If True, call ``device.connect(mock)`` in parallel on all
252
- collected Devices
253
- mock:
254
- If True, connect Signals in simulation mode
255
- timeout:
256
- How long to wait for connect before logging an exception
257
-
258
- Notes
259
- -----
260
- Example usage::
261
-
262
- [async] with DeviceCollector():
263
- t1x = motor.Motor("BLxxI-MO-TABLE-01:X")
264
- t1y = motor.Motor("pva://BLxxI-MO-TABLE-01:Y")
265
- # Names and connects devices here
266
- assert t1x.comm.velocity.source
267
- assert t1x.name == "t1x"
265
+ class DeviceProcessor:
266
+ """Sync/Async Context Manager that finds all the Devices declared within it.
268
267
 
268
+ Used in `init_devices`
269
269
  """
270
270
 
271
- def __init__(
272
- self,
273
- set_name=True,
274
- connect=True,
275
- mock=False,
276
- timeout: float = 10.0,
277
- ):
278
- self._set_name = set_name
279
- self._connect = connect
280
- self._mock = mock
281
- self._timeout = timeout
282
- self._names_on_enter: set[str] = set()
283
- self._objects_on_exit: dict[str, Any] = {}
284
-
285
- 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]:
286
277
  """Walk up until we find a stack frame that doesn't have us as self"""
287
278
  try:
288
279
  raise ValueError
289
280
  except ValueError:
290
281
  _, _, tb = sys.exc_info()
291
- 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
292
285
  caller_frame = tb.tb_frame
293
286
  while caller_frame.f_locals.get("self", None) is self:
294
287
  caller_frame = caller_frame.f_back
295
- assert (
296
- caller_frame
297
- ), "No previous frame to the one with self in it, this shouldn't happen"
298
- 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()
299
297
 
300
- def __enter__(self) -> DeviceCollector:
298
+ def __enter__(self) -> DeviceProcessor:
301
299
  # Stash the names that were defined before we were called
302
- self._names_on_enter = set(self._caller_locals())
300
+ self._locals_on_enter = self._caller_locals()
303
301
  return self
304
302
 
305
- async def __aenter__(self) -> DeviceCollector:
303
+ async def __aenter__(self) -> DeviceProcessor:
306
304
  return self.__enter__()
307
305
 
308
- async def _on_exit(self) -> None:
309
- # Name and kick off connect for devices
310
- connect_coroutines: dict[str, Coroutine] = {}
311
- for name, obj in self._objects_on_exit.items():
312
- if name not in self._names_on_enter and isinstance(obj, Device):
313
- if self._set_name and not obj.name:
314
- obj.set_name(name)
315
- if self._connect:
316
- connect_coroutines[name] = obj.connect(
317
- self._mock, timeout=self._timeout
318
- )
319
-
320
- # Connect to all the devices
321
- if connect_coroutines:
322
- await wait_for_connection(**connect_coroutines)
323
-
324
306
  async def __aexit__(self, type, value, traceback):
325
- self._objects_on_exit = self._caller_locals()
307
+ self._locals_on_exit = self._caller_locals()
326
308
  await self._on_exit()
327
309
 
328
310
  def __exit__(self, type_, value, traceback):
@@ -331,7 +313,7 @@ class DeviceCollector:
331
313
  "Cannot use DeviceConnector inside a plan, instead use "
332
314
  "`yield from ophyd_async.plan_stubs.ensure_connected(device)`"
333
315
  )
334
- self._objects_on_exit = self._caller_locals()
316
+ self._locals_on_exit = self._caller_locals()
335
317
  try:
336
318
  fut = call_in_bluesky_event_loop(self._on_exit())
337
319
  except RuntimeError as e:
@@ -341,3 +323,62 @@ class DeviceCollector:
341
323
  "user/explanations/event-loop-choice.html for more info."
342
324
  ) from e
343
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
@@ -13,38 +13,10 @@ from typing import (
13
13
  from bluesky.protocols import HasName, Reading
14
14
  from event_model import DataKey
15
15
 
16
- from ._utils import DEFAULT_TIMEOUT
17
-
18
16
  if TYPE_CHECKING:
19
- from unittest.mock import Mock
20
-
21
17
  from ._status import AsyncStatus
22
18
 
23
19
 
24
- @runtime_checkable
25
- class Connectable(Protocol):
26
- @abstractmethod
27
- async def connect(
28
- self,
29
- mock: bool | Mock = False,
30
- timeout: float = DEFAULT_TIMEOUT,
31
- force_reconnect: bool = False,
32
- ):
33
- """Connect self and all child Devices.
34
-
35
- Contains a timeout that gets propagated to child.connect methods.
36
-
37
- Parameters
38
- ----------
39
- mock:
40
- If True then use ``MockSignalBackend`` for all Signals
41
- timeout:
42
- Time to wait before failing with a TimeoutError.
43
- force_reconnect:
44
- Reconnect even if previous connect was successful.
45
- """
46
-
47
-
48
20
  @runtime_checkable
49
21
  class AsyncReadable(HasName, Protocol):
50
22
  @abstractmethod
@@ -123,29 +123,31 @@ class StandardReadable(
123
123
  # we want to combine them when they are Sequences, and ensure they are
124
124
  # identical when string values.
125
125
  for key, value in new_hint.hints.items():
126
+ # fail early for unkwon types
126
127
  if isinstance(value, str):
127
128
  if key in hints:
128
- assert (
129
- hints[key] == value # type: ignore[literal-required]
130
- ), f"Hints key {key} value may not be overridden"
129
+ if hints[key] != value:
130
+ msg = f"Hints key {key} value may not be overridden"
131
+ raise RuntimeError(msg)
131
132
  else:
132
133
  hints[key] = value # type: ignore[literal-required]
133
134
  elif isinstance(value, Sequence):
134
135
  if key in hints:
135
136
  for new_val in value:
136
- assert (
137
- new_val not in hints[key] # type: ignore[literal-required]
138
- ), f"Hint {key} {new_val} overrides existing hint"
137
+ if new_val in hints[key]:
138
+ msg = f"Hint {key} {new_val} overrides existing hint"
139
+ raise RuntimeError(msg)
139
140
  hints[key] = ( # type: ignore[literal-required]
140
141
  hints[key] + value # type: ignore[literal-required]
141
142
  )
142
143
  else:
143
144
  hints[key] = value # type: ignore[literal-required]
144
145
  else:
145
- raise TypeError(
146
- f"{new_hint.name}: Unknown type for value '{value}' "
146
+ msg = (
147
+ f"{new_hint.name}: Unknown type for value '{value}'"
147
148
  f" for key '{key}'"
148
149
  )
150
+ raise TypeError(msg)
149
151
 
150
152
  return hints
151
153
 
@@ -204,6 +206,11 @@ class StandardReadable(
204
206
  `StandardReadableFormat` documentation
205
207
  """
206
208
 
209
+ def assert_device_is_signalr(device: Device) -> SignalR:
210
+ if not isinstance(device, SignalR):
211
+ raise TypeError(f"{device} is not a SignalR")
212
+ return device
213
+
207
214
  for device in devices:
208
215
  match format:
209
216
  case StandardReadableFormat.CHILD:
@@ -218,24 +225,24 @@ class StandardReadable(
218
225
  if isinstance(device, HasHints):
219
226
  self._has_hints += (device,)
220
227
  case StandardReadableFormat.CONFIG_SIGNAL:
221
- assert isinstance(device, SignalR), f"{device} is not a SignalR"
222
- self._describe_config_funcs += (device.describe,)
223
- self._read_config_funcs += (device.read,)
228
+ signalr_device = assert_device_is_signalr(device=device)
229
+ self._describe_config_funcs += (signalr_device.describe,)
230
+ self._read_config_funcs += (signalr_device.read,)
224
231
  case StandardReadableFormat.HINTED_SIGNAL:
225
- assert isinstance(device, SignalR), f"{device} is not a SignalR"
226
- self._describe_funcs += (device.describe,)
227
- self._read_funcs += (device.read,)
228
- self._stageables += (device,)
229
- self._has_hints += (_HintsFromName(device),)
232
+ signalr_device = assert_device_is_signalr(device=device)
233
+ self._describe_funcs += (signalr_device.describe,)
234
+ self._read_funcs += (signalr_device.read,)
235
+ self._stageables += (signalr_device,)
236
+ self._has_hints += (_HintsFromName(signalr_device),)
230
237
  case StandardReadableFormat.UNCACHED_SIGNAL:
231
- assert isinstance(device, SignalR), f"{device} is not a SignalR"
232
- self._describe_funcs += (device.describe,)
233
- self._read_funcs += (_UncachedRead(device),)
238
+ signalr_device = assert_device_is_signalr(device=device)
239
+ self._describe_funcs += (signalr_device.describe,)
240
+ self._read_funcs += (_UncachedRead(signalr_device),)
234
241
  case StandardReadableFormat.HINTED_UNCACHED_SIGNAL:
235
- assert isinstance(device, SignalR), f"{device} is not a SignalR"
236
- self._describe_funcs += (device.describe,)
237
- self._read_funcs += (_UncachedRead(device),)
238
- self._has_hints += (_HintsFromName(device),)
242
+ signalr_device = assert_device_is_signalr(device=device)
243
+ self._describe_funcs += (signalr_device.describe,)
244
+ self._read_funcs += (_UncachedRead(signalr_device),)
245
+ self._has_hints += (_HintsFromName(signalr_device),)
239
246
 
240
247
 
241
248
  class _UncachedRead:
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import abstractmethod
4
+ from collections.abc import Callable, Iterator, MutableMapping
5
+ from typing import Any, Generic
6
+
7
+ from ._device import Device, DeviceT
8
+ from ._signal import SignalRW
9
+ from ._signal_backend import SignalDatatypeT
10
+
11
+
12
+ class Settings(MutableMapping[SignalRW[Any], Any], Generic[DeviceT]):
13
+ def __init__(
14
+ self, device: DeviceT, settings: MutableMapping[SignalRW, Any] | None = None
15
+ ):
16
+ self.device = device
17
+ self._settings = {}
18
+ self.update(settings or {})
19
+
20
+ def __getitem__(self, key: SignalRW[SignalDatatypeT]) -> SignalDatatypeT:
21
+ return self._settings[key]
22
+
23
+ def _is_in_device(self, device: Device) -> bool:
24
+ while device.parent and device.parent is not self.device:
25
+ # While we have a parent that is not the right device
26
+ # continue searching up the tree
27
+ device = device.parent
28
+ return device.parent is self.device
29
+
30
+ def __setitem__(
31
+ self, key: SignalRW[SignalDatatypeT], value: SignalDatatypeT | None
32
+ ) -> None:
33
+ # Check the types on entry to dict to make sure we can't accidentally
34
+ # add a non-signal type
35
+ if not isinstance(key, SignalRW):
36
+ raise TypeError(f"Expected SignalRW, got {key}")
37
+ if not self._is_in_device(key):
38
+ raise KeyError(f"Signal {key} is not a child of {self.device}")
39
+ self._settings[key] = value
40
+
41
+ def __delitem__(self, key: SignalRW) -> None:
42
+ del self._settings[key]
43
+
44
+ def __iter__(self) -> Iterator[SignalRW]:
45
+ yield from iter(self._settings)
46
+
47
+ def __len__(self) -> int:
48
+ return len(self._settings)
49
+
50
+ def __or__(self, other: MutableMapping[SignalRW, Any]) -> Settings[DeviceT]:
51
+ """Create a new Settings that is the union of self overridden by other.
52
+
53
+ For example::
54
+
55
+ settings1 = Settings(device, {device.sig1: 1, device.sig2: 2})
56
+ settings2 = Settings(device, {device.sig1: 10, device.sig3: 3})
57
+ settings = settings1 | settings2
58
+ assert dict(settings) == {
59
+ device.sig1: 10,
60
+ device.sig2: 2,
61
+ device.sig3: 3,
62
+ }
63
+ """
64
+ if isinstance(other, Settings) and not self._is_in_device(other.device):
65
+ raise ValueError(f"{other.device} is not a child of {self.device}")
66
+ return Settings(self.device, self._settings | dict(other))
67
+
68
+ def partition(
69
+ self, predicate: Callable[[SignalRW], bool]
70
+ ) -> tuple[Settings[DeviceT], Settings[DeviceT]]:
71
+ """Partition into two Settings based on a predicate.
72
+
73
+ Parameters
74
+ ----------
75
+ predicate
76
+ Callable that takes each signal, and returns a boolean to say if it
77
+ should be in the first returned Settings
78
+
79
+ Returns
80
+ -------
81
+ (where_true, where_false)
82
+
83
+ For example::
84
+
85
+ settings = Settings(device, {device.special: 1, device.sig: 2})
86
+ specials, others = settings.partition(lambda sig: "special" in sig.name)
87
+ """
88
+ where_true, where_false = Settings(self.device), Settings(self.device)
89
+ for signal, value in self.items():
90
+ dest = where_true if predicate(signal) else where_false
91
+ dest[signal] = value
92
+ return where_true, where_false
93
+
94
+
95
+ class SettingsProvider:
96
+ @abstractmethod
97
+ async def store(self, name: str, data: dict[str, Any]):
98
+ """Store the data, associating it with the given name."""
99
+ ...
100
+
101
+ @abstractmethod
102
+ async def retrieve(self, name: str) -> dict[str, Any]:
103
+ """Retrieve the data associated with the given name."""
104
+ ...