ophyd-async 0.8.0a2__py3-none-any.whl → 0.8.0a3__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 (40) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +7 -1
  3. ophyd_async/core/_device.py +1 -0
  4. ophyd_async/core/_device_filler.py +208 -130
  5. ophyd_async/core/_readable.py +128 -129
  6. ophyd_async/core/_utils.py +9 -1
  7. ophyd_async/epics/adaravis/_aravis_io.py +1 -1
  8. ophyd_async/epics/adcore/_core_io.py +1 -1
  9. ophyd_async/epics/adcore/_single_trigger.py +4 -9
  10. ophyd_async/epics/adkinetix/_kinetix_io.py +1 -1
  11. ophyd_async/epics/adpilatus/_pilatus_io.py +1 -1
  12. ophyd_async/epics/advimba/_vimba_io.py +1 -1
  13. ophyd_async/epics/core/__init__.py +26 -0
  14. ophyd_async/epics/{signal → core}/_aioca.py +3 -6
  15. ophyd_async/epics/core/_epics_connector.py +53 -0
  16. ophyd_async/epics/core/_epics_device.py +13 -0
  17. ophyd_async/epics/{signal → core}/_p4p.py +3 -6
  18. ophyd_async/epics/core/_pvi_connector.py +92 -0
  19. ophyd_async/epics/{signal → core}/_signal.py +31 -16
  20. ophyd_async/epics/{signal/_common.py → core/_util.py} +19 -1
  21. ophyd_async/epics/demo/_mover.py +4 -5
  22. ophyd_async/epics/demo/_sensor.py +9 -12
  23. ophyd_async/epics/eiger/_eiger_io.py +1 -1
  24. ophyd_async/epics/eiger/_odin_io.py +1 -1
  25. ophyd_async/epics/motor.py +4 -5
  26. ophyd_async/epics/signal.py +11 -0
  27. ophyd_async/fastcs/core.py +2 -2
  28. ophyd_async/sim/demo/_sim_motor.py +3 -4
  29. ophyd_async/tango/base_devices/_base_device.py +15 -16
  30. ophyd_async/tango/demo/_counter.py +6 -16
  31. ophyd_async/tango/demo/_mover.py +3 -4
  32. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a3.dist-info}/METADATA +1 -1
  33. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a3.dist-info}/RECORD +37 -35
  34. ophyd_async/epics/pvi/__init__.py +0 -3
  35. ophyd_async/epics/pvi/_pvi.py +0 -73
  36. ophyd_async/epics/signal/__init__.py +0 -20
  37. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a3.dist-info}/LICENSE +0 -0
  38. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a3.dist-info}/WHEEL +0 -0
  39. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a3.dist-info}/entry_points.txt +0 -0
  40. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a3.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.8.0a2'
15
+ __version__ = version = '0.8.0a3'
16
16
  __version_tuple__ = version_tuple = (0, 8, 0)
@@ -45,7 +45,12 @@ from ._providers import (
45
45
  UUIDFilenameProvider,
46
46
  YMDPathProvider,
47
47
  )
48
- from ._readable import ConfigSignal, HintedSignal, StandardReadable
48
+ from ._readable import (
49
+ ConfigSignal,
50
+ HintedSignal,
51
+ StandardReadable,
52
+ StandardReadableFormat,
53
+ )
49
54
  from ._signal import (
50
55
  Signal,
51
56
  SignalR,
@@ -141,6 +146,7 @@ __all__ = [
141
146
  "ConfigSignal",
142
147
  "HintedSignal",
143
148
  "StandardReadable",
149
+ "StandardReadableFormat",
144
150
  "Signal",
145
151
  "SignalR",
146
152
  "SignalRW",
@@ -75,6 +75,7 @@ class Device(HasName, Connectable):
75
75
  self, name: str = "", connector: DeviceConnector | None = None
76
76
  ) -> None:
77
77
  self._connector = connector or DeviceConnector()
78
+ self._connector.create_children_from_annotations(self)
78
79
  self.set_name(name)
79
80
 
80
81
  @property
@@ -1,14 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
- import re
4
- from collections.abc import Callable
3
+ from abc import abstractmethod
4
+ from collections.abc import Callable, Iterator, Sequence
5
5
  from typing import (
6
+ Any,
6
7
  Generic,
8
+ NewType,
7
9
  NoReturn,
10
+ Protocol,
8
11
  TypeVar,
12
+ cast,
9
13
  get_args,
10
- get_origin,
11
14
  get_type_hints,
15
+ runtime_checkable,
12
16
  )
13
17
 
14
18
  from ._device import Device, DeviceConnector, DeviceVector
@@ -16,21 +20,29 @@ from ._signal import Signal, SignalX
16
20
  from ._signal_backend import SignalBackend, SignalDatatype
17
21
  from ._utils import get_origin_class
18
22
 
23
+ SignalBackendT = TypeVar("SignalBackendT", bound=SignalBackend)
24
+ DeviceConnectorT = TypeVar("DeviceConnectorT", bound=DeviceConnector)
25
+ # Unique name possibly with trailing understore, the attribute name on the Device
26
+ UniqueName = NewType("UniqueName", str)
27
+ # Logical name without trailing underscore, the name in the control system
28
+ LogicalName = NewType("LogicalName", str)
19
29
 
20
- def _strip_number_from_string(string: str) -> tuple[str, int | None]:
21
- match = re.match(r"(.*?)(\d*)$", string)
22
- assert match
23
30
 
24
- name = match.group(1)
25
- number = match.group(2) or None
26
- if number is None:
27
- return name, None
28
- else:
29
- return name, int(number)
31
+ def _get_datatype(annotation: Any) -> type | None:
32
+ """Return int from SignalRW[int]."""
33
+ args = get_args(annotation)
34
+ if len(args) == 1 and get_origin_class(args[0]):
35
+ return args[0]
30
36
 
31
37
 
32
- SignalBackendT = TypeVar("SignalBackendT", bound=SignalBackend)
33
- DeviceConnectorT = TypeVar("DeviceConnectorT", bound=DeviceConnector)
38
+ def _logical(name: UniqueName) -> LogicalName:
39
+ return LogicalName(name.rstrip("_"))
40
+
41
+
42
+ @runtime_checkable
43
+ class DeviceAnnotation(Protocol):
44
+ @abstractmethod
45
+ def __call__(self, parent: Device, child: Device): ...
34
46
 
35
47
 
36
48
  class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
@@ -43,107 +55,180 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
43
55
  self._device = device
44
56
  self._signal_backend_factory = signal_backend_factory
45
57
  self._device_connector_factory = device_connector_factory
46
- self._vectors: dict[str, DeviceVector] = {}
47
- self._vector_device_type: dict[str, type[Device] | None] = {}
48
- self._signal_backends: dict[str, tuple[SignalBackendT, type[Signal]]] = {}
49
- self._device_connectors: dict[str, DeviceConnectorT] = {}
58
+ # Annotations stored ready for the creation phase
59
+ self._uncreated_signals: dict[UniqueName, type[Signal]] = {}
60
+ self._uncreated_devices: dict[UniqueName, type[Device]] = {}
61
+ self._extras: dict[UniqueName, Sequence[Any]] = {}
62
+ self._signal_datatype: dict[LogicalName, type | None] = {}
63
+ self._vector_device_type: dict[LogicalName, type[Device] | None] = {}
64
+ # Backends and Connectors stored ready for the connection phase
65
+ self._unfilled_backends: dict[
66
+ LogicalName, tuple[SignalBackendT, type[Signal]]
67
+ ] = {}
68
+ self._unfilled_connectors: dict[LogicalName, DeviceConnectorT] = {}
69
+ # Once they are filled they go here in case we reconnect
70
+ self._filled_backends: dict[
71
+ LogicalName, tuple[SignalBackendT, type[Signal]]
72
+ ] = {}
73
+ self._filled_connectors: dict[LogicalName, DeviceConnectorT] = {}
74
+ self._scan_for_annotations()
75
+
76
+ def _raise(self, name: str, error: str) -> NoReturn:
77
+ raise TypeError(f"{type(self._device).__name__}.{name}: {error}")
78
+
79
+ def _store_signal_datatype(self, name: UniqueName, annotation: Any):
80
+ origin = get_origin_class(annotation)
81
+ datatype = _get_datatype(annotation)
82
+ if origin == SignalX:
83
+ # SignalX doesn't need datatype
84
+ self._signal_datatype[_logical(name)] = None
85
+ elif origin and issubclass(origin, Signal) and datatype:
86
+ # All other Signals need one
87
+ self._signal_datatype[_logical(name)] = datatype
88
+ else:
89
+ # Not recognized
90
+ self._raise(
91
+ name,
92
+ f"Expected SignalX or SignalR/W/RW[type], got {annotation}",
93
+ )
94
+
95
+ def _scan_for_annotations(self):
50
96
  # Get type hints on the class, not the instance
51
97
  # https://github.com/python/cpython/issues/124840
52
- self._annotations = get_type_hints(type(device))
53
- for name, annotation in self._annotations.items():
54
- # names have a trailing underscore if the clash with a bluesky verb,
55
- # so strip this off to get what the CS will provide
56
- stripped_name = name.rstrip("_")
98
+ cls = type(self._device)
99
+ # Get hints without Annotated for determining types
100
+ hints = get_type_hints(cls)
101
+ # Get hints with Annotated for wrapping signals and backends
102
+ extra_hints = get_type_hints(cls, include_extras=True)
103
+ for attr_name, annotation in hints.items():
104
+ name = UniqueName(attr_name)
57
105
  origin = get_origin_class(annotation)
58
- if name == "parent" or name.startswith("_") or not origin:
59
- # Ignore
60
- pass
61
- elif issubclass(origin, Signal):
62
- # SignalX doesn't need datatype, all others need one
63
- datatype = self.get_datatype(name)
64
- if origin != SignalX and datatype is None:
65
- self._raise(
66
- name,
67
- f"Expected SignalX or SignalR/W/RW[type], got {annotation}",
68
- )
69
- self._signal_backends[stripped_name] = (
70
- self.make_child_signal(name, origin),
71
- origin,
72
- )
106
+ if (
107
+ name == "parent"
108
+ or name.startswith("_")
109
+ or not origin
110
+ or not issubclass(origin, Device)
111
+ ):
112
+ # Ignore any child that is not a public Device
113
+ continue
114
+ self._extras[name] = getattr(extra_hints[attr_name], "__metadata__", ())
115
+ if issubclass(origin, Signal):
116
+ self._store_signal_datatype(name, annotation)
117
+ self._uncreated_signals[name] = origin
73
118
  elif origin == DeviceVector:
74
- # DeviceVector needs a type of device
75
- args = get_args(annotation) or [None]
76
- child_origin = get_origin(args[0]) or args[0]
119
+ child_type = _get_datatype(annotation)
120
+ child_origin = get_origin_class(child_type)
77
121
  if child_origin is None or not issubclass(child_origin, Device):
78
122
  self._raise(
79
123
  name,
80
124
  f"Expected DeviceVector[SomeDevice], got {annotation}",
81
125
  )
82
- self.make_device_vector(name, child_origin)
83
- elif issubclass(origin, Device):
84
- self._device_connectors[stripped_name] = self.make_child_device(
85
- name, origin
86
- )
126
+ if issubclass(child_origin, Signal):
127
+ self._store_signal_datatype(name, child_type)
128
+ self._vector_device_type[_logical(name)] = child_origin
129
+ setattr(self._device, name, DeviceVector({}))
130
+ else:
131
+ self._uncreated_devices[name] = origin
87
132
 
88
- def unfilled(self) -> set[str]:
89
- return set(self._device_connectors).union(self._signal_backends)
133
+ def check_created(self):
134
+ uncreated = sorted(set(self._uncreated_signals).union(self._uncreated_devices))
135
+ if uncreated:
136
+ raise RuntimeError(
137
+ f"{self._device.name}: {uncreated} have not been created yet"
138
+ )
90
139
 
91
- def _raise(self, name: str, error: str) -> NoReturn:
92
- raise TypeError(f"{type(self._device).__name__}.{name}: {error}")
140
+ def create_signals_from_annotations(
141
+ self,
142
+ filled=True,
143
+ ) -> Iterator[tuple[SignalBackendT, list[Any]]]:
144
+ for name in list(self._uncreated_signals):
145
+ child_type = self._uncreated_signals.pop(name)
146
+ backend = self._signal_backend_factory(
147
+ self._signal_datatype[_logical(name)]
148
+ )
149
+ extras = list(self._extras[name])
150
+ yield backend, extras
151
+ signal = child_type(backend)
152
+ for anno in extras:
153
+ assert isinstance(anno, DeviceAnnotation), anno
154
+ anno(self._device, signal)
155
+ setattr(self._device, name, signal)
156
+ dest = self._filled_backends if filled else self._unfilled_backends
157
+ dest[_logical(name)] = (backend, child_type)
93
158
 
94
- def make_device_vector(self, name: str, device_type: type[Device] | None):
95
- self._vectors[name] = DeviceVector({})
96
- self._vector_device_type[name] = device_type
97
- setattr(self._device, name, self._vectors[name])
98
-
99
- def make_device_vectors(self, names: list[str]):
100
- basenames: dict[str, set[int]] = {}
101
- for name in names:
102
- basename, number = _strip_number_from_string(name)
103
- if number is not None:
104
- basenames.setdefault(basename, set()).add(number)
105
- for basename, numbers in basenames.items():
106
- # If contiguous numbers starting at 1 then it's a device vector
107
- length = len(numbers)
108
- if length > 1 and numbers == set(range(1, length + 1)):
109
- # DeviceVector needs a type of device
110
- self.make_device_vector(basename, None)
111
-
112
- def get_datatype(self, name: str) -> type[SignalDatatype] | None:
113
- # Get dtype from SignalRW[dtype] or DeviceVector[SignalRW[dtype]]
114
- basename, _ = _strip_number_from_string(name)
115
- if basename in self._vectors:
116
- # We decided to put it in a device vector, so get datatype from that
117
- annotation = self._annotations.get(basename, None)
118
- if annotation:
119
- annotation = get_args(annotation)[0]
120
- else:
121
- # It's not a device vector, so get it from the full name
122
- annotation = self._annotations.get(name, None)
123
- args = get_args(annotation)
124
- if args and get_origin_class(args[0]):
125
- return args[0]
126
-
127
- def make_child_signal(self, name: str, signal_type: type[Signal]) -> SignalBackendT:
128
- if name in self._signal_backends:
159
+ def create_devices_from_annotations(
160
+ self,
161
+ filled=True,
162
+ ) -> Iterator[tuple[DeviceConnectorT, list[Any]]]:
163
+ for name in list(self._uncreated_devices):
164
+ child_type = self._uncreated_devices.pop(name)
165
+ connector = self._device_connector_factory()
166
+ extras = list(self._extras[name])
167
+ yield connector, extras
168
+ device = child_type(connector=connector)
169
+ for anno in extras:
170
+ assert isinstance(anno, DeviceAnnotation), anno
171
+ anno(self._device, device)
172
+ setattr(self._device, name, device)
173
+ dest = self._filled_connectors if filled else self._unfilled_connectors
174
+ dest[_logical(name)] = connector
175
+
176
+ def create_device_vector_entries_to_mock(self, num: int):
177
+ for name, cls in self._vector_device_type.items():
178
+ assert cls, "Shouldn't happen"
179
+ for i in range(1, num + 1):
180
+ if issubclass(cls, Signal):
181
+ self.fill_child_signal(name, cls, i)
182
+ elif issubclass(cls, Device):
183
+ self.fill_child_device(name, cls, i)
184
+ else:
185
+ self._raise(name, f"Can't make {cls}")
186
+
187
+ def check_filled(self, source: str):
188
+ unfilled = sorted(set(self._unfilled_connectors).union(self._unfilled_backends))
189
+ if unfilled:
190
+ raise RuntimeError(
191
+ f"{self._device.name}: cannot provision {unfilled} from {source}"
192
+ )
193
+
194
+ def _ensure_device_vector(self, name: LogicalName) -> DeviceVector:
195
+ if not hasattr(self._device, name):
196
+ # We have no type hints, so use whatever we are told
197
+ self._vector_device_type[name] = None
198
+ setattr(self._device, name, DeviceVector({}))
199
+ vector = getattr(self._device, name)
200
+ if not isinstance(vector, DeviceVector):
201
+ self._raise(name, f"Expected DeviceVector, got {vector}")
202
+ return vector
203
+
204
+ def fill_child_signal(
205
+ self,
206
+ name: str,
207
+ signal_type: type[Signal],
208
+ vector_index: int | None = None,
209
+ ) -> SignalBackendT:
210
+ name = cast(LogicalName, name)
211
+ if name in self._unfilled_backends:
129
212
  # We made it above
130
- backend, expected_signal_type = self._signal_backends.pop(name)
213
+ backend, expected_signal_type = self._unfilled_backends.pop(name)
214
+ self._filled_backends[name] = backend, expected_signal_type
215
+ elif name in self._filled_backends:
216
+ # We made it and filled it so return for validation
217
+ backend, expected_signal_type = self._filled_backends[name]
218
+ elif vector_index:
219
+ # We need to add a new entry to a DeviceVector
220
+ vector = self._ensure_device_vector(name)
221
+ backend = self._signal_backend_factory(self._signal_datatype.get(name))
222
+ expected_signal_type = self._vector_device_type[name] or signal_type
223
+ vector[vector_index] = signal_type(backend)
224
+ elif child := getattr(self._device, name, None):
225
+ # There is an existing child, so raise
226
+ self._raise(name, f"Cannot make child as it would shadow {child}")
131
227
  else:
132
- # We need to make a new one
133
- basename, number = _strip_number_from_string(name)
134
- child = getattr(self._device, name, None)
135
- backend = self._signal_backend_factory(self.get_datatype(name))
136
- signal = signal_type(backend)
137
- if basename in self._vectors and isinstance(number, int):
138
- # We need to add a new entry to an existing DeviceVector
139
- expected_signal_type = self._vector_device_type[basename] or signal_type
140
- self._vectors[basename][number] = signal
141
- elif child is None:
142
- # We need to add a new child to the top level Device
143
- expected_signal_type = signal_type
144
- setattr(self._device, name, signal)
145
- else:
146
- self._raise(name, f"Cannot make child as it would shadow {child}")
228
+ # We need to add a new child to the top level Device
229
+ backend = self._signal_backend_factory(None)
230
+ expected_signal_type = signal_type
231
+ setattr(self._device, name, signal_type(backend))
147
232
  if signal_type is not expected_signal_type:
148
233
  self._raise(
149
234
  name,
@@ -151,41 +236,34 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
151
236
  )
152
237
  return backend
153
238
 
154
- def make_child_device(
155
- self, name: str, device_type: type[Device] = Device
239
+ def fill_child_device(
240
+ self,
241
+ name: str,
242
+ device_type: type[Device] = Device,
243
+ vector_index: int | None = None,
156
244
  ) -> DeviceConnectorT:
157
- basename, number = _strip_number_from_string(name)
158
- child = getattr(self._device, name, None)
159
- if connector := self._device_connectors.pop(name, None):
245
+ name = cast(LogicalName, name)
246
+ if name in self._unfilled_connectors:
160
247
  # We made it above
161
- return connector
162
- elif basename in self._vectors and isinstance(number, int):
163
- # We need to add a new entry to an existing DeviceVector
164
- vector_device_type = self._vector_device_type[basename] or device_type
248
+ connector = self._unfilled_connectors.pop(name)
249
+ self._filled_connectors[name] = connector
250
+ elif name in self._filled_backends:
251
+ # We made it and filled it so return for validation
252
+ connector = self._filled_connectors[name]
253
+ elif vector_index:
254
+ # We need to add a new entry to a DeviceVector
255
+ vector = self._ensure_device_vector(name)
256
+ vector_device_type = self._vector_device_type[name] or device_type
165
257
  assert issubclass(
166
258
  vector_device_type, Device
167
259
  ), f"{vector_device_type} is not a Device"
168
260
  connector = self._device_connector_factory()
169
- device = vector_device_type(connector=connector)
170
- self._vectors[basename][number] = device
171
- elif child is None:
261
+ vector[vector_index] = vector_device_type(connector=connector)
262
+ elif child := getattr(self._device, name, None):
263
+ # There is an existing child, so raise
264
+ self._raise(name, f"Cannot make child as it would shadow {child}")
265
+ else:
172
266
  # We need to add a new child to the top level Device
173
267
  connector = self._device_connector_factory()
174
- device = device_type(connector=connector)
175
- setattr(self._device, name, device)
176
- else:
177
- self._raise(name, f"Cannot make child as it would shadow {child}")
178
- connector.create_children_from_annotations(device)
268
+ setattr(self._device, name, device_type(connector=connector))
179
269
  return connector
180
-
181
- def make_soft_device_vector_entries(self, num: int):
182
- for basename, cls in self._vector_device_type.items():
183
- assert cls, "Shouldn't happen"
184
- for i in range(num):
185
- name = f"{basename}{i + 1}"
186
- if issubclass(cls, Signal):
187
- self.make_child_signal(name, cls)
188
- elif issubclass(cls, Device):
189
- self.make_child_device(name, cls)
190
- else:
191
- self._raise(name, f"Can't make {cls}")