ophyd-async 0.5.2__py3-none-any.whl → 0.7.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 (79) 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 +13 -15
  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/_control.py +16 -17
  48. ophyd_async/fastcs/panda/_hdf_panda.py +11 -4
  49. ophyd_async/fastcs/panda/_table.py +77 -138
  50. ophyd_async/fastcs/panda/_trigger.py +4 -5
  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 +15 -17
  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 +27 -21
  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/tango/__init__.py +45 -0
  61. ophyd_async/tango/base_devices/__init__.py +4 -0
  62. ophyd_async/tango/base_devices/_base_device.py +225 -0
  63. ophyd_async/tango/base_devices/_tango_readable.py +33 -0
  64. ophyd_async/tango/demo/__init__.py +12 -0
  65. ophyd_async/tango/demo/_counter.py +37 -0
  66. ophyd_async/tango/demo/_detector.py +42 -0
  67. ophyd_async/tango/demo/_mover.py +77 -0
  68. ophyd_async/tango/demo/_tango/__init__.py +3 -0
  69. ophyd_async/tango/demo/_tango/_servers.py +108 -0
  70. ophyd_async/tango/signal/__init__.py +39 -0
  71. ophyd_async/tango/signal/_signal.py +223 -0
  72. ophyd_async/tango/signal/_tango_transport.py +764 -0
  73. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/METADATA +50 -45
  74. ophyd_async-0.7.0a1.dist-info/RECORD +108 -0
  75. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/WHEEL +1 -1
  76. ophyd_async-0.5.2.dist-info/RECORD +0 -95
  77. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/LICENSE +0 -0
  78. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/entry_points.txt +0 -0
  79. {ophyd_async-0.5.2.dist-info → ophyd_async-0.7.0a1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- from typing import Sequence
1
+ from collections.abc import Sequence
2
2
 
3
3
  from ophyd_async.core import PathProvider, SignalR, StandardDetector
4
4
  from ophyd_async.epics import adcore
@@ -12,14 +12,15 @@ class SimDetector(StandardDetector):
12
12
 
13
13
  def __init__(
14
14
  self,
15
- drv: adcore.ADBaseIO,
16
- hdf: adcore.NDFileHDFIO,
15
+ prefix: str,
17
16
  path_provider: PathProvider,
17
+ drv_suffix="cam1:",
18
+ hdf_suffix="HDF1:",
18
19
  name: str = "",
19
20
  config_sigs: Sequence[SignalR] = (),
20
21
  ):
21
- self.drv = drv
22
- self.hdf = hdf
22
+ self.drv = adcore.ADBaseIO(prefix + drv_suffix)
23
+ self.hdf = adcore.NDFileHDFIO(prefix + hdf_suffix)
23
24
 
24
25
  super().__init__(
25
26
  SimController(self.drv),
@@ -29,6 +30,6 @@ class SimDetector(StandardDetector):
29
30
  lambda: self.name,
30
31
  adcore.ADBaseDatasetDescriber(self.drv),
31
32
  ),
32
- config_sigs=config_sigs,
33
+ config_sigs=(self.drv.acquire_period, self.drv.acquire_time, *config_sigs),
33
34
  name=name,
34
35
  )
@@ -1,45 +1,50 @@
1
1
  import asyncio
2
- from typing import Optional, Set
3
2
 
4
3
  from ophyd_async.core import (
5
4
  DEFAULT_TIMEOUT,
6
- AsyncStatus,
7
- DetectorControl,
5
+ DetectorController,
8
6
  DetectorTrigger,
9
7
  )
8
+ from ophyd_async.core._detector import TriggerInfo
9
+ from ophyd_async.core._status import AsyncStatus
10
10
  from ophyd_async.epics import adcore
11
11
 
12
12
 
13
- class SimController(DetectorControl):
13
+ class SimController(DetectorController):
14
14
  def __init__(
15
15
  self,
16
16
  driver: adcore.ADBaseIO,
17
- good_states: Set[adcore.DetectorState] = set(adcore.DEFAULT_GOOD_STATES),
17
+ good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
18
18
  ) -> None:
19
19
  self.driver = driver
20
20
  self.good_states = good_states
21
+ self.frame_timeout: float
22
+ self._arm_status: AsyncStatus | None = None
21
23
 
22
- def get_deadtime(self, exposure: float) -> float:
24
+ def get_deadtime(self, exposure: float | None) -> float:
23
25
  return 0.002
24
26
 
25
- async def arm(
26
- self,
27
- num: int,
28
- trigger: DetectorTrigger = DetectorTrigger.internal,
29
- exposure: Optional[float] = None,
30
- ) -> AsyncStatus:
27
+ async def prepare(self, trigger_info: TriggerInfo):
31
28
  assert (
32
- trigger == DetectorTrigger.internal
29
+ trigger_info.trigger == DetectorTrigger.internal
33
30
  ), "fly scanning (i.e. external triggering) is not supported for this device"
34
- frame_timeout = DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
31
+ self.frame_timeout = (
32
+ DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
33
+ )
35
34
  await asyncio.gather(
36
- self.driver.num_images.set(num),
35
+ self.driver.num_images.set(trigger_info.total_number_of_triggers),
37
36
  self.driver.image_mode.set(adcore.ImageMode.multiple),
38
37
  )
39
- return await adcore.start_acquiring_driver_and_ensure_status(
40
- self.driver, good_states=self.good_states, timeout=frame_timeout
38
+
39
+ async def arm(self):
40
+ self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
41
+ self.driver, good_states=self.good_states, timeout=self.frame_timeout
41
42
  )
42
43
 
44
+ async def wait_for_idle(self):
45
+ if self._arm_status:
46
+ await self._arm_status
47
+
43
48
  async def disarm(self):
44
49
  # We can't use caput callback as we already used it in arm() and we can't have
45
50
  # 2 or they will deadlock
@@ -1,7 +1,8 @@
1
1
  import asyncio
2
- from typing import Optional
3
2
 
4
- from ophyd_async.core import AsyncStatus, DetectorControl, DetectorTrigger
3
+ from ophyd_async.core import DetectorController, DetectorTrigger
4
+ from ophyd_async.core._detector import TriggerInfo
5
+ from ophyd_async.core._status import AsyncStatus
5
6
  from ophyd_async.epics import adcore
6
7
 
7
8
  from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
@@ -21,38 +22,42 @@ EXPOSE_OUT_MODE = {
21
22
  }
22
23
 
23
24
 
24
- class VimbaController(DetectorControl):
25
+ class VimbaController(DetectorController):
25
26
  def __init__(
26
27
  self,
27
28
  driver: VimbaDriverIO,
28
29
  ) -> None:
29
30
  self._drv = driver
31
+ self._arm_status: AsyncStatus | None = None
30
32
 
31
- def get_deadtime(self, exposure: float) -> float:
33
+ def get_deadtime(self, exposure: float | None) -> float:
32
34
  return 0.001
33
35
 
34
- async def arm(
35
- self,
36
- num: int,
37
- trigger: DetectorTrigger = DetectorTrigger.internal,
38
- exposure: Optional[float] = None,
39
- ) -> AsyncStatus:
36
+ async def prepare(self, trigger_info: TriggerInfo):
40
37
  await asyncio.gather(
41
- self._drv.trigger_mode.set(TRIGGER_MODE[trigger]),
42
- self._drv.exposure_mode.set(EXPOSE_OUT_MODE[trigger]),
43
- self._drv.num_images.set(num),
38
+ self._drv.trigger_mode.set(TRIGGER_MODE[trigger_info.trigger]),
39
+ self._drv.exposure_mode.set(EXPOSE_OUT_MODE[trigger_info.trigger]),
40
+ self._drv.num_images.set(trigger_info.total_number_of_triggers),
44
41
  self._drv.image_mode.set(adcore.ImageMode.multiple),
45
42
  )
46
- if exposure is not None and trigger not in [
43
+ if trigger_info.livetime is not None and trigger_info.trigger not in [
47
44
  DetectorTrigger.variable_gate,
48
45
  DetectorTrigger.constant_gate,
49
46
  ]:
50
- await self._drv.acquire_time.set(exposure)
51
- if trigger != DetectorTrigger.internal:
47
+ await self._drv.acquire_time.set(trigger_info.livetime)
48
+ if trigger_info.trigger != DetectorTrigger.internal:
52
49
  self._drv.trigger_source.set(VimbaTriggerSource.line1)
53
50
  else:
54
51
  self._drv.trigger_source.set(VimbaTriggerSource.freerun)
55
- return await adcore.start_acquiring_driver_and_ensure_status(self._drv)
52
+
53
+ async def arm(self):
54
+ self._arm_status = await adcore.start_acquiring_driver_and_ensure_status(
55
+ self._drv
56
+ )
57
+
58
+ async def wait_for_idle(self):
59
+ if self._arm_status:
60
+ await self._arm_status
56
61
 
57
62
  async def disarm(self):
58
63
  await adcore.stop_busy_record(self._drv.acquire, False, timeout=1)
@@ -4,10 +4,10 @@ import numpy as np
4
4
  from bluesky.protocols import Movable, Stoppable
5
5
 
6
6
  from ophyd_async.core import (
7
+ CALCULATE_TIMEOUT,
7
8
  DEFAULT_TIMEOUT,
8
9
  AsyncStatus,
9
10
  CalculatableTimeout,
10
- CalculateTimeout,
11
11
  ConfigSignal,
12
12
  Device,
13
13
  HintedSignal,
@@ -44,9 +44,8 @@ class Mover(StandardReadable, Movable, Stoppable):
44
44
  self.readback.set_name(name)
45
45
 
46
46
  @WatchableAsyncStatus.wrap
47
- async def set(
48
- self, new_position: float, timeout: CalculatableTimeout = CalculateTimeout
49
- ):
47
+ async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT):
48
+ new_position = value
50
49
  self._set_success = True
51
50
  old_position, units, precision, velocity = await asyncio.gather(
52
51
  self.setpoint.get_value(),
@@ -54,7 +53,7 @@ class Mover(StandardReadable, Movable, Stoppable):
54
53
  self.precision.get_value(),
55
54
  self.velocity.get_value(),
56
55
  )
57
- if timeout is CalculateTimeout:
56
+ if timeout == CALCULATE_TIMEOUT:
58
57
  assert velocity > 0, "Mover has zero velocity"
59
58
  timeout = abs(new_position - old_position) / velocity + DEFAULT_TIMEOUT
60
59
  # Make an Event that will be set on completion, and a Status that will
@@ -17,4 +17,3 @@ record(calc, "$(P)Value") {
17
17
  field(EGU, "$(EGU=cts/s)")
18
18
  field(PREC, "$(PREC=3)")
19
19
  }
20
-
@@ -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
- DetectorControl,
5
+ DetectorController,
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
 
@@ -19,14 +18,14 @@ EIGER_TRIGGER_MODE_MAP = {
19
18
  }
20
19
 
21
20
 
22
- class EigerController(DetectorControl):
21
+ class EigerController(DetectorController):
23
22
  def __init__(
24
23
  self,
25
24
  driver: EigerDriverIO,
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.total_number_of_triggers),
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():