ophyd-async 0.8.0a6__py3-none-any.whl → 0.9.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +2 -26
  3. ophyd_async/core/_detector.py +9 -9
  4. ophyd_async/core/_device.py +27 -8
  5. ophyd_async/core/_protocol.py +0 -28
  6. ophyd_async/core/_signal.py +38 -136
  7. ophyd_async/core/_utils.py +11 -2
  8. ophyd_async/epics/adaravis/_aravis_controller.py +8 -8
  9. ophyd_async/epics/adaravis/_aravis_io.py +4 -4
  10. ophyd_async/epics/adcore/_core_io.py +21 -21
  11. ophyd_async/epics/adcore/_core_logic.py +3 -2
  12. ophyd_async/epics/adcore/_hdf_writer.py +6 -3
  13. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  14. ophyd_async/epics/adcore/_utils.py +35 -35
  15. ophyd_async/epics/adkinetix/_kinetix_controller.py +7 -7
  16. ophyd_async/epics/adkinetix/_kinetix_io.py +7 -7
  17. ophyd_async/epics/adpilatus/_pilatus.py +3 -3
  18. ophyd_async/epics/adpilatus/_pilatus_controller.py +4 -4
  19. ophyd_async/epics/adpilatus/_pilatus_io.py +5 -5
  20. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  21. ophyd_async/epics/advimba/_vimba_controller.py +14 -14
  22. ophyd_async/epics/advimba/_vimba_io.py +23 -23
  23. ophyd_async/epics/core/_p4p.py +19 -0
  24. ophyd_async/epics/core/_pvi_connector.py +4 -2
  25. ophyd_async/epics/core/_signal.py +9 -2
  26. ophyd_async/epics/core/_util.py +9 -0
  27. ophyd_async/epics/demo/_mover.py +2 -2
  28. ophyd_async/epics/demo/_sensor.py +2 -2
  29. ophyd_async/epics/eiger/_eiger_controller.py +4 -4
  30. ophyd_async/epics/eiger/_eiger_io.py +3 -3
  31. ophyd_async/epics/motor.py +8 -5
  32. ophyd_async/epics/testing/_example_ioc.py +5 -3
  33. ophyd_async/epics/testing/test_records.db +6 -0
  34. ophyd_async/fastcs/core.py +2 -2
  35. ophyd_async/fastcs/panda/_block.py +9 -9
  36. ophyd_async/fastcs/panda/_control.py +2 -2
  37. ophyd_async/fastcs/panda/_hdf_panda.py +4 -1
  38. ophyd_async/fastcs/panda/_trigger.py +7 -7
  39. ophyd_async/plan_stubs/_fly.py +1 -1
  40. ophyd_async/sim/demo/_sim_motor.py +34 -32
  41. ophyd_async/tango/core/_tango_transport.py +1 -1
  42. ophyd_async/testing/__init__.py +33 -0
  43. ophyd_async/testing/_assert.py +128 -0
  44. ophyd_async/{core → testing}/_mock_signal_utils.py +12 -8
  45. ophyd_async/testing/_wait_for_pending.py +22 -0
  46. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/METADATA +3 -1
  47. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/RECORD +51 -48
  48. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/LICENSE +0 -0
  49. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/WHEEL +0 -0
  50. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.dist-info}/entry_points.txt +0 -0
  51. {ophyd_async-0.8.0a6.dist-info → ophyd_async-0.9.0a1.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.8.0a6'
16
- __version_tuple__ = version_tuple = (0, 8, 0)
15
+ __version__ = version = '0.9.0a1'
16
+ __version_tuple__ = version_tuple = (0, 9, 0)
@@ -21,16 +21,6 @@ from ._flyer import FlyerController, StandardFlyer
21
21
  from ._hdf_dataset import HDFDataset, HDFFile
22
22
  from ._log import config_ophyd_async_logging
23
23
  from ._mock_signal_backend import MockSignalBackend
24
- from ._mock_signal_utils import (
25
- callback_on_mock_put,
26
- get_mock,
27
- get_mock_put,
28
- mock_puts_blocked,
29
- reset_mock_put_calls,
30
- set_mock_put_proceeds,
31
- set_mock_value,
32
- set_mock_values,
33
- )
34
24
  from ._protocol import AsyncConfigurable, AsyncReadable, AsyncStageable
35
25
  from ._providers import (
36
26
  AutoIncrementFilenameProvider,
@@ -53,14 +43,11 @@ from ._readable import (
53
43
  )
54
44
  from ._signal import (
55
45
  Signal,
46
+ SignalConnector,
56
47
  SignalR,
57
48
  SignalRW,
58
49
  SignalW,
59
50
  SignalX,
60
- assert_configuration,
61
- assert_emitted,
62
- assert_reading,
63
- assert_value,
64
51
  observe_signals_value,
65
52
  observe_value,
66
53
  set_and_wait_for_other_value,
@@ -123,14 +110,6 @@ __all__ = [
123
110
  "HDFFile",
124
111
  "config_ophyd_async_logging",
125
112
  "MockSignalBackend",
126
- "callback_on_mock_put",
127
- "get_mock",
128
- "get_mock_put",
129
- "mock_puts_blocked",
130
- "reset_mock_put_calls",
131
- "set_mock_put_proceeds",
132
- "set_mock_value",
133
- "set_mock_values",
134
113
  "AsyncConfigurable",
135
114
  "AsyncReadable",
136
115
  "AsyncStageable",
@@ -150,14 +129,11 @@ __all__ = [
150
129
  "StandardReadable",
151
130
  "StandardReadableFormat",
152
131
  "Signal",
132
+ "SignalConnector",
153
133
  "SignalR",
154
134
  "SignalRW",
155
135
  "SignalW",
156
136
  "SignalX",
157
- "assert_configuration",
158
- "assert_emitted",
159
- "assert_reading",
160
- "assert_value",
161
137
  "observe_value",
162
138
  "observe_signals_value",
163
139
  "set_and_wait_for_value",
@@ -30,13 +30,13 @@ class DetectorTrigger(StrictEnum):
30
30
  """Type of mechanism for triggering a detector to take frames"""
31
31
 
32
32
  #: Detector generates internal trigger for given rate
33
- internal = "internal"
33
+ INTERNAL = "internal"
34
34
  #: Expect a series of arbitrary length trigger signals
35
- edge_trigger = "edge_trigger"
35
+ EDGE_TRIGGER = "edge_trigger"
36
36
  #: Expect a series of constant width external gate signals
37
- constant_gate = "constant_gate"
37
+ CONSTANT_GATE = "constant_gate"
38
38
  #: Expect a series of variable width external gate signals
39
- variable_gate = "variable_gate"
39
+ VARIABLE_GATE = "variable_gate"
40
40
 
41
41
 
42
42
  class TriggerInfo(BaseModel):
@@ -53,7 +53,7 @@ class TriggerInfo(BaseModel):
53
53
  #: - 3 times for final flat field images
54
54
  number_of_triggers: NonNegativeInt | list[NonNegativeInt]
55
55
  #: Sort of triggers that will be sent
56
- trigger: DetectorTrigger = Field(default=DetectorTrigger.internal)
56
+ trigger: DetectorTrigger = Field(default=DetectorTrigger.INTERNAL)
57
57
  #: What is the minimum deadtime between triggers
58
58
  deadtime: float | None = Field(default=None, ge=0)
59
59
  #: What is the maximum high time of the triggers
@@ -265,14 +265,14 @@ class StandardDetector(
265
265
  await self.prepare(
266
266
  TriggerInfo(
267
267
  number_of_triggers=1,
268
- trigger=DetectorTrigger.internal,
268
+ trigger=DetectorTrigger.INTERNAL,
269
269
  deadtime=None,
270
270
  livetime=None,
271
271
  frame_timeout=None,
272
272
  )
273
273
  )
274
274
  assert self._trigger_info
275
- assert self._trigger_info.trigger is DetectorTrigger.internal
275
+ assert self._trigger_info.trigger is DetectorTrigger.INTERNAL
276
276
  # Arm the detector and wait for it to finish.
277
277
  indices_written = await self.writer.get_indices_written()
278
278
  await self.controller.arm()
@@ -303,7 +303,7 @@ class StandardDetector(
303
303
  Args:
304
304
  value: TriggerInfo describing how to trigger the detector
305
305
  """
306
- if value.trigger != DetectorTrigger.internal:
306
+ if value.trigger != DetectorTrigger.INTERNAL:
307
307
  assert (
308
308
  value.deadtime
309
309
  ), "Deadtime must be supplied when in externally triggered mode"
@@ -323,7 +323,7 @@ class StandardDetector(
323
323
  self._describe, _ = await asyncio.gather(
324
324
  self.writer.open(value.multiplier), self.controller.prepare(value)
325
325
  )
326
- if value.trigger != DetectorTrigger.internal:
326
+ if value.trigger != DetectorTrigger.INTERNAL:
327
327
  await self.controller.arm()
328
328
  self._fly_start = time.monotonic()
329
329
 
@@ -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,10 @@ class Device(HasName, Connectable):
147
158
  timeout:
148
159
  Time to wait before failing with a TimeoutError.
149
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
+ )
150
165
  if mock:
151
166
  # Always connect in mock mode serially
152
167
  if isinstance(mock, LazyMock):
@@ -247,6 +262,8 @@ class DeviceCollector:
247
262
  set_name:
248
263
  If True, call ``device.set_name(variable_name)`` on all collected
249
264
  Devices
265
+ child_name_separator:
266
+ Use this as a separator if we call ``set_name``.
250
267
  connect:
251
268
  If True, call ``device.connect(mock)`` in parallel on all
252
269
  collected Devices
@@ -271,11 +288,13 @@ class DeviceCollector:
271
288
  def __init__(
272
289
  self,
273
290
  set_name=True,
291
+ child_name_separator: str = "-",
274
292
  connect=True,
275
293
  mock=False,
276
294
  timeout: float = 10.0,
277
295
  ):
278
296
  self._set_name = set_name
297
+ self._child_name_separator = child_name_separator
279
298
  self._connect = connect
280
299
  self._mock = mock
281
300
  self._timeout = timeout
@@ -311,7 +330,7 @@ class DeviceCollector:
311
330
  for name, obj in self._objects_on_exit.items():
312
331
  if name not in self._names_on_enter and isinstance(obj, Device):
313
332
  if self._set_name and not obj.name:
314
- obj.set_name(name)
333
+ obj.set_name(name, child_name_separator=self._child_name_separator)
315
334
  if self._connect:
316
335
  connect_coroutines[name] = obj.connect(
317
336
  self._mock, timeout=self._timeout
@@ -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
@@ -2,8 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import functools
5
- from collections.abc import AsyncGenerator, Awaitable, Callable, Mapping
6
- from typing import Any, Generic, cast
5
+ import time
6
+ from collections.abc import AsyncGenerator, Awaitable, Callable
7
+ from typing import Generic, cast
7
8
 
8
9
  from bluesky.protocols import (
9
10
  Locatable,
@@ -17,7 +18,6 @@ from event_model import DataKey
17
18
  from ._device import Device, DeviceConnector
18
19
  from ._mock_signal_backend import MockSignalBackend
19
20
  from ._protocol import (
20
- AsyncConfigurable,
21
21
  AsyncReadable,
22
22
  AsyncStageable,
23
23
  Reading,
@@ -122,7 +122,7 @@ class _SignalCache(Generic[SignalDatatypeT]):
122
122
 
123
123
  def _callback(self, reading: Reading[SignalDatatypeT]):
124
124
  self._signal.log.debug(
125
- f"Updated subscription: reading of source {self._signal.source} changed"
125
+ f"Updated subscription: reading of source {self._signal.source} changed "
126
126
  f"from {self._reading} to {reading}"
127
127
  )
128
128
  self._reading = reading
@@ -301,130 +301,11 @@ def soft_signal_r_and_setter(
301
301
  return (signal, backend.set_value)
302
302
 
303
303
 
304
- def _generate_assert_error_msg(name: str, expected_result, actual_result) -> str:
305
- WARNING = "\033[93m"
306
- FAIL = "\033[91m"
307
- ENDC = "\033[0m"
308
- return (
309
- f"Expected {WARNING}{name}{ENDC} to produce"
310
- + f"\n{FAIL}{expected_result}{ENDC}"
311
- + f"\nbut actually got \n{FAIL}{actual_result}{ENDC}"
312
- )
313
-
314
-
315
- async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
316
- """Assert a signal's value and compare it an expected signal.
317
-
318
- Parameters
319
- ----------
320
- signal:
321
- signal with get_value.
322
- value:
323
- The expected value from the signal.
324
-
325
- Notes
326
- -----
327
- Example usage::
328
- await assert_value(signal, value)
329
-
330
- """
331
- actual_value = await signal.get_value()
332
- assert actual_value == value, _generate_assert_error_msg(
333
- name=signal.name,
334
- expected_result=value,
335
- actual_result=actual_value,
336
- )
337
-
338
-
339
- async def assert_reading(
340
- readable: AsyncReadable, expected_reading: Mapping[str, Reading]
341
- ) -> None:
342
- """Assert readings from readable.
343
-
344
- Parameters
345
- ----------
346
- readable:
347
- Callable with readable.read function that generate readings.
348
-
349
- reading:
350
- The expected readings from the readable.
351
-
352
- Notes
353
- -----
354
- Example usage::
355
- await assert_reading(readable, reading)
356
-
357
- """
358
- actual_reading = await readable.read()
359
- assert expected_reading == actual_reading, _generate_assert_error_msg(
360
- name=readable.name,
361
- expected_result=expected_reading,
362
- actual_result=actual_reading,
363
- )
364
-
365
-
366
- async def assert_configuration(
367
- configurable: AsyncConfigurable,
368
- configuration: Mapping[str, Reading],
369
- ) -> None:
370
- """Assert readings from Configurable.
371
-
372
- Parameters
373
- ----------
374
- configurable:
375
- Configurable with Configurable.read function that generate readings.
376
-
377
- configuration:
378
- The expected readings from configurable.
379
-
380
- Notes
381
- -----
382
- Example usage::
383
- await assert_configuration(configurable configuration)
384
-
385
- """
386
- actual_configurable = await configurable.read_configuration()
387
- assert configuration == actual_configurable, _generate_assert_error_msg(
388
- name=configurable.name,
389
- expected_result=configuration,
390
- actual_result=actual_configurable,
391
- )
392
-
393
-
394
- def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
395
- """Assert emitted document generated by running a Bluesky plan
396
-
397
- Parameters
398
- ----------
399
- Doc:
400
- A dictionary
401
-
402
- numbers:
403
- expected emission in kwarg from
404
-
405
- Notes
406
- -----
407
- Example usage::
408
- assert_emitted(docs, start=1, descriptor=1,
409
- resource=1, datum=1, event=1, stop=1)
410
- """
411
- assert list(docs) == list(numbers), _generate_assert_error_msg(
412
- name="documents",
413
- expected_result=list(numbers),
414
- actual_result=list(docs),
415
- )
416
- actual_numbers = {name: len(d) for name, d in docs.items()}
417
- assert actual_numbers == numbers, _generate_assert_error_msg(
418
- name="emitted",
419
- expected_result=numbers,
420
- actual_result=actual_numbers,
421
- )
422
-
423
-
424
304
  async def observe_value(
425
305
  signal: SignalR[SignalDatatypeT],
426
306
  timeout: float | None = None,
427
307
  done_status: Status | None = None,
308
+ done_timeout: float | None = None,
428
309
  ) -> AsyncGenerator[SignalDatatypeT, None]:
429
310
  """Subscribe to the value of a signal so it can be iterated from.
430
311
 
@@ -439,9 +320,17 @@ async def observe_value(
439
320
  done_status:
440
321
  If this status is complete, stop observing and make the iterator return.
441
322
  If it raises an exception then this exception will be raised by the iterator.
323
+ done_timeout:
324
+ If given, the maximum time to watch a signal, in seconds. If the loop is still
325
+ being watched after this length, raise asyncio.TimeoutError. This should be used
326
+ instead of on an 'asyncio.wait_for' timeout
442
327
 
443
328
  Notes
444
329
  -----
330
+ Due to a rare condition with busy signals, it is not recommended to use this
331
+ function with asyncio.timeout, including in an 'asyncio.wait_for' loop. Instead,
332
+ this timeout should be given to the done_timeout parameter.
333
+
445
334
  Example usage::
446
335
 
447
336
  async for value in observe_value(sig):
@@ -449,15 +338,26 @@ async def observe_value(
449
338
  """
450
339
 
451
340
  async for _, value in observe_signals_value(
452
- signal, timeout=timeout, done_status=done_status
341
+ signal,
342
+ timeout=timeout,
343
+ done_status=done_status,
344
+ done_timeout=done_timeout,
453
345
  ):
454
346
  yield value
455
347
 
456
348
 
349
+ def _get_iteration_timeout(
350
+ timeout: float | None, overall_deadline: float | None
351
+ ) -> float | None:
352
+ overall_deadline = overall_deadline - time.monotonic() if overall_deadline else None
353
+ return min([x for x in [overall_deadline, timeout] if x is not None], default=None)
354
+
355
+
457
356
  async def observe_signals_value(
458
357
  *signals: SignalR[SignalDatatypeT],
459
358
  timeout: float | None = None,
460
359
  done_status: Status | None = None,
360
+ done_timeout: float | None = None,
461
361
  ) -> AsyncGenerator[tuple[SignalR[SignalDatatypeT], SignalDatatypeT], None]:
462
362
  """Subscribe to the value of a signal so it can be iterated from.
463
363
 
@@ -472,6 +372,10 @@ async def observe_signals_value(
472
372
  done_status:
473
373
  If this status is complete, stop observing and make the iterator return.
474
374
  If it raises an exception then this exception will be raised by the iterator.
375
+ done_timeout:
376
+ If given, the maximum time to watch a signal, in seconds. If the loop is still
377
+ being watched after this length, raise asyncio.TimeoutError. This should be used
378
+ instead of on an 'asyncio.wait_for' timeout
475
379
 
476
380
  Notes
477
381
  -----
@@ -486,12 +390,6 @@ async def observe_signals_value(
486
390
  q: asyncio.Queue[tuple[SignalR[SignalDatatypeT], SignalDatatypeT] | Status] = (
487
391
  asyncio.Queue()
488
392
  )
489
- if timeout is None:
490
- get_value = q.get
491
- else:
492
-
493
- async def get_value():
494
- return await asyncio.wait_for(q.get(), timeout)
495
393
 
496
394
  cbs: dict[SignalR, Callback] = {}
497
395
  for signal in signals:
@@ -504,13 +402,17 @@ async def observe_signals_value(
504
402
 
505
403
  if done_status is not None:
506
404
  done_status.add_callback(q.put_nowait)
507
-
405
+ overall_deadline = time.monotonic() + done_timeout if done_timeout else None
508
406
  try:
509
407
  while True:
510
- # yield here in case something else is filling the queue
511
- # like in test_observe_value_times_out_with_no_external_task()
512
- await asyncio.sleep(0)
513
- item = await get_value()
408
+ if overall_deadline and time.monotonic() >= overall_deadline:
409
+ raise asyncio.TimeoutError(
410
+ f"observe_value was still observing signals "
411
+ f"{[signal.source for signal in signals]} after "
412
+ f"timeout {done_timeout}s"
413
+ )
414
+ iteration_timeout = _get_iteration_timeout(timeout, overall_deadline)
415
+ item = await asyncio.wait_for(q.get(), iteration_timeout)
514
416
  if done_status and item is done_status:
515
417
  if exc := done_status.exception():
516
418
  raise exc
@@ -17,11 +17,20 @@ DEFAULT_TIMEOUT = 10.0
17
17
  ErrorText = str | Mapping[str, Exception]
18
18
 
19
19
 
20
- class StrictEnum(str, Enum):
20
+ class StrictEnumMeta(EnumMeta):
21
+ def __new__(metacls, *args, **kwargs):
22
+ ret = super().__new__(metacls, *args, **kwargs)
23
+ lowercase_names = [x.name for x in ret if not x.name.isupper()] # type: ignore
24
+ if lowercase_names:
25
+ raise TypeError(f"Names {lowercase_names} should be uppercase")
26
+ return ret
27
+
28
+
29
+ class StrictEnum(str, Enum, metaclass=StrictEnumMeta):
21
30
  """All members should exist in the Backend, and there will be no extras"""
22
31
 
23
32
 
24
- class SubsetEnumMeta(EnumMeta):
33
+ class SubsetEnumMeta(StrictEnumMeta):
25
34
  def __call__(self, value, *args, **kwargs): # type: ignore
26
35
  if isinstance(value, str) and not isinstance(value, self):
27
36
  return value
@@ -31,9 +31,9 @@ class AravisController(DetectorController):
31
31
 
32
32
  async def prepare(self, trigger_info: TriggerInfo):
33
33
  if trigger_info.total_number_of_triggers == 0:
34
- image_mode = adcore.ImageMode.continuous
34
+ image_mode = adcore.ImageMode.CONTINUOUS
35
35
  else:
36
- image_mode = adcore.ImageMode.multiple
36
+ image_mode = adcore.ImageMode.MULTIPLE
37
37
  if (exposure := trigger_info.livetime) is not None:
38
38
  await self._drv.acquire_time.set(exposure)
39
39
 
@@ -58,9 +58,9 @@ class AravisController(DetectorController):
58
58
  self, trigger: DetectorTrigger
59
59
  ) -> tuple[AravisTriggerMode, AravisTriggerSource]:
60
60
  supported_trigger_types = (
61
- DetectorTrigger.constant_gate,
62
- DetectorTrigger.edge_trigger,
63
- DetectorTrigger.internal,
61
+ DetectorTrigger.CONSTANT_GATE,
62
+ DetectorTrigger.EDGE_TRIGGER,
63
+ DetectorTrigger.INTERNAL,
64
64
  )
65
65
  if trigger not in supported_trigger_types:
66
66
  raise ValueError(
@@ -68,10 +68,10 @@ class AravisController(DetectorController):
68
68
  f"types: {supported_trigger_types} but was asked to "
69
69
  f"use {trigger}"
70
70
  )
71
- if trigger == DetectorTrigger.internal:
72
- return AravisTriggerMode.off, AravisTriggerSource.freerun
71
+ if trigger == DetectorTrigger.INTERNAL:
72
+ return AravisTriggerMode.OFF, AravisTriggerSource.FREERUN
73
73
  else:
74
- return (AravisTriggerMode.on, f"Line{self.gpio_number}") # type: ignore
74
+ return (AravisTriggerMode.ON, f"Line{self.gpio_number}") # type: ignore
75
75
 
76
76
  async def disarm(self):
77
77
  await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
@@ -6,8 +6,8 @@ from ophyd_async.epics.core import epics_signal_rw_rbv
6
6
  class AravisTriggerMode(StrictEnum):
7
7
  """GigEVision GenICAM standard: on=externally triggered"""
8
8
 
9
- on = "On"
10
- off = "Off"
9
+ ON = "On"
10
+ OFF = "Off"
11
11
 
12
12
 
13
13
  """A minimal set of TriggerSources that must be supported by the underlying record.
@@ -20,8 +20,8 @@ class AravisTriggerMode(StrictEnum):
20
20
 
21
21
 
22
22
  class AravisTriggerSource(SubsetEnum):
23
- freerun = "Freerun"
24
- line1 = "Line1"
23
+ FREERUN = "Freerun"
24
+ LINE1 = "Line1"
25
25
 
26
26
 
27
27
  class AravisDriverIO(adcore.ADBaseIO):
@@ -9,8 +9,8 @@ from ._utils import ADBaseDataType, FileWriteMode, ImageMode
9
9
 
10
10
 
11
11
  class Callback(StrictEnum):
12
- Enable = "Enable"
13
- Disable = "Disable"
12
+ ENABLE = "Enable"
13
+ DISABLE = "Disable"
14
14
 
15
15
 
16
16
  class NDArrayBaseIO(Device):
@@ -72,17 +72,17 @@ class DetectorState(StrictEnum):
72
72
  See definition in ADApp/ADSrc/ADDriver.h in https://github.com/areaDetector/ADCore
73
73
  """
74
74
 
75
- Idle = "Idle"
76
- Acquire = "Acquire"
77
- Readout = "Readout"
78
- Correct = "Correct"
79
- Saving = "Saving"
80
- Aborting = "Aborting"
81
- Error = "Error"
82
- Waiting = "Waiting"
83
- Initializing = "Initializing"
84
- Disconnected = "Disconnected"
85
- Aborted = "Aborted"
75
+ IDLE = "Idle"
76
+ ACQUIRE = "Acquire"
77
+ READOUT = "Readout"
78
+ CORRECT = "Correct"
79
+ SAVING = "Saving"
80
+ ABORTING = "Aborting"
81
+ ERROR = "Error"
82
+ WAITING = "Waiting"
83
+ INITIALIZING = "Initializing"
84
+ DISCONNECTED = "Disconnected"
85
+ ABORTED = "Aborted"
86
86
 
87
87
 
88
88
  class ADBaseIO(NDArrayBaseIO):
@@ -99,14 +99,14 @@ class ADBaseIO(NDArrayBaseIO):
99
99
 
100
100
 
101
101
  class Compression(StrictEnum):
102
- none = "None"
103
- nbit = "N-bit"
104
- szip = "szip"
105
- zlib = "zlib"
106
- blosc = "Blosc"
107
- bslz4 = "BSLZ4"
108
- lz4 = "LZ4"
109
- jpeg = "JPEG"
102
+ NONE = "None"
103
+ NBIT = "N-bit"
104
+ SZIP = "szip"
105
+ ZLIB = "zlib"
106
+ BLOSC = "Blosc"
107
+ BSLZ4 = "BSLZ4"
108
+ LZ4 = "LZ4"
109
+ JPEG = "JPEG"
110
110
 
111
111
 
112
112
  class NDFileHDFIO(NDPluginBaseIO):