ophyd-async 0.5.2__py3-none-any.whl → 0.6.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 (66) hide show
  1. ophyd_async/__init__.py +10 -1
  2. ophyd_async/__main__.py +12 -4
  3. ophyd_async/_version.py +2 -2
  4. ophyd_async/core/__init__.py +11 -3
  5. ophyd_async/core/_detector.py +72 -63
  6. ophyd_async/core/_device.py +13 -15
  7. ophyd_async/core/_device_save_loader.py +30 -19
  8. ophyd_async/core/_flyer.py +6 -4
  9. ophyd_async/core/_hdf_dataset.py +8 -9
  10. ophyd_async/core/_log.py +3 -1
  11. ophyd_async/core/_mock_signal_backend.py +11 -9
  12. ophyd_async/core/_mock_signal_utils.py +8 -5
  13. ophyd_async/core/_protocol.py +7 -7
  14. ophyd_async/core/_providers.py +11 -11
  15. ophyd_async/core/_readable.py +30 -22
  16. ophyd_async/core/_signal.py +52 -51
  17. ophyd_async/core/_signal_backend.py +20 -7
  18. ophyd_async/core/_soft_signal_backend.py +62 -32
  19. ophyd_async/core/_status.py +7 -9
  20. ophyd_async/core/_table.py +63 -0
  21. ophyd_async/core/_utils.py +24 -28
  22. ophyd_async/epics/adaravis/_aravis_controller.py +17 -16
  23. ophyd_async/epics/adaravis/_aravis_io.py +2 -1
  24. ophyd_async/epics/adcore/_core_io.py +2 -0
  25. ophyd_async/epics/adcore/_core_logic.py +2 -3
  26. ophyd_async/epics/adcore/_hdf_writer.py +19 -8
  27. ophyd_async/epics/adcore/_single_trigger.py +1 -1
  28. ophyd_async/epics/adcore/_utils.py +5 -6
  29. ophyd_async/epics/adkinetix/_kinetix_controller.py +19 -14
  30. ophyd_async/epics/adpilatus/_pilatus_controller.py +18 -16
  31. ophyd_async/epics/adsimdetector/_sim.py +6 -5
  32. ophyd_async/epics/adsimdetector/_sim_controller.py +20 -15
  33. ophyd_async/epics/advimba/_vimba_controller.py +21 -16
  34. ophyd_async/epics/demo/_mover.py +4 -5
  35. ophyd_async/epics/demo/sensor.db +0 -1
  36. ophyd_async/epics/eiger/_eiger.py +1 -1
  37. ophyd_async/epics/eiger/_eiger_controller.py +16 -16
  38. ophyd_async/epics/eiger/_odin_io.py +6 -5
  39. ophyd_async/epics/motor.py +8 -10
  40. ophyd_async/epics/pvi/_pvi.py +30 -33
  41. ophyd_async/epics/signal/_aioca.py +55 -25
  42. ophyd_async/epics/signal/_common.py +3 -10
  43. ophyd_async/epics/signal/_epics_transport.py +11 -8
  44. ophyd_async/epics/signal/_p4p.py +79 -30
  45. ophyd_async/epics/signal/_signal.py +6 -8
  46. ophyd_async/fastcs/panda/__init__.py +0 -6
  47. ophyd_async/fastcs/panda/_control.py +14 -15
  48. ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
  49. ophyd_async/fastcs/panda/_table.py +111 -138
  50. ophyd_async/fastcs/panda/_trigger.py +1 -2
  51. ophyd_async/fastcs/panda/_utils.py +3 -2
  52. ophyd_async/fastcs/panda/_writer.py +28 -13
  53. ophyd_async/plan_stubs/_fly.py +16 -16
  54. ophyd_async/plan_stubs/_nd_attributes.py +12 -6
  55. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
  56. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +24 -20
  57. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
  58. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
  59. ophyd_async/sim/demo/_sim_motor.py +2 -1
  60. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/METADATA +46 -45
  61. ophyd_async-0.6.0.dist-info/RECORD +96 -0
  62. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/WHEEL +1 -1
  63. ophyd_async-0.5.2.dist-info/RECORD +0 -95
  64. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/LICENSE +0 -0
  65. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/entry_points.txt +0 -0
  66. {ophyd_async-0.5.2.dist-info → ophyd_async-0.6.0.dist-info}/top_level.txt +0 -0
@@ -38,6 +38,6 @@ class EigerDetector(StandardDetector):
38
38
  )
39
39
 
40
40
  @AsyncStatus.wrap
41
- async def prepare(self, value: EigerTriggerInfo) -> None:
41
+ async def prepare(self, value: EigerTriggerInfo) -> None: # type: ignore
42
42
  await self._controller.set_energy(value.energy_ev)
43
43
  await super().prepare(value)
@@ -1,13 +1,12 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
3
  from ophyd_async.core import (
5
4
  DEFAULT_TIMEOUT,
6
- AsyncStatus,
7
5
  DetectorControl,
8
6
  DetectorTrigger,
9
7
  set_and_wait_for_other_value,
10
8
  )
9
+ from ophyd_async.core._detector import TriggerInfo
11
10
 
12
11
  from ._eiger_io import EigerDriverIO, EigerTriggerMode
13
12
 
@@ -26,7 +25,7 @@ class EigerController(DetectorControl):
26
25
  ) -> None:
27
26
  self._drv = driver
28
27
 
29
- def get_deadtime(self, exposure: float) -> float:
28
+ def get_deadtime(self, exposure: float | None) -> float:
30
29
  # See https://media.dectris.com/filer_public/30/14/3014704e-5f3b-43ba-8ccf-8ef720e60d2a/240202_usermanual_eiger2.pdf
31
30
  return 0.0001
32
31
 
@@ -37,30 +36,31 @@ class EigerController(DetectorControl):
37
36
  if abs(current_energy - energy) > tolerance:
38
37
  await self._drv.photon_energy.set(energy)
39
38
 
40
- @AsyncStatus.wrap
41
- async def arm(
42
- self,
43
- num: int,
44
- trigger: DetectorTrigger = DetectorTrigger.internal,
45
- exposure: Optional[float] = None,
46
- ):
39
+ async def prepare(self, trigger_info: TriggerInfo):
47
40
  coros = [
48
- self._drv.trigger_mode.set(EIGER_TRIGGER_MODE_MAP[trigger].value),
49
- self._drv.num_images.set(num),
41
+ self._drv.trigger_mode.set(
42
+ EIGER_TRIGGER_MODE_MAP[trigger_info.trigger].value
43
+ ),
44
+ self._drv.num_images.set(trigger_info.number),
50
45
  ]
51
- if exposure is not None:
46
+ if trigger_info.livetime is not None:
52
47
  coros.extend(
53
48
  [
54
- self._drv.acquire_time.set(exposure),
55
- self._drv.acquire_period.set(exposure),
49
+ self._drv.acquire_time.set(trigger_info.livetime),
50
+ self._drv.acquire_period.set(trigger_info.livetime),
56
51
  ]
57
52
  )
58
53
  await asyncio.gather(*coros)
59
54
 
55
+ async def arm(self):
60
56
  # TODO: Detector state should be an enum see https://github.com/DiamondLightSource/eiger-fastcs/issues/43
61
- await set_and_wait_for_other_value(
57
+ self._arm_status = set_and_wait_for_other_value(
62
58
  self._drv.arm, 1, self._drv.state, "ready", timeout=DEFAULT_TIMEOUT
63
59
  )
64
60
 
61
+ async def wait_for_idle(self):
62
+ if self._arm_status:
63
+ await self._arm_status
64
+
65
65
  async def disarm(self):
66
66
  await self._drv.disarm.set(1)
@@ -1,9 +1,9 @@
1
1
  import asyncio
2
+ from collections.abc import AsyncGenerator, AsyncIterator
2
3
  from enum import Enum
3
- from typing import AsyncGenerator, AsyncIterator, Dict
4
4
 
5
5
  from bluesky.protocols import StreamAsset
6
- from event_model.documents.event_descriptor import DataKey
6
+ from event_model import DataKey
7
7
 
8
8
  from ophyd_async.core import (
9
9
  DEFAULT_TIMEOUT,
@@ -77,7 +77,7 @@ class OdinWriter(DetectorWriter):
77
77
  self._name_provider = name_provider
78
78
  super().__init__()
79
79
 
80
- async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
80
+ async def open(self, multiplier: int = 1) -> dict[str, DataKey]:
81
81
  info = self._path_provider(device_name=self._name_provider())
82
82
 
83
83
  await asyncio.gather(
@@ -93,7 +93,7 @@ class OdinWriter(DetectorWriter):
93
93
 
94
94
  return await self._describe()
95
95
 
96
- async def _describe(self) -> Dict[str, DataKey]:
96
+ async def _describe(self) -> dict[str, DataKey]:
97
97
  data_shape = await asyncio.gather(
98
98
  self._drv.image_height.get_value(), self._drv.image_width.get_value()
99
99
  )
@@ -103,7 +103,8 @@ class OdinWriter(DetectorWriter):
103
103
  source=self._drv.file_name.source,
104
104
  shape=data_shape,
105
105
  dtype="array",
106
- dtype_numpy="<u2", # TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529
106
+ # TODO: Use correct type based on eiger https://github.com/bluesky/ophyd-async/issues/529
107
+ dtype_numpy="<u2", # type: ignore
107
108
  external="STREAM:",
108
109
  )
109
110
  }
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
3
  from bluesky.protocols import (
5
4
  Flyable,
@@ -11,10 +10,10 @@ from bluesky.protocols import (
11
10
  from pydantic import BaseModel, Field
12
11
 
13
12
  from ophyd_async.core import (
13
+ CALCULATE_TIMEOUT,
14
14
  DEFAULT_TIMEOUT,
15
15
  AsyncStatus,
16
16
  CalculatableTimeout,
17
- CalculateTimeout,
18
17
  ConfigSignal,
19
18
  HintedSignal,
20
19
  StandardReadable,
@@ -54,7 +53,7 @@ class FlyMotorInfo(BaseModel):
54
53
 
55
54
  #: Maximum time for the complete motor move, including run up and run down.
56
55
  #: Defaults to `time_for_move` + run up and run down times + 10s.
57
- timeout: CalculatableTimeout = Field(frozen=True, default=CalculateTimeout)
56
+ timeout: CalculatableTimeout = Field(frozen=True, default=CALCULATE_TIMEOUT)
58
57
 
59
58
 
60
59
  class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
@@ -83,13 +82,13 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
83
82
  self._set_success = True
84
83
 
85
84
  # end_position of a fly move, with run_up_distance added on.
86
- self._fly_completed_position: Optional[float] = None
85
+ self._fly_completed_position: float | None = None
87
86
 
88
87
  # Set on kickoff(), complete when motor reaches self._fly_completed_position
89
- self._fly_status: Optional[WatchableAsyncStatus] = None
88
+ self._fly_status: WatchableAsyncStatus | None = None
90
89
 
91
90
  # Set during prepare
92
- self._fly_timeout: Optional[CalculatableTimeout] = CalculateTimeout
91
+ self._fly_timeout: CalculatableTimeout | None = CALCULATE_TIMEOUT
93
92
 
94
93
  super().__init__(name=name)
95
94
 
@@ -138,9 +137,8 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
138
137
  return self._fly_status
139
138
 
140
139
  @WatchableAsyncStatus.wrap
141
- async def set(
142
- self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
143
- ):
140
+ async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
141
+ new_position = value
144
142
  self._set_success = True
145
143
  (
146
144
  old_position,
@@ -155,7 +153,7 @@ class Motor(StandardReadable, Locatable, Stoppable, Flyable, Preparable):
155
153
  self.velocity.get_value(),
156
154
  self.acceleration_time.get_value(),
157
155
  )
158
- if timeout is CalculateTimeout:
156
+ if timeout is CALCULATE_TIMEOUT:
159
157
  assert velocity > 0, "Motor has zero velocity"
160
158
  timeout = (
161
159
  abs(new_position - old_position) / velocity
@@ -1,15 +1,11 @@
1
1
  import re
2
+ import types
3
+ from collections.abc import Callable
2
4
  from dataclasses import dataclass
3
5
  from inspect import isclass
4
6
  from typing import (
5
7
  Any,
6
- Callable,
7
- Dict,
8
- FrozenSet,
9
8
  Literal,
10
- Optional,
11
- Tuple,
12
- Type,
13
9
  Union,
14
10
  get_args,
15
11
  get_origin,
@@ -32,23 +28,24 @@ from ophyd_async.epics.signal import (
32
28
  epics_signal_x,
33
29
  )
34
30
 
35
- Access = FrozenSet[
36
- Union[Literal["r"], Literal["w"], Literal["rw"], Literal["x"], Literal["d"]]
31
+ Access = frozenset[
32
+ Literal["r"] | Literal["w"] | Literal["rw"] | Literal["x"] | Literal["d"]
37
33
  ]
38
34
 
39
35
 
40
- def _strip_number_from_string(string: str) -> Tuple[str, Optional[int]]:
36
+ def _strip_number_from_string(string: str) -> tuple[str, int | None]:
41
37
  match = re.match(r"(.*?)(\d*)$", string)
42
38
  assert match
43
39
 
44
40
  name = match.group(1)
45
41
  number = match.group(2) or None
46
- if number:
47
- number = int(number)
48
- return name, number
42
+ if number is None:
43
+ return name, None
44
+ else:
45
+ return name, int(number)
49
46
 
50
47
 
51
- def _split_subscript(tp: T) -> Union[Tuple[Any, Tuple[Any]], Tuple[T, None]]:
48
+ def _split_subscript(tp: T) -> tuple[Any, tuple[Any]] | tuple[T, None]:
52
49
  """Split a subscripted type into the its origin and args.
53
50
 
54
51
  If `tp` is not a subscripted type, then just return the type and None as args.
@@ -60,8 +57,8 @@ def _split_subscript(tp: T) -> Union[Tuple[Any, Tuple[Any]], Tuple[T, None]]:
60
57
  return tp, None
61
58
 
62
59
 
63
- def _strip_union(field: Union[Union[T], T]) -> Tuple[T, bool]:
64
- if get_origin(field) is Union:
60
+ def _strip_union(field: T | T) -> tuple[T, bool]:
61
+ if get_origin(field) in [Union, types.UnionType]:
65
62
  args = get_args(field)
66
63
  is_optional = type(None) in args
67
64
  for arg in args:
@@ -70,7 +67,7 @@ def _strip_union(field: Union[Union[T], T]) -> Tuple[T, bool]:
70
67
  return field, False
71
68
 
72
69
 
73
- def _strip_device_vector(field: Union[Type[Device]]) -> Tuple[bool, Type[Device]]:
70
+ def _strip_device_vector(field: type[Device]) -> tuple[bool, type[Device]]:
74
71
  if get_origin(field) is DeviceVector:
75
72
  return True, get_args(field)[0]
76
73
  return False, field
@@ -83,13 +80,13 @@ class _PVIEntry:
83
80
  This could either be a signal or a sub-table.
84
81
  """
85
82
 
86
- sub_entries: Dict[str, Union[Dict[int, "_PVIEntry"], "_PVIEntry"]]
87
- pvi_pv: Optional[str] = None
88
- device: Optional[Device] = None
89
- common_device_type: Optional[Type[Device]] = None
83
+ sub_entries: dict[str, Union[dict[int, "_PVIEntry"], "_PVIEntry"]]
84
+ pvi_pv: str | None = None
85
+ device: Device | None = None
86
+ common_device_type: type[Device] | None = None
90
87
 
91
88
 
92
- def _verify_common_blocks(entry: _PVIEntry, common_device: Type[Device]):
89
+ def _verify_common_blocks(entry: _PVIEntry, common_device: type[Device]):
93
90
  if not entry.sub_entries:
94
91
  return
95
92
  common_sub_devices = get_type_hints(common_device)
@@ -107,12 +104,12 @@ def _verify_common_blocks(entry: _PVIEntry, common_device: Type[Device]):
107
104
  _verify_common_blocks(sub_sub_entry, sub_device) # type: ignore
108
105
  else:
109
106
  _verify_common_blocks(
110
- entry.sub_entries[sub_name],
107
+ entry.sub_entries[sub_name], # type: ignore
111
108
  sub_device, # type: ignore
112
109
  )
113
110
 
114
111
 
115
- _pvi_mapping: Dict[FrozenSet[str], Callable[..., Signal]] = {
112
+ _pvi_mapping: dict[frozenset[str], Callable[..., Signal]] = {
116
113
  frozenset({"r", "w"}): lambda dtype, read_pv, write_pv: epics_signal_rw(
117
114
  dtype, "pva://" + read_pv, "pva://" + write_pv
118
115
  ),
@@ -129,8 +126,8 @@ _pvi_mapping: Dict[FrozenSet[str], Callable[..., Signal]] = {
129
126
 
130
127
  def _parse_type(
131
128
  is_pvi_table: bool,
132
- number_suffix: Optional[int],
133
- common_device_type: Optional[Type[Device]],
129
+ number_suffix: int | None,
130
+ common_device_type: type[Device] | None,
134
131
  ):
135
132
  if common_device_type:
136
133
  # pre-defined type
@@ -159,7 +156,7 @@ def _parse_type(
159
156
  return is_device_vector, is_signal, signal_dtype, device_cls
160
157
 
161
158
 
162
- def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None):
159
+ def _mock_common_blocks(device: Device, stripped_type: type | None = None):
163
160
  device_t = stripped_type or type(device)
164
161
  sub_devices = (
165
162
  (field, field_type)
@@ -173,11 +170,10 @@ def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None):
173
170
  device_cls, device_args = _split_subscript(device_cls)
174
171
  assert issubclass(device_cls, Device)
175
172
 
176
- is_signal = issubclass(device_cls, Signal)
177
173
  signal_dtype = device_args[0] if device_args is not None else None
178
174
 
179
175
  if is_device_vector:
180
- if is_signal:
176
+ if issubclass(device_cls, Signal):
181
177
  sub_device_1 = device_cls(SoftSignalBackend(signal_dtype))
182
178
  sub_device_2 = device_cls(SoftSignalBackend(signal_dtype))
183
179
  sub_device = DeviceVector({1: sub_device_1, 2: sub_device_2})
@@ -198,7 +194,7 @@ def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None):
198
194
  for value in sub_device.values():
199
195
  value.parent = sub_device
200
196
  else:
201
- if is_signal:
197
+ if issubclass(device_cls, Signal):
202
198
  sub_device = device_cls(SoftSignalBackend(signal_dtype))
203
199
  else:
204
200
  sub_device = getattr(device, device_name, device_cls())
@@ -271,7 +267,8 @@ def _set_device_attributes(entry: _PVIEntry):
271
267
  # Set the device vector entry to have the device vector as a parent
272
268
  device_vector_sub_entry.device.parent = sub_device # type: ignore
273
269
  else:
274
- sub_device = sub_entry.device # type: ignore
270
+ sub_device = sub_entry.device
271
+ assert sub_device, f"Device of {sub_entry} is None"
275
272
  if sub_entry.pvi_pv:
276
273
  _set_device_attributes(sub_entry)
277
274
 
@@ -308,8 +305,8 @@ async def fill_pvi_entries(
308
305
 
309
306
  def create_children_from_annotations(
310
307
  device: Device,
311
- included_optional_fields: Tuple[str, ...] = (),
312
- device_vectors: Optional[Dict[str, int]] = None,
308
+ included_optional_fields: tuple[str, ...] = (),
309
+ device_vectors: dict[str, int] | None = None,
313
310
  ):
314
311
  """For intializing blocks at __init__ of ``device``."""
315
312
  for name, device_type in get_type_hints(type(device)).items():
@@ -328,7 +325,7 @@ def create_children_from_annotations(
328
325
 
329
326
  if is_device_vector:
330
327
  n_device_vector = DeviceVector(
331
- {i: device_type() for i in range(1, device_vectors[name] + 1)}
328
+ {i: device_type() for i in range(1, device_vectors[name] + 1)} # type: ignore
332
329
  )
333
330
  setattr(device, name, n_device_vector)
334
331
  for sub_device in n_device_vector.values():
@@ -1,9 +1,11 @@
1
+ import inspect
1
2
  import logging
2
3
  import sys
4
+ from collections.abc import Sequence
3
5
  from dataclasses import dataclass
4
6
  from enum import Enum
5
7
  from math import isnan, nan
6
- from typing import Any, Dict, List, Optional, Type, Union
8
+ from typing import Any, get_origin
7
9
 
8
10
  import numpy as np
9
11
  from aioca import (
@@ -17,13 +19,16 @@ from aioca import (
17
19
  caput,
18
20
  )
19
21
  from aioca.types import AugmentedValue, Dbr, Format
20
- from bluesky.protocols import DataKey, Dtype, Reading
22
+ from bluesky.protocols import Reading
21
23
  from epicscorelibs.ca import dbr
24
+ from event_model import DataKey
25
+ from event_model.documents.event_descriptor import Dtype
22
26
 
23
27
  from ophyd_async.core import (
24
28
  DEFAULT_TIMEOUT,
25
29
  NotConnected,
26
30
  ReadingValueCallback,
31
+ RuntimeSubsetEnum,
27
32
  SignalBackend,
28
33
  T,
29
34
  get_dtype,
@@ -33,7 +38,7 @@ from ophyd_async.core import (
33
38
 
34
39
  from ._common import LimitPair, Limits, common_meta, get_supported_values
35
40
 
36
- dbr_to_dtype: Dict[Dbr, Dtype] = {
41
+ dbr_to_dtype: dict[Dbr, Dtype] = {
37
42
  dbr.DBR_STRING: "string",
38
43
  dbr.DBR_SHORT: "integer",
39
44
  dbr.DBR_FLOAT: "number",
@@ -46,8 +51,8 @@ dbr_to_dtype: Dict[Dbr, Dtype] = {
46
51
  def _data_key_from_augmented_value(
47
52
  value: AugmentedValue,
48
53
  *,
49
- choices: Optional[List[str]] = None,
50
- dtype: Optional[Dtype] = None,
54
+ choices: list[str] | None = None,
55
+ dtype: Dtype | None = None,
51
56
  ) -> DataKey:
52
57
  """Use the return value of get with FORMAT_CTRL to construct a DataKey
53
58
  describing the signal. See docstring of AugmentedValue for expected
@@ -65,14 +70,15 @@ def _data_key_from_augmented_value(
65
70
  assert value.ok, f"Error reading {source}: {value}"
66
71
 
67
72
  scalar = value.element_count == 1
68
- dtype = dtype or dbr_to_dtype[value.datatype]
73
+ dtype = dtype or dbr_to_dtype[value.datatype] # type: ignore
69
74
 
70
75
  dtype_numpy = np.dtype(dbr.DbrCodeToType[value.datatype].dtype).descr[0][1]
71
76
 
72
77
  d = DataKey(
73
78
  source=source,
74
79
  dtype=dtype if scalar else "array",
75
- dtype_numpy=dtype_numpy,
80
+ # Ignore until https://github.com/bluesky/event-model/issues/308
81
+ dtype_numpy=dtype_numpy, # type: ignore
76
82
  # strictly value.element_count >= len(value)
77
83
  shape=[] if scalar else [len(value)],
78
84
  )
@@ -82,10 +88,10 @@ def _data_key_from_augmented_value(
82
88
  d[key] = attr
83
89
 
84
90
  if choices is not None:
85
- d["choices"] = choices
91
+ d["choices"] = choices # type: ignore
86
92
 
87
93
  if limits := _limits_from_augmented_value(value):
88
- d["limits"] = limits
94
+ d["limits"] = limits # type: ignore
89
95
 
90
96
  return d
91
97
 
@@ -108,8 +114,8 @@ def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
108
114
 
109
115
  @dataclass
110
116
  class CaConverter:
111
- read_dbr: Optional[Dbr]
112
- write_dbr: Optional[Dbr]
117
+ read_dbr: Dbr | None
118
+ write_dbr: Dbr | None
113
119
 
114
120
  def write_value(self, value) -> Any:
115
121
  return value
@@ -118,9 +124,9 @@ class CaConverter:
118
124
  # for channel access ca_xxx classes, this
119
125
  # invokes __pos__ operator to return an instance of
120
126
  # the builtin base class
121
- return +value
127
+ return +value # type: ignore
122
128
 
123
- def reading(self, value: AugmentedValue):
129
+ def reading(self, value: AugmentedValue) -> Reading:
124
130
  return {
125
131
  "value": self.value(value),
126
132
  "timestamp": value.timestamp,
@@ -155,14 +161,14 @@ class CaEnumConverter(CaConverter):
155
161
 
156
162
  choices: dict[str, str]
157
163
 
158
- def write_value(self, value: Union[Enum, str]):
164
+ def write_value(self, value: Enum | str):
159
165
  if isinstance(value, Enum):
160
166
  return value.value
161
167
  else:
162
168
  return value
163
169
 
164
170
  def value(self, value: AugmentedValue):
165
- return self.choices[value]
171
+ return self.choices[value] # type: ignore
166
172
 
167
173
  def get_datakey(self, value: AugmentedValue) -> DataKey:
168
174
  # Sometimes DBR_TYPE returns as String, must pass choices still
@@ -184,7 +190,7 @@ class DisconnectedCaConverter(CaConverter):
184
190
 
185
191
 
186
192
  def make_converter(
187
- datatype: Optional[Type], values: Dict[str, AugmentedValue]
193
+ datatype: type | None, values: dict[str, AugmentedValue]
188
194
  ) -> CaConverter:
189
195
  pv = list(values)[0]
190
196
  pv_dbr = get_unique({k: v.datatype for k, v in values.items()}, "datatypes")
@@ -200,7 +206,7 @@ def make_converter(
200
206
  raise TypeError(f"{pv} has type [str] not {datatype.__name__}")
201
207
  return CaArrayConverter(pv_dbr, None)
202
208
  elif is_array:
203
- pv_dtype = get_unique({k: v.dtype for k, v in values.items()}, "dtypes")
209
+ pv_dtype = get_unique({k: v.dtype for k, v in values.items()}, "dtypes") # type: ignore
204
210
  # This is an array
205
211
  if datatype:
206
212
  # Check we wanted an array of this type
@@ -209,9 +215,10 @@ def make_converter(
209
215
  raise TypeError(f"{pv} has type [{pv_dtype}] not {datatype.__name__}")
210
216
  if dtype != pv_dtype:
211
217
  raise TypeError(f"{pv} has type [{pv_dtype}] not [{dtype}]")
212
- return CaArrayConverter(pv_dbr, None)
218
+ return CaArrayConverter(pv_dbr, None) # type: ignore
213
219
  elif pv_dbr == dbr.DBR_ENUM and datatype is bool:
214
- # Database can't do bools, so are often representated as enums, CA can do int
220
+ # Database can't do bools, so are often representated as enums,
221
+ # CA can do int
215
222
  pv_choices_len = get_unique(
216
223
  {k: len(v.enums) for k, v in values.items()}, "number of choices"
217
224
  )
@@ -240,7 +247,7 @@ def make_converter(
240
247
  f"{pv} has type {type(value).__name__.replace('ca_', '')} "
241
248
  + f"not {datatype.__name__}"
242
249
  )
243
- return CaConverter(pv_dbr, None)
250
+ return CaConverter(pv_dbr, None) # type: ignore
244
251
 
245
252
 
246
253
  _tried_pyepics = False
@@ -256,13 +263,36 @@ def _use_pyepics_context_if_imported():
256
263
 
257
264
 
258
265
  class CaSignalBackend(SignalBackend[T]):
259
- def __init__(self, datatype: Optional[Type[T]], read_pv: str, write_pv: str):
266
+ _ALLOWED_DATATYPES = (
267
+ bool,
268
+ int,
269
+ float,
270
+ str,
271
+ Sequence,
272
+ Enum,
273
+ RuntimeSubsetEnum,
274
+ np.ndarray,
275
+ )
276
+
277
+ @classmethod
278
+ def datatype_allowed(cls, dtype: Any) -> bool:
279
+ stripped_origin = get_origin(dtype) or dtype
280
+ if dtype is None:
281
+ return True
282
+
283
+ return inspect.isclass(stripped_origin) and issubclass(
284
+ stripped_origin, cls._ALLOWED_DATATYPES
285
+ )
286
+
287
+ def __init__(self, datatype: type[T] | None, read_pv: str, write_pv: str):
260
288
  self.datatype = datatype
289
+ if not CaSignalBackend.datatype_allowed(self.datatype):
290
+ raise TypeError(f"Given datatype {self.datatype} unsupported in CA.")
261
291
  self.read_pv = read_pv
262
292
  self.write_pv = write_pv
263
- self.initial_values: Dict[str, AugmentedValue] = {}
293
+ self.initial_values: dict[str, AugmentedValue] = {}
264
294
  self.converter: CaConverter = DisconnectedCaConverter(None, None)
265
- self.subscription: Optional[Subscription] = None
295
+ self.subscription: Subscription | None = None
266
296
 
267
297
  def source(self, name: str):
268
298
  return f"ca://{self.read_pv}"
@@ -289,7 +319,7 @@ class CaSignalBackend(SignalBackend[T]):
289
319
  await self._store_initial_value(self.read_pv, timeout=timeout)
290
320
  self.converter = make_converter(self.datatype, self.initial_values)
291
321
 
292
- async def put(self, value: Optional[T], wait=True, timeout=None):
322
+ async def put(self, value: T | None, wait=True, timeout=None):
293
323
  if value is None:
294
324
  write_value = self.initial_values[self.write_pv]
295
325
  else:
@@ -331,7 +361,7 @@ class CaSignalBackend(SignalBackend[T]):
331
361
  )
332
362
  return self.converter.value(value)
333
363
 
334
- def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
364
+ def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
335
365
  if callback:
336
366
  assert (
337
367
  not self.subscription
@@ -1,6 +1,5 @@
1
1
  import inspect
2
2
  from enum import Enum
3
- from typing import Dict, Optional, Tuple, Type
4
3
 
5
4
  from typing_extensions import TypedDict
6
5
 
@@ -16,9 +15,6 @@ class LimitPair(TypedDict):
16
15
  high: float | None
17
16
  low: float | None
18
17
 
19
- def __bool__(self) -> bool:
20
- return self.low is None and self.high is None
21
-
22
18
 
23
19
  class Limits(TypedDict):
24
20
  alarm: LimitPair
@@ -26,15 +22,12 @@ class Limits(TypedDict):
26
22
  display: LimitPair
27
23
  warning: LimitPair
28
24
 
29
- def __bool__(self) -> bool:
30
- return any(self.alarm, self.control, self.display, self.warning)
31
-
32
25
 
33
26
  def get_supported_values(
34
27
  pv: str,
35
- datatype: Optional[Type[str]],
36
- pv_choices: Tuple[str, ...],
37
- ) -> Dict[str, str]:
28
+ datatype: type[str] | None,
29
+ pv_choices: tuple[str, ...],
30
+ ) -> dict[str, str]:
38
31
  if inspect.isclass(datatype) and issubclass(datatype, RuntimeSubsetEnum):
39
32
  if not set(datatype.choices).issubset(set(pv_choices)):
40
33
  raise TypeError(
@@ -4,22 +4,25 @@ from __future__ import annotations
4
4
 
5
5
  from enum import Enum
6
6
 
7
+
8
+ def _make_unavailable_class(error: Exception) -> type:
9
+ class TransportNotAvailable:
10
+ def __init__(*args, **kwargs):
11
+ raise NotImplementedError("Transport not available") from error
12
+
13
+ return TransportNotAvailable
14
+
15
+
7
16
  try:
8
17
  from ._aioca import CaSignalBackend
9
18
  except ImportError as ca_error:
10
-
11
- class CaSignalBackend: # type: ignore
12
- def __init__(*args, ca_error=ca_error, **kwargs):
13
- raise NotImplementedError("CA support not available") from ca_error
19
+ CaSignalBackend = _make_unavailable_class(ca_error)
14
20
 
15
21
 
16
22
  try:
17
23
  from ._p4p import PvaSignalBackend
18
24
  except ImportError as pva_error:
19
-
20
- class PvaSignalBackend: # type: ignore
21
- def __init__(*args, pva_error=pva_error, **kwargs):
22
- raise NotImplementedError("PVA support not available") from pva_error
25
+ PvaSignalBackend = _make_unavailable_class(pva_error)
23
26
 
24
27
 
25
28
  class _EpicsTransport(Enum):