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
@@ -2,22 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import functools
5
- from typing import (
6
- Any,
7
- AsyncGenerator,
8
- Callable,
9
- Dict,
10
- Generic,
11
- Mapping,
12
- Optional,
13
- Tuple,
14
- Type,
15
- TypeVar,
16
- Union,
17
- )
5
+ from collections.abc import AsyncGenerator, Callable, Mapping
6
+ from typing import Any, Generic, TypeVar, cast
18
7
 
19
8
  from bluesky.protocols import (
20
- DataKey,
21
9
  Locatable,
22
10
  Location,
23
11
  Movable,
@@ -25,6 +13,7 @@ from bluesky.protocols import (
25
13
  Status,
26
14
  Subscribable,
27
15
  )
16
+ from event_model import DataKey
28
17
 
29
18
  from ._device import Device
30
19
  from ._mock_signal_backend import MockSignalBackend
@@ -32,7 +21,7 @@ from ._protocol import AsyncConfigurable, AsyncReadable, AsyncStageable
32
21
  from ._signal_backend import SignalBackend
33
22
  from ._soft_signal_backend import SignalMetadata, SoftSignalBackend
34
23
  from ._status import AsyncStatus
35
- from ._utils import DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, Callback, T
24
+ from ._utils import CALCULATE_TIMEOUT, DEFAULT_TIMEOUT, CalculatableTimeout, Callback, T
36
25
 
37
26
  S = TypeVar("S")
38
27
 
@@ -45,13 +34,26 @@ def _add_timeout(func):
45
34
  return wrapper
46
35
 
47
36
 
37
+ def _fail(*args, **kwargs):
38
+ raise RuntimeError("Signal has not been supplied a backend yet")
39
+
40
+
41
+ class DisconnectedBackend(SignalBackend):
42
+ source = connect = put = get_datakey = get_reading = get_value = get_setpoint = (
43
+ set_callback
44
+ ) = _fail
45
+
46
+
47
+ DISCONNECTED_BACKEND = DisconnectedBackend()
48
+
49
+
48
50
  class Signal(Device, Generic[T]):
49
51
  """A Device with the concept of a value, with R, RW, W and X flavours"""
50
52
 
51
53
  def __init__(
52
54
  self,
53
- backend: Optional[SignalBackend[T]] = None,
54
- timeout: Optional[float] = DEFAULT_TIMEOUT,
55
+ backend: SignalBackend[T] = DISCONNECTED_BACKEND,
56
+ timeout: float | None = DEFAULT_TIMEOUT,
55
57
  name: str = "",
56
58
  ) -> None:
57
59
  self._timeout = timeout
@@ -63,10 +65,13 @@ class Signal(Device, Generic[T]):
63
65
  mock=False,
64
66
  timeout=DEFAULT_TIMEOUT,
65
67
  force_reconnect: bool = False,
66
- backend: Optional[SignalBackend[T]] = None,
68
+ backend: SignalBackend[T] | None = None,
67
69
  ):
68
70
  if backend:
69
- if self._backend and backend is not self._backend:
71
+ if (
72
+ self._backend is not DISCONNECTED_BACKEND
73
+ and backend is not self._backend
74
+ ):
70
75
  raise ValueError("Backend at connection different from previous one.")
71
76
 
72
77
  self._backend = backend
@@ -114,10 +119,10 @@ class _SignalCache(Generic[T]):
114
119
  def __init__(self, backend: SignalBackend[T], signal: Signal):
115
120
  self._signal = signal
116
121
  self._staged = False
117
- self._listeners: Dict[Callback, bool] = {}
122
+ self._listeners: dict[Callback, bool] = {}
118
123
  self._valid = asyncio.Event()
119
- self._reading: Optional[Reading] = None
120
- self._value: Optional[T] = None
124
+ self._reading: Reading | None = None
125
+ self._value: T | None = None
121
126
 
122
127
  self.backend = backend
123
128
  signal.log.debug(f"Making subscription on source {signal.source}")
@@ -171,11 +176,9 @@ class _SignalCache(Generic[T]):
171
176
  class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
172
177
  """Signal that can be read from and monitored"""
173
178
 
174
- _cache: Optional[_SignalCache] = None
179
+ _cache: _SignalCache | None = None
175
180
 
176
- def _backend_or_cache(
177
- self, cached: Optional[bool]
178
- ) -> Union[_SignalCache, SignalBackend]:
181
+ def _backend_or_cache(self, cached: bool | None) -> _SignalCache | SignalBackend:
179
182
  # If cached is None then calculate it based on whether we already have a cache
180
183
  if cached is None:
181
184
  cached = self._cache is not None
@@ -196,17 +199,17 @@ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
196
199
  self._cache = None
197
200
 
198
201
  @_add_timeout
199
- async def read(self, cached: Optional[bool] = None) -> Dict[str, Reading]:
202
+ async def read(self, cached: bool | None = None) -> dict[str, Reading]:
200
203
  """Return a single item dict with the reading in it"""
201
204
  return {self.name: await self._backend_or_cache(cached).get_reading()}
202
205
 
203
206
  @_add_timeout
204
- async def describe(self) -> Dict[str, DataKey]:
207
+ async def describe(self) -> dict[str, DataKey]:
205
208
  """Return a single item dict with the descriptor in it"""
206
209
  return {self.name: await self._backend.get_datakey(self.source)}
207
210
 
208
211
  @_add_timeout
209
- async def get_value(self, cached: Optional[bool] = None) -> T:
212
+ async def get_value(self, cached: bool | None = None) -> T:
210
213
  """The current value"""
211
214
  value = await self._backend_or_cache(cached).get_value()
212
215
  self.log.debug(f"get_value() on source {self.source} returned {value}")
@@ -216,7 +219,7 @@ class SignalR(Signal[T], AsyncReadable, AsyncStageable, Subscribable):
216
219
  """Subscribe to updates in value of a device"""
217
220
  self._get_cache().subscribe(function, want_value=True)
218
221
 
219
- def subscribe(self, function: Callback[Dict[str, Reading]]) -> None:
222
+ def subscribe(self, function: Callback[dict[str, Reading]]) -> None:
220
223
  """Subscribe to updates in the reading"""
221
224
  self._get_cache().subscribe(function, want_value=False)
222
225
 
@@ -239,10 +242,10 @@ class SignalW(Signal[T], Movable):
239
242
  """Signal that can be set"""
240
243
 
241
244
  def set(
242
- self, value: T, wait=True, timeout: CalculatableTimeout = CalculateTimeout
245
+ self, value: T, wait=True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
243
246
  ) -> AsyncStatus:
244
247
  """Set the value and return a status saying when it's done"""
245
- if timeout is CalculateTimeout:
248
+ if timeout is CALCULATE_TIMEOUT:
246
249
  timeout = self._timeout
247
250
 
248
251
  async def do_set():
@@ -270,18 +273,18 @@ class SignalX(Signal):
270
273
  """Signal that puts the default value"""
271
274
 
272
275
  def trigger(
273
- self, wait=True, timeout: CalculatableTimeout = CalculateTimeout
276
+ self, wait=True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
274
277
  ) -> AsyncStatus:
275
278
  """Trigger the action and return a status saying when it's done"""
276
- if timeout is CalculateTimeout:
279
+ if timeout is CALCULATE_TIMEOUT:
277
280
  timeout = self._timeout
278
281
  coro = self._backend.put(None, wait=wait, timeout=timeout)
279
282
  return AsyncStatus(coro)
280
283
 
281
284
 
282
285
  def soft_signal_rw(
283
- datatype: Optional[Type[T]] = None,
284
- initial_value: Optional[T] = None,
286
+ datatype: type[T] | None = None,
287
+ initial_value: T | None = None,
285
288
  name: str = "",
286
289
  units: str | None = None,
287
290
  precision: int | None = None,
@@ -298,12 +301,12 @@ def soft_signal_rw(
298
301
 
299
302
 
300
303
  def soft_signal_r_and_setter(
301
- datatype: Optional[Type[T]] = None,
302
- initial_value: Optional[T] = None,
304
+ datatype: type[T] | None = None,
305
+ initial_value: T | None = None,
303
306
  name: str = "",
304
307
  units: str | None = None,
305
308
  precision: int | None = None,
306
- ) -> Tuple[SignalR[T], Callable[[T], None]]:
309
+ ) -> tuple[SignalR[T], Callable[[T], None]]:
307
310
  """Returns a tuple of a read-only Signal and a callable through
308
311
  which the signal can be internally modified within the device.
309
312
  May pass metadata, which are propagated into describe.
@@ -316,9 +319,7 @@ def soft_signal_r_and_setter(
316
319
  return (signal, backend.set_value)
317
320
 
318
321
 
319
- def _generate_assert_error_msg(
320
- name: str, expected_result: str, actual_result: str
321
- ) -> str:
322
+ def _generate_assert_error_msg(name: str, expected_result, actual_result) -> str:
322
323
  WARNING = "\033[93m"
323
324
  FAIL = "\033[91m"
324
325
  ENDC = "\033[0m"
@@ -484,14 +485,14 @@ async def observe_value(
484
485
  else:
485
486
  break
486
487
  else:
487
- yield item
488
+ yield cast(T, item)
488
489
  finally:
489
490
  signal.clear_sub(q.put_nowait)
490
491
 
491
492
 
492
493
  class _ValueChecker(Generic[T]):
493
494
  def __init__(self, matcher: Callable[[T], bool], matcher_name: str):
494
- self._last_value: Optional[T] = None
495
+ self._last_value: T | None = None
495
496
  self._matcher = matcher
496
497
  self._matcher_name = matcher_name
497
498
 
@@ -501,11 +502,11 @@ class _ValueChecker(Generic[T]):
501
502
  if self._matcher(value):
502
503
  return
503
504
 
504
- async def wait_for_value(self, signal: SignalR[T], timeout: Optional[float]):
505
+ async def wait_for_value(self, signal: SignalR[T], timeout: float | None):
505
506
  try:
506
507
  await asyncio.wait_for(self._wait_for_value(signal), timeout)
507
508
  except asyncio.TimeoutError as e:
508
- raise TimeoutError(
509
+ raise asyncio.TimeoutError(
509
510
  f"{signal.name} didn't match {self._matcher_name} in {timeout}s, "
510
511
  f"last value {self._last_value!r}"
511
512
  ) from e
@@ -513,8 +514,8 @@ class _ValueChecker(Generic[T]):
513
514
 
514
515
  async def wait_for_value(
515
516
  signal: SignalR[T],
516
- match: Union[T, Callable[[T], bool]],
517
- timeout: Optional[float],
517
+ match: T | Callable[[T], bool],
518
+ timeout: float | None,
518
519
  ):
519
520
  """Wait for a signal to have a matching value.
520
521
 
@@ -540,7 +541,7 @@ async def wait_for_value(
540
541
  wait_for_value(device.num_captured, lambda v: v > 45, timeout=1)
541
542
  """
542
543
  if callable(match):
543
- checker = _ValueChecker(match, match.__name__)
544
+ checker = _ValueChecker(match, match.__name__) # type: ignore
544
545
  else:
545
546
  checker = _ValueChecker(lambda v: v == match, repr(match))
546
547
  await checker.wait_for_value(signal, timeout)
@@ -552,7 +553,7 @@ async def set_and_wait_for_other_value(
552
553
  read_signal: SignalR[S],
553
554
  read_value: S,
554
555
  timeout: float = DEFAULT_TIMEOUT,
555
- set_timeout: Optional[float] = None,
556
+ set_timeout: float | None = None,
556
557
  ) -> AsyncStatus:
557
558
  """Set a signal and monitor another signal until it has the specified value.
558
559
 
@@ -610,7 +611,7 @@ async def set_and_wait_for_value(
610
611
  signal: SignalRW[T],
611
612
  value: T,
612
613
  timeout: float = DEFAULT_TIMEOUT,
613
- status_timeout: Optional[float] = None,
614
+ status_timeout: float | None = None,
614
615
  ) -> AsyncStatus:
615
616
  """Set a signal and monitor it until it has that value.
616
617
 
@@ -1,7 +1,15 @@
1
1
  from abc import abstractmethod
2
- from typing import TYPE_CHECKING, ClassVar, Generic, Literal, Optional, Tuple, Type
2
+ from typing import (
3
+ TYPE_CHECKING,
4
+ Any,
5
+ ClassVar,
6
+ Generic,
7
+ Literal,
8
+ )
9
+
10
+ from bluesky.protocols import Reading
11
+ from event_model import DataKey
3
12
 
4
- from ._protocol import DataKey, Reading
5
13
  from ._utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
6
14
 
7
15
 
@@ -9,7 +17,12 @@ class SignalBackend(Generic[T]):
9
17
  """A read/write/monitor backend for a Signals"""
10
18
 
11
19
  #: Datatype of the signal value
12
- datatype: Optional[Type[T]] = None
20
+ datatype: type[T] | None = None
21
+
22
+ @classmethod
23
+ @abstractmethod
24
+ def datatype_allowed(cls, dtype: Any) -> bool:
25
+ """Check if a given datatype is acceptable for this signal backend."""
13
26
 
14
27
  #: Like ca://PV_PREFIX:SIGNAL
15
28
  @abstractmethod
@@ -22,7 +35,7 @@ class SignalBackend(Generic[T]):
22
35
  """Connect to underlying hardware"""
23
36
 
24
37
  @abstractmethod
25
- async def put(self, value: Optional[T], wait=True, timeout=None):
38
+ async def put(self, value: T | None, wait=True, timeout=None):
26
39
  """Put a value to the PV, if wait then wait for completion for up to timeout"""
27
40
 
28
41
  @abstractmethod
@@ -42,14 +55,14 @@ class SignalBackend(Generic[T]):
42
55
  """The point that a signal was requested to move to."""
43
56
 
44
57
  @abstractmethod
45
- def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
58
+ def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
46
59
  """Observe changes to the current value, timestamp and severity"""
47
60
 
48
61
 
49
62
  class _RuntimeSubsetEnumMeta(type):
50
63
  def __str__(cls):
51
64
  if hasattr(cls, "choices"):
52
- return f"SubsetEnum{list(cls.choices)}"
65
+ return f"SubsetEnum{list(cls.choices)}" # type: ignore
53
66
  return "SubsetEnum"
54
67
 
55
68
  def __getitem__(cls, _choices):
@@ -72,7 +85,7 @@ class _RuntimeSubsetEnumMeta(type):
72
85
 
73
86
 
74
87
  class RuntimeSubsetEnum(metaclass=_RuntimeSubsetEnumMeta):
75
- choices: ClassVar[Tuple[str, ...]]
88
+ choices: ClassVar[tuple[str, ...]]
76
89
 
77
90
  def __init__(self):
78
91
  raise RuntimeError("SubsetEnum cannot be instantiated")
@@ -4,16 +4,28 @@ import inspect
4
4
  import time
5
5
  from collections import abc
6
6
  from enum import Enum
7
- from typing import Dict, Generic, Optional, Tuple, Type, Union, cast, get_origin
7
+ from typing import Generic, cast, get_origin
8
8
 
9
9
  import numpy as np
10
- from bluesky.protocols import DataKey, Dtype, Reading
10
+ from bluesky.protocols import Reading
11
+ from event_model import DataKey
12
+ from event_model.documents.event_descriptor import Dtype
13
+ from pydantic import BaseModel
11
14
  from typing_extensions import TypedDict
12
15
 
13
- from ._signal_backend import RuntimeSubsetEnum, SignalBackend
14
- from ._utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype
15
-
16
- primitive_dtypes: Dict[type, Dtype] = {
16
+ from ._signal_backend import (
17
+ RuntimeSubsetEnum,
18
+ SignalBackend,
19
+ )
20
+ from ._utils import (
21
+ DEFAULT_TIMEOUT,
22
+ ReadingValueCallback,
23
+ T,
24
+ get_dtype,
25
+ is_pydantic_model,
26
+ )
27
+
28
+ primitive_dtypes: dict[type, Dtype] = {
17
29
  str: "string",
18
30
  int: "integer",
19
31
  float: "number",
@@ -22,8 +34,8 @@ primitive_dtypes: Dict[type, Dtype] = {
22
34
 
23
35
 
24
36
  class SignalMetadata(TypedDict):
25
- units: str | None = None
26
- precision: int | None = None
37
+ units: str | None
38
+ precision: int | None
27
39
 
28
40
 
29
41
  class SoftConverter(Generic[T]):
@@ -41,7 +53,7 @@ class SoftConverter(Generic[T]):
41
53
  )
42
54
 
43
55
  def get_datakey(self, source: str, value, **metadata) -> DataKey:
44
- dk = {"source": source, "shape": [], **metadata}
56
+ dk: DataKey = {"source": source, "shape": [], **metadata} # type: ignore
45
57
  dtype = type(value)
46
58
  if np.issubdtype(dtype, np.integer):
47
59
  dtype = int
@@ -51,13 +63,14 @@ class SoftConverter(Generic[T]):
51
63
  dtype in primitive_dtypes
52
64
  ), f"invalid converter for value of type {type(value)}"
53
65
  dk["dtype"] = primitive_dtypes[dtype]
66
+ # type ignore until https://github.com/bluesky/event-model/issues/308
54
67
  try:
55
- dk["dtype_numpy"] = np.dtype(dtype).descr[0][1]
68
+ dk["dtype_numpy"] = np.dtype(dtype).descr[0][1] # type: ignore
56
69
  except TypeError:
57
- dk["dtype_numpy"] = ""
70
+ dk["dtype_numpy"] = "" # type: ignore
58
71
  return dk
59
72
 
60
- def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
73
+ def make_initial_value(self, datatype: type[T] | None) -> T:
61
74
  if datatype is None:
62
75
  return cast(T, None)
63
76
 
@@ -76,12 +89,12 @@ class SoftArrayConverter(SoftConverter):
76
89
  return {
77
90
  "source": source,
78
91
  "dtype": "array",
79
- "dtype_numpy": dtype_numpy,
92
+ "dtype_numpy": dtype_numpy, # type: ignore
80
93
  "shape": [len(value)],
81
94
  **metadata,
82
95
  }
83
96
 
84
- def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
97
+ def make_initial_value(self, datatype: type[T] | None) -> T:
85
98
  if datatype is None:
86
99
  return cast(T, None)
87
100
 
@@ -92,28 +105,29 @@ class SoftArrayConverter(SoftConverter):
92
105
 
93
106
 
94
107
  class SoftEnumConverter(SoftConverter):
95
- choices: Tuple[str, ...]
108
+ choices: tuple[str, ...]
96
109
 
97
- def __init__(self, datatype: Union[RuntimeSubsetEnum, Enum]):
98
- if issubclass(datatype, Enum):
110
+ def __init__(self, datatype: RuntimeSubsetEnum | type[Enum]):
111
+ if issubclass(datatype, Enum): # type: ignore
99
112
  self.choices = tuple(v.value for v in datatype)
100
113
  else:
101
114
  self.choices = datatype.choices
102
115
 
103
- def write_value(self, value: Union[Enum, str]) -> str:
104
- return value
116
+ def write_value(self, value: Enum | str) -> str:
117
+ return value # type: ignore
105
118
 
106
119
  def get_datakey(self, source: str, value, **metadata) -> DataKey:
107
120
  return {
108
121
  "source": source,
109
122
  "dtype": "string",
110
- "dtype_numpy": "|S40",
123
+ # type ignore until https://github.com/bluesky/event-model/issues/308
124
+ "dtype_numpy": "|S40", # type: ignore
111
125
  "shape": [],
112
126
  "choices": self.choices,
113
127
  **metadata,
114
128
  }
115
129
 
116
- def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
130
+ def make_initial_value(self, datatype: type[T] | None) -> T:
117
131
  if datatype is None:
118
132
  return cast(T, None)
119
133
 
@@ -122,6 +136,16 @@ class SoftEnumConverter(SoftConverter):
122
136
  return cast(T, self.choices[0])
123
137
 
124
138
 
139
+ class SoftPydanticModelConverter(SoftConverter):
140
+ def __init__(self, datatype: type[BaseModel]):
141
+ self.datatype = datatype
142
+
143
+ def write_value(self, value):
144
+ if isinstance(value, dict):
145
+ return self.datatype(**value)
146
+ return value
147
+
148
+
125
149
  def make_converter(datatype):
126
150
  is_array = get_dtype(datatype) is not None
127
151
  is_sequence = get_origin(datatype) == abc.Sequence
@@ -132,7 +156,9 @@ def make_converter(datatype):
132
156
  if is_array or is_sequence:
133
157
  return SoftArrayConverter()
134
158
  if is_enum:
135
- return SoftEnumConverter(datatype)
159
+ return SoftEnumConverter(datatype) # type: ignore
160
+ if is_pydantic_model(datatype):
161
+ return SoftPydanticModelConverter(datatype) # type: ignore
136
162
 
137
163
  return SoftConverter()
138
164
 
@@ -141,15 +167,19 @@ class SoftSignalBackend(SignalBackend[T]):
141
167
  """An backend to a soft Signal, for test signals see ``MockSignalBackend``."""
142
168
 
143
169
  _value: T
144
- _initial_value: Optional[T]
170
+ _initial_value: T | None
145
171
  _timestamp: float
146
172
  _severity: int
147
173
 
174
+ @classmethod
175
+ def datatype_allowed(cls, dtype: type) -> bool:
176
+ return True # Any value allowed in a soft signal
177
+
148
178
  def __init__(
149
179
  self,
150
- datatype: Optional[Type[T]],
151
- initial_value: Optional[T] = None,
152
- metadata: SignalMetadata = None,
180
+ datatype: type[T] | None,
181
+ initial_value: T | None = None,
182
+ metadata: SignalMetadata = None, # type: ignore
153
183
  ) -> None:
154
184
  self.datatype = datatype
155
185
  self._initial_value = initial_value
@@ -158,11 +188,11 @@ class SoftSignalBackend(SignalBackend[T]):
158
188
  if self._initial_value is None:
159
189
  self._initial_value = self.converter.make_initial_value(self.datatype)
160
190
  else:
161
- self._initial_value = self.converter.write_value(self._initial_value)
191
+ self._initial_value = self.converter.write_value(self._initial_value) # type: ignore
162
192
 
163
- self.callback: Optional[ReadingValueCallback[T]] = None
193
+ self.callback: ReadingValueCallback[T] | None = None
164
194
  self._severity = 0
165
- self.set_value(self._initial_value)
195
+ self.set_value(self._initial_value) # type: ignore
166
196
 
167
197
  def source(self, name: str) -> str:
168
198
  return f"soft://{name}"
@@ -171,14 +201,14 @@ class SoftSignalBackend(SignalBackend[T]):
171
201
  """Connection isn't required for soft signals."""
172
202
  pass
173
203
 
174
- async def put(self, value: Optional[T], wait=True, timeout=None):
204
+ async def put(self, value: T | None, wait=True, timeout=None):
175
205
  write_value = (
176
206
  self.converter.write_value(value)
177
207
  if value is not None
178
208
  else self._initial_value
179
209
  )
180
210
 
181
- self.set_value(write_value)
211
+ self.set_value(write_value) # type: ignore
182
212
 
183
213
  def set_value(self, value: T):
184
214
  """Method to bypass asynchronous logic."""
@@ -204,7 +234,7 @@ class SoftSignalBackend(SignalBackend[T]):
204
234
  """For a soft signal, the setpoint and readback values are the same."""
205
235
  return await self.get_value()
206
236
 
207
- def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
237
+ def set_callback(self, callback: ReadingValueCallback[T] | None) -> None:
208
238
  if callback:
209
239
  assert not self.callback, "Cannot set a callback when one is already set"
210
240
  reading: Reading = self.converter.reading(
@@ -3,14 +3,10 @@
3
3
  import asyncio
4
4
  import functools
5
5
  import time
6
+ from collections.abc import AsyncIterator, Callable, Coroutine
6
7
  from dataclasses import asdict, replace
7
8
  from typing import (
8
- AsyncIterator,
9
- Awaitable,
10
- Callable,
11
9
  Generic,
12
- Optional,
13
- Type,
14
10
  TypeVar,
15
11
  cast,
16
12
  )
@@ -27,7 +23,7 @@ WAS = TypeVar("WAS", bound="WatchableAsyncStatus")
27
23
  class AsyncStatusBase(Status):
28
24
  """Convert asyncio awaitable to bluesky Status interface"""
29
25
 
30
- def __init__(self, awaitable: Awaitable):
26
+ def __init__(self, awaitable: Coroutine | asyncio.Task):
31
27
  if isinstance(awaitable, asyncio.Task):
32
28
  self.task = awaitable
33
29
  else:
@@ -86,8 +82,10 @@ class AsyncStatusBase(Status):
86
82
 
87
83
 
88
84
  class AsyncStatus(AsyncStatusBase):
85
+ """Convert asyncio awaitable to bluesky Status interface"""
86
+
89
87
  @classmethod
90
- def wrap(cls: Type[AS], f: Callable[P, Awaitable]) -> Callable[P, AS]:
88
+ def wrap(cls: type[AS], f: Callable[P, Coroutine]) -> Callable[P, AS]:
91
89
  """Wrap an async function in an AsyncStatus."""
92
90
 
93
91
  @functools.wraps(f)
@@ -131,7 +129,7 @@ class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
131
129
 
132
130
  @classmethod
133
131
  def wrap(
134
- cls: Type[WAS],
132
+ cls: type[WAS],
135
133
  f: Callable[P, AsyncIterator[WatcherUpdate[T]]],
136
134
  ) -> Callable[P, WAS]:
137
135
  """Wrap an AsyncIterator in a WatchableAsyncStatus."""
@@ -144,7 +142,7 @@ class WatchableAsyncStatus(AsyncStatusBase, Generic[T]):
144
142
 
145
143
 
146
144
  @AsyncStatus.wrap
147
- async def completed_status(exception: Optional[Exception] = None):
145
+ async def completed_status(exception: Exception | None = None):
148
146
  if exception:
149
147
  raise exception
150
148
  return None