ophyd-async 0.5.2__py3-none-any.whl → 0.7.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 (80) 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 +15 -7
  5. ophyd_async/core/_detector.py +133 -87
  6. ophyd_async/core/_device.py +19 -16
  7. ophyd_async/core/_device_save_loader.py +30 -19
  8. ophyd_async/core/_flyer.py +6 -19
  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 +146 -0
  21. ophyd_async/core/_utils.py +24 -28
  22. ophyd_async/epics/adaravis/_aravis_controller.py +20 -19
  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 +4 -5
  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 +20 -15
  30. ophyd_async/epics/adpilatus/_pilatus_controller.py +22 -18
  31. ophyd_async/epics/adsimdetector/_sim.py +7 -6
  32. ophyd_async/epics/adsimdetector/_sim_controller.py +22 -17
  33. ophyd_async/epics/advimba/_vimba_controller.py +22 -17
  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 +18 -18
  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/_block.py +7 -0
  48. ophyd_async/fastcs/panda/_control.py +16 -17
  49. ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
  50. ophyd_async/fastcs/panda/_table.py +77 -138
  51. ophyd_async/fastcs/panda/_trigger.py +4 -5
  52. ophyd_async/fastcs/panda/_utils.py +3 -2
  53. ophyd_async/fastcs/panda/_writer.py +30 -15
  54. ophyd_async/plan_stubs/_fly.py +15 -17
  55. ophyd_async/plan_stubs/_nd_attributes.py +12 -6
  56. ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +3 -3
  57. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +27 -21
  58. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +9 -6
  59. ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +21 -23
  60. ophyd_async/sim/demo/_sim_motor.py +2 -1
  61. ophyd_async/tango/__init__.py +45 -0
  62. ophyd_async/tango/base_devices/__init__.py +4 -0
  63. ophyd_async/tango/base_devices/_base_device.py +225 -0
  64. ophyd_async/tango/base_devices/_tango_readable.py +33 -0
  65. ophyd_async/tango/demo/__init__.py +12 -0
  66. ophyd_async/tango/demo/_counter.py +37 -0
  67. ophyd_async/tango/demo/_detector.py +42 -0
  68. ophyd_async/tango/demo/_mover.py +77 -0
  69. ophyd_async/tango/demo/_tango/__init__.py +3 -0
  70. ophyd_async/tango/demo/_tango/_servers.py +108 -0
  71. ophyd_async/tango/signal/__init__.py +39 -0
  72. ophyd_async/tango/signal/_signal.py +223 -0
  73. ophyd_async/tango/signal/_tango_transport.py +764 -0
  74. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/METADATA +50 -45
  75. ophyd_async-0.7.0.dist-info/RECORD +108 -0
  76. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/WHEEL +1 -1
  77. ophyd_async-0.5.2.dist-info/RECORD +0 -95
  78. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/LICENSE +0 -0
  79. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/entry_points.txt +0 -0
  80. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0.dist-info}/top_level.txt +0 -0
@@ -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):
@@ -3,14 +3,19 @@ import atexit
3
3
  import inspect
4
4
  import logging
5
5
  import time
6
+ from collections.abc import Sequence
6
7
  from dataclasses import dataclass
7
8
  from enum import Enum
8
9
  from math import isnan, nan
9
- from typing import Any, Dict, List, Optional, Sequence, Type, Union
10
+ from typing import Any, get_origin
10
11
 
11
- from bluesky.protocols import DataKey, Dtype, Reading
12
+ import numpy as np
13
+ from bluesky.protocols import Reading
14
+ from event_model import DataKey
15
+ from event_model.documents.event_descriptor import Dtype
12
16
  from p4p import Value
13
17
  from p4p.client.asyncio import Context, Subscription
18
+ from pydantic import BaseModel
14
19
 
15
20
  from ophyd_async.core import (
16
21
  DEFAULT_TIMEOUT,
@@ -21,13 +26,14 @@ from ophyd_async.core import (
21
26
  T,
22
27
  get_dtype,
23
28
  get_unique,
29
+ is_pydantic_model,
24
30
  wait_for_connection,
25
31
  )
26
32
 
27
33
  from ._common import LimitPair, Limits, common_meta, get_supported_values
28
34
 
29
35
  # https://mdavidsaver.github.io/p4p/values.html
30
- specifier_to_dtype: Dict[str, Dtype] = {
36
+ specifier_to_dtype: dict[str, Dtype] = {
31
37
  "?": "integer", # bool
32
38
  "b": "integer", # int8
33
39
  "B": "integer", # uint8
@@ -42,7 +48,7 @@ specifier_to_dtype: Dict[str, Dtype] = {
42
48
  "s": "string",
43
49
  }
44
50
 
45
- specifier_to_np_dtype: Dict[str, str] = {
51
+ specifier_to_np_dtype: dict[str, str] = {
46
52
  "?": "<i2", # bool
47
53
  "b": "|i1", # int8
48
54
  "B": "|u1", # uint8
@@ -62,9 +68,9 @@ def _data_key_from_value(
62
68
  source: str,
63
69
  value: Value,
64
70
  *,
65
- shape: Optional[list[int]] = None,
66
- choices: Optional[list[str]] = None,
67
- dtype: Optional[Dtype] = None,
71
+ shape: list[int] | None = None,
72
+ choices: list[str] | None = None,
73
+ dtype: Dtype | None = None,
68
74
  ) -> DataKey:
69
75
  """
70
76
  Args:
@@ -105,7 +111,8 @@ def _data_key_from_value(
105
111
  d = DataKey(
106
112
  source=source,
107
113
  dtype=dtype,
108
- dtype_numpy=dtype_numpy,
114
+ # type ignore until https://github.com/bluesky/event-model/issues/308
115
+ dtype_numpy=dtype_numpy, # type: ignore
109
116
  shape=shape,
110
117
  )
111
118
  if display_data is not None:
@@ -115,10 +122,12 @@ def _data_key_from_value(
115
122
  d[key] = attr
116
123
 
117
124
  if choices is not None:
118
- d["choices"] = choices
125
+ # type ignore until https://github.com/bluesky/event-model/issues/309
126
+ d["choices"] = choices # type: ignore
119
127
 
120
128
  if limits := _limits_from_value(value):
121
- d["limits"] = limits
129
+ # type ignore until https://github.com/bluesky/event-model/issues/309
130
+ d["limits"] = limits # type: ignore
122
131
 
123
132
  return d
124
133
 
@@ -149,7 +158,7 @@ class PvaConverter:
149
158
  def value(self, value):
150
159
  return value["value"]
151
160
 
152
- def reading(self, value):
161
+ def reading(self, value) -> Reading:
153
162
  ts = value["timeStamp"]
154
163
  sv = value["alarm"]["severity"]
155
164
  return {
@@ -161,13 +170,13 @@ class PvaConverter:
161
170
  def get_datakey(self, source: str, value) -> DataKey:
162
171
  return _data_key_from_value(source, value)
163
172
 
164
- def metadata_fields(self) -> List[str]:
173
+ def metadata_fields(self) -> list[str]:
165
174
  """
166
175
  Fields to request from PVA for metadata.
167
176
  """
168
177
  return ["alarm", "timeStamp"]
169
178
 
170
- def value_fields(self) -> List[str]:
179
+ def value_fields(self) -> list[str]:
171
180
  """
172
181
  Fields to request from PVA for the value.
173
182
  """
@@ -182,11 +191,11 @@ class PvaArrayConverter(PvaConverter):
182
191
 
183
192
 
184
193
  class PvaNDArrayConverter(PvaConverter):
185
- def metadata_fields(self) -> List[str]:
194
+ def metadata_fields(self) -> list[str]:
186
195
  return super().metadata_fields() + ["dimension"]
187
196
 
188
- def _get_dimensions(self, value) -> List[int]:
189
- dimensions: List[Value] = value["dimension"]
197
+ def _get_dimensions(self, value) -> list[int]:
198
+ dimensions: list[Value] = value["dimension"]
190
199
  dims = [dim.size for dim in dimensions]
191
200
  # Note: dimensions in NTNDArray are in fortran-like order
192
201
  # with first index changing fastest.
@@ -221,7 +230,7 @@ class PvaEnumConverter(PvaConverter):
221
230
  def __init__(self, choices: dict[str, str]):
222
231
  self.choices = tuple(choices.values())
223
232
 
224
- def write_value(self, value: Union[Enum, str]):
233
+ def write_value(self, value: Enum | str):
225
234
  if isinstance(value, Enum):
226
235
  return value.value
227
236
  else:
@@ -250,11 +259,24 @@ class PvaTableConverter(PvaConverter):
250
259
 
251
260
  def get_datakey(self, source: str, value) -> DataKey:
252
261
  # This is wrong, but defer until we know how to actually describe a table
253
- return _data_key_from_value(source, value, dtype="object")
262
+ return _data_key_from_value(source, value, dtype="object") # type: ignore
263
+
264
+
265
+ class PvaPydanticModelConverter(PvaConverter):
266
+ def __init__(self, datatype: BaseModel):
267
+ self.datatype = datatype
268
+
269
+ def value(self, value: Value):
270
+ return self.datatype(**value.todict()) # type: ignore
271
+
272
+ def write_value(self, value: BaseModel | dict[str, Any]):
273
+ if isinstance(value, self.datatype): # type: ignore
274
+ return value.model_dump(mode="python") # type: ignore
275
+ return value
254
276
 
255
277
 
256
278
  class PvaDictConverter(PvaConverter):
257
- def reading(self, value):
279
+ def reading(self, value) -> Reading:
258
280
  ts = time.time()
259
281
  value = value.todict()
260
282
  # Alarm severity is vacuously 0 for a table
@@ -266,13 +288,13 @@ class PvaDictConverter(PvaConverter):
266
288
  def get_datakey(self, source: str, value) -> DataKey:
267
289
  raise NotImplementedError("Describing Dict signals not currently supported")
268
290
 
269
- def metadata_fields(self) -> List[str]:
291
+ def metadata_fields(self) -> list[str]:
270
292
  """
271
293
  Fields to request from PVA for metadata.
272
294
  """
273
295
  return []
274
296
 
275
- def value_fields(self) -> List[str]:
297
+ def value_fields(self) -> list[str]:
276
298
  """
277
299
  Fields to request from PVA for the value.
278
300
  """
@@ -284,7 +306,7 @@ class DisconnectedPvaConverter(PvaConverter):
284
306
  raise NotImplementedError("No PV has been set as connect() has not been called")
285
307
 
286
308
 
287
- def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConverter:
309
+ def make_converter(datatype: type | None, values: dict[str, Any]) -> PvaConverter:
288
310
  pv = list(values)[0]
289
311
  typeid = get_unique({k: v.getID() for k, v in values.items()}, "typeids")
290
312
  typ = get_unique(
@@ -333,7 +355,7 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
333
355
  and issubclass(datatype, RuntimeSubsetEnum)
334
356
  ):
335
357
  return PvaEnumConverter(
336
- get_supported_values(pv, datatype, datatype.choices)
358
+ get_supported_values(pv, datatype, datatype.choices) # type: ignore
337
359
  )
338
360
  elif datatype and not issubclass(typ, datatype):
339
361
  # Allow int signals to represent float records when prec is 0
@@ -348,6 +370,8 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
348
370
  raise TypeError(f"{pv} has type {typ.__name__} not {datatype.__name__}")
349
371
  return PvaConverter()
350
372
  elif "NTTable" in typeid:
373
+ if is_pydantic_model(datatype):
374
+ return PvaPydanticModelConverter(datatype) # type: ignore
351
375
  return PvaTableConverter()
352
376
  elif "structure" in typeid:
353
377
  return PvaDictConverter()
@@ -356,15 +380,40 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
356
380
 
357
381
 
358
382
  class PvaSignalBackend(SignalBackend[T]):
359
- _ctxt: Optional[Context] = None
383
+ _ctxt: Context | None = None
384
+
385
+ _ALLOWED_DATATYPES = (
386
+ bool,
387
+ int,
388
+ float,
389
+ str,
390
+ Sequence,
391
+ np.ndarray,
392
+ Enum,
393
+ RuntimeSubsetEnum,
394
+ BaseModel,
395
+ dict,
396
+ )
360
397
 
361
- def __init__(self, datatype: Optional[Type[T]], read_pv: str, write_pv: str):
398
+ @classmethod
399
+ def datatype_allowed(cls, dtype: Any) -> bool:
400
+ stripped_origin = get_origin(dtype) or dtype
401
+ if dtype is None:
402
+ return True
403
+ return inspect.isclass(stripped_origin) and issubclass(
404
+ stripped_origin, cls._ALLOWED_DATATYPES
405
+ )
406
+
407
+ def __init__(self, datatype: type[T] | None, read_pv: str, write_pv: str):
362
408
  self.datatype = datatype
409
+ if not PvaSignalBackend.datatype_allowed(self.datatype):
410
+ raise TypeError(f"Given datatype {self.datatype} unsupported in PVA.")
411
+
363
412
  self.read_pv = read_pv
364
413
  self.write_pv = write_pv
365
- self.initial_values: Dict[str, Any] = {}
414
+ self.initial_values: dict[str, Any] = {}
366
415
  self.converter: PvaConverter = DisconnectedPvaConverter()
367
- self.subscription: Optional[Subscription] = None
416
+ self.subscription: Subscription | None = None
368
417
 
369
418
  def source(self, name: str):
370
419
  return f"pva://{self.read_pv}"
@@ -404,7 +453,7 @@ class PvaSignalBackend(SignalBackend[T]):
404
453
  await self._store_initial_value(self.read_pv, timeout=timeout)
405
454
  self.converter = make_converter(self.datatype, self.initial_values)
406
455
 
407
- async def put(self, value: Optional[T], wait=True, timeout=None):
456
+ async def put(self, value: T | None, wait=True, timeout=None):
408
457
  if value is None:
409
458
  write_value = self.initial_values[self.write_pv]
410
459
  else:
@@ -424,7 +473,7 @@ class PvaSignalBackend(SignalBackend[T]):
424
473
  value = await self.ctxt.get(self.read_pv)
425
474
  return self.converter.get_datakey(source, value)
426
475
 
427
- def _pva_request_string(self, fields: List[str]) -> str:
476
+ def _pva_request_string(self, fields: list[str]) -> str:
428
477
  """
429
478
  Converts a list of requested fields into a PVA request string which can be
430
479
  passed to p4p.
@@ -447,7 +496,7 @@ class PvaSignalBackend(SignalBackend[T]):
447
496
  value = await self.ctxt.get(self.write_pv, "field(value)")
448
497
  return self.converter.value(value)
449
498
 
450
- def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
499
+ def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
451
500
  if callback:
452
501
  assert (
453
502
  not self.subscription
@@ -2,8 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Optional, Tuple, Type
6
-
7
5
  from ophyd_async.core import (
8
6
  SignalBackend,
9
7
  SignalR,
@@ -19,7 +17,7 @@ from ._epics_transport import _EpicsTransport
19
17
  _default_epics_transport = _EpicsTransport.ca
20
18
 
21
19
 
22
- def _transport_pv(pv: str) -> Tuple[_EpicsTransport, str]:
20
+ def _transport_pv(pv: str) -> tuple[_EpicsTransport, str]:
23
21
  split = pv.split("://", 1)
24
22
  if len(split) > 1:
25
23
  # We got something like pva://mydevice, so use specified comms mode
@@ -32,7 +30,7 @@ def _transport_pv(pv: str) -> Tuple[_EpicsTransport, str]:
32
30
 
33
31
 
34
32
  def _epics_signal_backend(
35
- datatype: Optional[Type[T]], read_pv: str, write_pv: str
33
+ datatype: type[T] | None, read_pv: str, write_pv: str
36
34
  ) -> SignalBackend[T]:
37
35
  """Create an epics signal backend."""
38
36
  r_transport, r_pv = _transport_pv(read_pv)
@@ -42,7 +40,7 @@ def _epics_signal_backend(
42
40
 
43
41
 
44
42
  def epics_signal_rw(
45
- datatype: Type[T], read_pv: str, write_pv: Optional[str] = None, name: str = ""
43
+ datatype: type[T], read_pv: str, write_pv: str | None = None, name: str = ""
46
44
  ) -> SignalRW[T]:
47
45
  """Create a `SignalRW` backed by 1 or 2 EPICS PVs
48
46
 
@@ -60,7 +58,7 @@ def epics_signal_rw(
60
58
 
61
59
 
62
60
  def epics_signal_rw_rbv(
63
- datatype: Type[T], write_pv: str, read_suffix: str = "_RBV", name: str = ""
61
+ datatype: type[T], write_pv: str, read_suffix: str = "_RBV", name: str = ""
64
62
  ) -> SignalRW[T]:
65
63
  """Create a `SignalRW` backed by 1 or 2 EPICS PVs, with a suffix on the readback pv
66
64
 
@@ -76,7 +74,7 @@ def epics_signal_rw_rbv(
76
74
  return epics_signal_rw(datatype, f"{write_pv}{read_suffix}", write_pv, name)
77
75
 
78
76
 
79
- def epics_signal_r(datatype: Type[T], read_pv: str, name: str = "") -> SignalR[T]:
77
+ def epics_signal_r(datatype: type[T], read_pv: str, name: str = "") -> SignalR[T]:
80
78
  """Create a `SignalR` backed by 1 EPICS PV
81
79
 
82
80
  Parameters
@@ -90,7 +88,7 @@ def epics_signal_r(datatype: Type[T], read_pv: str, name: str = "") -> SignalR[T
90
88
  return SignalR(backend, name=name)
91
89
 
92
90
 
93
- def epics_signal_w(datatype: Type[T], write_pv: str, name: str = "") -> SignalW[T]:
91
+ def epics_signal_w(datatype: type[T], write_pv: str, name: str = "") -> SignalW[T]:
94
92
  """Create a `SignalW` backed by 1 EPICS PVs
95
93
 
96
94
  Parameters
@@ -15,10 +15,7 @@ from ._table import (
15
15
  DatasetTable,
16
16
  PandaHdf5DatasetType,
17
17
  SeqTable,
18
- SeqTableRow,
19
18
  SeqTrigger,
20
- seq_table_from_arrays,
21
- seq_table_from_rows,
22
19
  )
23
20
  from ._trigger import (
24
21
  PcompInfo,
@@ -45,10 +42,7 @@ __all__ = [
45
42
  "DatasetTable",
46
43
  "PandaHdf5DatasetType",
47
44
  "SeqTable",
48
- "SeqTableRow",
49
45
  "SeqTrigger",
50
- "seq_table_from_arrays",
51
- "seq_table_from_rows",
52
46
  "PcompInfo",
53
47
  "SeqTableInfo",
54
48
  "StaticPcompTriggerLogic",
@@ -7,6 +7,12 @@ from ophyd_async.core import Device, DeviceVector, SignalR, SignalRW, SubsetEnum
7
7
  from ._table import DatasetTable, SeqTable
8
8
 
9
9
 
10
+ class CaptureMode(str, Enum):
11
+ FIRST_N = "FIRST_N"
12
+ LAST_N = "LAST_N"
13
+ FOREVER = "FOREVER"
14
+
15
+
10
16
  class DataBlock(Device):
11
17
  # In future we may decide to make hdf_* optional
12
18
  hdf_directory: SignalRW[str]
@@ -15,6 +21,7 @@ class DataBlock(Device):
15
21
  num_captured: SignalR[int]
16
22
  create_directory: SignalRW[int]
17
23
  directory_exists: SignalR[bool]
24
+ capture_mode: SignalRW[CaptureMode]
18
25
  capture: SignalRW[bool]
19
26
  flush_period: SignalRW[float]
20
27
  datasets: SignalR[DatasetTable]