ophyd-async 0.7.0a1__py3-none-any.whl → 0.8.0a2__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 (70) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +23 -8
  3. ophyd_async/core/_detector.py +5 -10
  4. ophyd_async/core/_device.py +145 -67
  5. ophyd_async/core/_device_filler.py +191 -0
  6. ophyd_async/core/_device_save_loader.py +6 -7
  7. ophyd_async/core/_mock_signal_backend.py +32 -40
  8. ophyd_async/core/_mock_signal_utils.py +22 -16
  9. ophyd_async/core/_protocol.py +28 -8
  10. ophyd_async/core/_readable.py +5 -5
  11. ophyd_async/core/_signal.py +140 -152
  12. ophyd_async/core/_signal_backend.py +131 -64
  13. ophyd_async/core/_soft_signal_backend.py +125 -194
  14. ophyd_async/core/_status.py +22 -6
  15. ophyd_async/core/_table.py +97 -100
  16. ophyd_async/core/_utils.py +71 -18
  17. ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
  18. ophyd_async/epics/adaravis/_aravis_io.py +7 -5
  19. ophyd_async/epics/adcore/_core_io.py +4 -6
  20. ophyd_async/epics/adcore/_hdf_writer.py +2 -2
  21. ophyd_async/epics/adcore/_utils.py +15 -10
  22. ophyd_async/epics/adkinetix/__init__.py +2 -1
  23. ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
  24. ophyd_async/epics/adkinetix/_kinetix_io.py +3 -4
  25. ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
  26. ophyd_async/epics/adpilatus/_pilatus_io.py +2 -3
  27. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  28. ophyd_async/epics/advimba/__init__.py +4 -1
  29. ophyd_async/epics/advimba/_vimba_controller.py +6 -3
  30. ophyd_async/epics/advimba/_vimba_io.py +7 -8
  31. ophyd_async/epics/demo/_sensor.py +8 -4
  32. ophyd_async/epics/eiger/_eiger.py +1 -2
  33. ophyd_async/epics/eiger/_eiger_controller.py +1 -1
  34. ophyd_async/epics/eiger/_eiger_io.py +2 -4
  35. ophyd_async/epics/eiger/_odin_io.py +4 -4
  36. ophyd_async/epics/pvi/__init__.py +2 -2
  37. ophyd_async/epics/pvi/_pvi.py +56 -321
  38. ophyd_async/epics/signal/__init__.py +3 -4
  39. ophyd_async/epics/signal/_aioca.py +184 -236
  40. ophyd_async/epics/signal/_common.py +35 -49
  41. ophyd_async/epics/signal/_p4p.py +254 -387
  42. ophyd_async/epics/signal/_signal.py +63 -21
  43. ophyd_async/fastcs/core.py +9 -0
  44. ophyd_async/fastcs/panda/__init__.py +4 -4
  45. ophyd_async/fastcs/panda/_block.py +23 -11
  46. ophyd_async/fastcs/panda/_control.py +3 -5
  47. ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
  48. ophyd_async/fastcs/panda/_table.py +29 -51
  49. ophyd_async/fastcs/panda/_trigger.py +8 -8
  50. ophyd_async/fastcs/panda/_writer.py +4 -7
  51. ophyd_async/plan_stubs/_ensure_connected.py +3 -1
  52. ophyd_async/plan_stubs/_fly.py +2 -2
  53. ophyd_async/plan_stubs/_nd_attributes.py +5 -4
  54. ophyd_async/py.typed +0 -0
  55. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
  56. ophyd_async/tango/__init__.py +2 -4
  57. ophyd_async/tango/base_devices/_base_device.py +76 -143
  58. ophyd_async/tango/demo/_counter.py +2 -2
  59. ophyd_async/tango/demo/_mover.py +2 -2
  60. ophyd_async/tango/signal/__init__.py +2 -4
  61. ophyd_async/tango/signal/_signal.py +29 -50
  62. ophyd_async/tango/signal/_tango_transport.py +38 -40
  63. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a2.dist-info}/METADATA +8 -12
  64. ophyd_async-0.8.0a2.dist-info/RECORD +110 -0
  65. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a2.dist-info}/WHEEL +1 -1
  66. ophyd_async/epics/signal/_epics_transport.py +0 -34
  67. ophyd_async-0.7.0a1.dist-info/RECORD +0 -108
  68. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a2.dist-info}/LICENSE +0 -0
  69. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a2.dist-info}/entry_points.txt +0 -0
  70. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a2.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import (
4
- TypeVar,
5
- get_args,
6
- get_origin,
7
- get_type_hints,
8
- )
3
+ from typing import TypeVar
4
+ from unittest.mock import Mock
9
5
 
10
- from ophyd_async.core import (
11
- DEFAULT_TIMEOUT,
12
- Device,
13
- Signal,
14
- )
6
+ from ophyd_async.core import Device, DeviceConnector, DeviceFiller
15
7
  from ophyd_async.tango.signal import (
16
8
  TangoSignalBackend,
17
- __tango_signal_auto,
18
- make_backend,
9
+ infer_python_type,
10
+ infer_signal_type,
19
11
  )
20
12
  from tango import DeviceProxy as DeviceProxy
21
13
  from tango.asyncio import DeviceProxy as AsyncDeviceProxy
@@ -50,64 +42,14 @@ class TangoDevice(Device):
50
42
  device_proxy: DeviceProxy | None = None,
51
43
  name: str = "",
52
44
  ) -> None:
53
- self.trl = trl if trl else ""
54
- self.proxy = device_proxy
55
- tango_create_children_from_annotations(self)
56
- super().__init__(name=name)
57
-
58
- def set_trl(self, trl: str):
59
- """Set the Tango resource locator."""
60
- if not isinstance(trl, str):
61
- raise ValueError("TRL must be a string.")
62
- self.trl = trl
63
-
64
- async def connect(
65
- self,
66
- mock: bool = False,
67
- timeout: float = DEFAULT_TIMEOUT,
68
- force_reconnect: bool = False,
69
- ):
70
- if self.trl and self.proxy is None:
71
- self.proxy = await AsyncDeviceProxy(self.trl)
72
- elif self.proxy and not self.trl:
73
- self.trl = self.proxy.name()
74
-
75
- # Set the trl of the signal backends
76
- for child in self.children():
77
- if isinstance(child[1], Signal):
78
- if isinstance(child[1]._backend, TangoSignalBackend): # noqa: SLF001
79
- resource_name = child[0].lstrip("_")
80
- read_trl = f"{self.trl}/{resource_name}"
81
- child[1]._backend.set_trl(read_trl, read_trl) # noqa: SLF001
82
-
83
- if self.proxy is not None:
84
- self.register_signals()
85
- await _fill_proxy_entries(self)
86
-
87
- # set_name should be called again to propagate the new signal names
88
- self.set_name(self.name)
89
-
90
- # Set the polling configuration
91
- if self._polling[0]:
92
- for child in self.children():
93
- child_type = type(child[1])
94
- if issubclass(child_type, Signal):
95
- if isinstance(child[1]._backend, TangoSignalBackend): # noqa: SLF001 # type: ignore
96
- child[1]._backend.set_polling(*self._polling) # noqa: SLF001 # type: ignore
97
- child[1]._backend.allow_events(False) # noqa: SLF001 # type: ignore
98
- if self._signal_polling:
99
- for signal_name, polling in self._signal_polling.items():
100
- if hasattr(self, signal_name):
101
- attr = getattr(self, signal_name)
102
- if isinstance(attr._backend, TangoSignalBackend): # noqa: SLF001
103
- attr._backend.set_polling(*polling) # noqa: SLF001
104
- attr._backend.allow_events(False) # noqa: SLF001
105
-
106
- await super().connect(mock=mock, timeout=timeout)
107
-
108
- # Users can override this method to register new signals
109
- def register_signals(self):
110
- pass
45
+ connector = TangoDeviceConnector(
46
+ trl=trl,
47
+ device_proxy=device_proxy,
48
+ polling=self._polling,
49
+ signal_polling=self._signal_polling,
50
+ )
51
+ connector.create_children_from_annotations(self)
52
+ super().__init__(name=name, connector=connector)
111
53
 
112
54
 
113
55
  def tango_polling(
@@ -150,76 +92,67 @@ def tango_polling(
150
92
  return decorator
151
93
 
152
94
 
153
- def tango_create_children_from_annotations(
154
- device: TangoDevice, included_optional_fields: tuple[str, ...] = ()
155
- ):
156
- """Initialize blocks at __init__ of `device`."""
157
- for name, device_type in get_type_hints(type(device)).items():
158
- if name in ("_name", "parent"):
159
- continue
160
-
161
- # device_type, is_optional = _strip_union(device_type)
162
- # if is_optional and name not in included_optional_fields:
163
- # continue
164
- #
165
- # is_device_vector, device_type = _strip_device_vector(device_type)
166
- # if is_device_vector:
167
- # n_device_vector = DeviceVector()
168
- # setattr(device, name, n_device_vector)
169
-
170
- # else:
171
- origin = get_origin(device_type)
172
- origin = origin if origin else device_type
173
-
174
- if issubclass(origin, Signal):
175
- type_args = get_args(device_type)
176
- datatype = type_args[0] if type_args else None
177
- backend = make_backend(datatype=datatype, device_proxy=device.proxy)
178
- setattr(device, name, origin(name=name, backend=backend))
179
-
180
- elif issubclass(origin, Device) or isinstance(origin, Device):
181
- assert callable(origin), f"{origin} is not callable."
182
- setattr(device, name, origin())
183
-
184
-
185
- async def _fill_proxy_entries(device: TangoDevice):
186
- if device.proxy is None:
187
- raise RuntimeError(f"Device proxy is not connected for {device.name}")
188
- proxy_trl = device.trl
189
- children = [name.lstrip("_") for name, _ in device.children()]
190
- proxy_attributes = list(device.proxy.get_attribute_list())
191
- proxy_commands = list(device.proxy.get_command_list())
192
- combined = proxy_attributes + proxy_commands
193
-
194
- for name in combined:
195
- if name not in children:
196
- full_trl = f"{proxy_trl}/{name}"
197
- try:
198
- auto_signal = await __tango_signal_auto(
199
- trl=full_trl, device_proxy=device.proxy
95
+ class TangoDeviceConnector(DeviceConnector):
96
+ def __init__(
97
+ self,
98
+ trl: str | None,
99
+ device_proxy: DeviceProxy | None,
100
+ polling: tuple[bool, float, float | None, float | None],
101
+ signal_polling: dict[str, tuple[bool, float, float, float]],
102
+ ) -> None:
103
+ self.trl = trl
104
+ self.proxy = device_proxy
105
+ self._polling = polling
106
+ self._signal_polling = signal_polling
107
+
108
+ def create_children_from_annotations(self, device: Device):
109
+ self._filler = DeviceFiller(
110
+ device=device,
111
+ signal_backend_factory=TangoSignalBackend,
112
+ device_connector_factory=lambda: TangoDeviceConnector(
113
+ None, None, (False, 0.1, None, None), {}
114
+ ),
115
+ )
116
+
117
+ async def connect(
118
+ self, device: Device, mock: bool | Mock, timeout: float, force_reconnect: bool
119
+ ) -> None:
120
+ if mock:
121
+ # Make 2 entries for each DeviceVector
122
+ self._filler.make_soft_device_vector_entries(2)
123
+ else:
124
+ if self.trl and self.proxy is None:
125
+ self.proxy = await AsyncDeviceProxy(self.trl)
126
+ elif self.proxy and not self.trl:
127
+ self.trl = self.proxy.name()
128
+ else:
129
+ raise TypeError("Neither proxy nor trl supplied")
130
+
131
+ children = sorted(
132
+ set()
133
+ .union(self.proxy.get_attribute_list())
134
+ .union(self.proxy.get_command_list())
135
+ )
136
+ for name in children:
137
+ # TODO: strip attribute name
138
+ full_trl = f"{self.trl}/{name}"
139
+ signal_type = await infer_signal_type(full_trl, self.proxy)
140
+ if signal_type:
141
+ backend = self._filler.make_child_signal(name, signal_type)
142
+ backend.datatype = await infer_python_type(full_trl, self.proxy)
143
+ backend.set_trl(full_trl)
144
+ if polling := self._signal_polling.get(name, ()):
145
+ backend.set_polling(*polling)
146
+ backend.allow_events(False)
147
+ elif self._polling[0]:
148
+ backend.set_polling(*self._polling)
149
+ backend.allow_events(False)
150
+ # Check that all the requested children have been created
151
+ if unfilled := self._filler.unfilled():
152
+ raise RuntimeError(
153
+ f"{device.name}: cannot provision {unfilled} from "
154
+ f"{self.trl}: {children}"
200
155
  )
201
- setattr(device, name, auto_signal)
202
- except RuntimeError as e:
203
- if "Commands with different in and out dtypes" in str(e):
204
- print(
205
- f"Skipping {name}. Commands with different in and out dtypes"
206
- f" are not supported."
207
- )
208
- continue
209
- raise e
210
-
211
-
212
- # def _strip_union(field: T | T) -> tuple[T, bool]:
213
- # if get_origin(field) is Union:
214
- # args = get_args(field)
215
- # is_optional = type(None) in args
216
- # for arg in args:
217
- # if arg is not type(None):
218
- # return arg, is_optional
219
- # return field, False
220
- #
221
- #
222
- # def _strip_device_vector(field: type[Device]) -> tuple[bool, type[Device]]:
223
- # if get_origin(field) is DeviceVector:
224
- # return True, get_args(field)[0]
225
- # return False, field
156
+ # Set the name of the device to name all children
157
+ device.set_name(device.name)
158
+ return await super().connect(device, mock, timeout, force_reconnect)
@@ -19,7 +19,7 @@ class TangoCounter(TangoReadable):
19
19
  counts: SignalR[int]
20
20
  sample_time: SignalRW[float]
21
21
  start: SignalX
22
- _reset: SignalX
22
+ reset_: SignalX
23
23
 
24
24
  def __init__(self, trl: str | None = "", name=""):
25
25
  super().__init__(trl, name=name)
@@ -34,4 +34,4 @@ class TangoCounter(TangoReadable):
34
34
 
35
35
  @AsyncStatus.wrap
36
36
  async def reset(self) -> None:
37
- await self._reset.trigger(wait=True, timeout=DEFAULT_TIMEOUT)
37
+ await self.reset_.trigger(wait=True, timeout=DEFAULT_TIMEOUT)
@@ -29,7 +29,7 @@ class TangoMover(TangoReadable, Movable, Stoppable):
29
29
  position: SignalRW[float]
30
30
  velocity: SignalRW[float]
31
31
  state: SignalR[DevState]
32
- _stop: SignalX
32
+ stop_: SignalX
33
33
 
34
34
  def __init__(self, trl: str | None = "", name=""):
35
35
  super().__init__(trl, name=name)
@@ -74,4 +74,4 @@ class TangoMover(TangoReadable, Movable, Stoppable):
74
74
 
75
75
  def stop(self, success: bool = True) -> AsyncStatus:
76
76
  self._set_success = success
77
- return self._stop.trigger()
77
+ return self.stop_.trigger()
@@ -1,7 +1,6 @@
1
1
  from ._signal import (
2
- __tango_signal_auto,
3
2
  infer_python_type,
4
- infer_signal_character,
3
+ infer_signal_type,
5
4
  make_backend,
6
5
  tango_signal_r,
7
6
  tango_signal_rw,
@@ -29,11 +28,10 @@ __all__ = (
29
28
  "get_trl_descriptor",
30
29
  "get_tango_trl",
31
30
  "infer_python_type",
32
- "infer_signal_character",
31
+ "infer_signal_type",
33
32
  "make_backend",
34
33
  "tango_signal_r",
35
34
  "tango_signal_rw",
36
35
  "tango_signal_w",
37
36
  "tango_signal_x",
38
- "__tango_signal_auto",
39
37
  )
@@ -2,21 +2,28 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  from enum import Enum, IntEnum
6
7
 
7
8
  import numpy.typing as npt
8
9
 
9
- from ophyd_async.core import DEFAULT_TIMEOUT, SignalR, SignalRW, SignalW, SignalX, T
10
- from ophyd_async.tango.signal._tango_transport import (
11
- TangoSignalBackend,
12
- get_python_type,
10
+ from ophyd_async.core import (
11
+ DEFAULT_TIMEOUT,
12
+ Signal,
13
+ SignalDatatypeT,
14
+ SignalR,
15
+ SignalRW,
16
+ SignalW,
17
+ SignalX,
13
18
  )
14
19
  from tango import AttrDataFormat, AttrWriteType, CmdArgType, DeviceProxy, DevState
15
20
  from tango.asyncio import DeviceProxy as AsyncDeviceProxy
16
21
 
22
+ from ._tango_transport import TangoSignalBackend, get_python_type
23
+
17
24
 
18
25
  def make_backend(
19
- datatype: type[T] | None,
26
+ datatype: type[SignalDatatypeT] | None,
20
27
  read_trl: str = "",
21
28
  write_trl: str = "",
22
29
  device_proxy: DeviceProxy | None = None,
@@ -25,13 +32,13 @@ def make_backend(
25
32
 
26
33
 
27
34
  def tango_signal_rw(
28
- datatype: type[T],
35
+ datatype: type[SignalDatatypeT],
29
36
  read_trl: str,
30
37
  write_trl: str = "",
31
38
  device_proxy: DeviceProxy | None = None,
32
39
  timeout: float = DEFAULT_TIMEOUT,
33
40
  name: str = "",
34
- ) -> SignalRW[T]:
41
+ ) -> SignalRW[SignalDatatypeT]:
35
42
  """Create a `SignalRW` backed by 1 or 2 Tango Attribute/Command
36
43
 
37
44
  Parameters
@@ -54,12 +61,12 @@ def tango_signal_rw(
54
61
 
55
62
 
56
63
  def tango_signal_r(
57
- datatype: type[T],
64
+ datatype: type[SignalDatatypeT],
58
65
  read_trl: str,
59
66
  device_proxy: DeviceProxy | None = None,
60
67
  timeout: float = DEFAULT_TIMEOUT,
61
68
  name: str = "",
62
- ) -> SignalR[T]:
69
+ ) -> SignalR[SignalDatatypeT]:
63
70
  """Create a `SignalR` backed by 1 Tango Attribute/Command
64
71
 
65
72
  Parameters
@@ -80,12 +87,12 @@ def tango_signal_r(
80
87
 
81
88
 
82
89
  def tango_signal_w(
83
- datatype: type[T],
90
+ datatype: type[SignalDatatypeT],
84
91
  write_trl: str,
85
92
  device_proxy: DeviceProxy | None = None,
86
93
  timeout: float = DEFAULT_TIMEOUT,
87
94
  name: str = "",
88
- ) -> SignalW[T]:
95
+ ) -> SignalW[SignalDatatypeT]:
89
96
  """Create a `SignalW` backed by 1 Tango Attribute/Command
90
97
 
91
98
  Parameters
@@ -128,39 +135,10 @@ def tango_signal_x(
128
135
  return SignalX(backend, timeout=timeout, name=name)
129
136
 
130
137
 
131
- async def __tango_signal_auto(
132
- datatype: type[T] | None = None,
133
- *,
134
- trl: str,
135
- device_proxy: DeviceProxy | None,
136
- timeout: float = DEFAULT_TIMEOUT,
137
- name: str = "",
138
- ) -> SignalW | SignalX | SignalR | SignalRW | None:
139
- try:
140
- signal_character = await infer_signal_character(trl, device_proxy)
141
- except RuntimeError as e:
142
- if "Commands with different in and out dtypes" in str(e):
143
- return None
144
- else:
145
- raise e
146
-
147
- if datatype is None:
148
- datatype = await infer_python_type(trl, device_proxy)
149
-
150
- backend = make_backend(datatype, trl, trl, device_proxy)
151
- if signal_character == "RW":
152
- return SignalRW(backend=backend, timeout=timeout, name=name)
153
- if signal_character == "R":
154
- return SignalR(backend=backend, timeout=timeout, name=name)
155
- if signal_character == "W":
156
- return SignalW(backend=backend, timeout=timeout, name=name)
157
- if signal_character == "X":
158
- return SignalX(backend=backend, timeout=timeout, name=name)
159
-
160
-
161
138
  async def infer_python_type(
162
139
  trl: str = "", proxy: DeviceProxy | None = None
163
140
  ) -> object | npt.NDArray | type[DevState] | IntEnum:
141
+ # TODO: work out if this is still needed
164
142
  device_trl, tr_name = trl.rsplit("/", 1)
165
143
  if proxy is None:
166
144
  dev_proxy = await AsyncDeviceProxy(device_trl)
@@ -187,7 +165,9 @@ async def infer_python_type(
187
165
  return npt.NDArray[py_type] if isarray else py_type
188
166
 
189
167
 
190
- async def infer_signal_character(trl, proxy: DeviceProxy | None = None) -> str:
168
+ async def infer_signal_type(
169
+ trl, proxy: DeviceProxy | None = None
170
+ ) -> type[Signal] | None:
191
171
  device_trl, tr_name = trl.rsplit("/", 1)
192
172
  if proxy is None:
193
173
  dev_proxy = await AsyncDeviceProxy(device_trl)
@@ -204,20 +184,19 @@ async def infer_signal_character(trl, proxy: DeviceProxy | None = None) -> str:
204
184
  if tr_name in dev_proxy.get_attribute_list():
205
185
  config = await dev_proxy.get_attribute_config(tr_name)
206
186
  if config.writable in [AttrWriteType.READ_WRITE, AttrWriteType.READ_WITH_WRITE]:
207
- return "RW"
187
+ return SignalRW
208
188
  elif config.writable == AttrWriteType.READ:
209
- return "R"
189
+ return SignalR
210
190
  else:
211
- return "W"
191
+ return SignalW
212
192
 
213
193
  if tr_name in dev_proxy.get_command_list():
214
194
  config = await dev_proxy.get_command_config(tr_name)
215
195
  if config.in_type == CmdArgType.DevVoid:
216
- return "X"
196
+ return SignalX
217
197
  elif config.in_type != config.out_type:
218
- raise RuntimeError(
219
- "Commands with different in and out dtypes are not" " supported"
220
- )
198
+ logging.debug("Commands with different in and out dtypes are not supported")
199
+ return None
221
200
  else:
222
- return "RW"
201
+ return SignalRW
223
202
  raise RuntimeError(f"Unable to infer signal character for {trl}")