ophyd-async 0.8.0a2__py3-none-any.whl → 0.8.0a4__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 (45) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +9 -1
  3. ophyd_async/core/_device.py +71 -49
  4. ophyd_async/core/_device_filler.py +208 -130
  5. ophyd_async/core/_mock_signal_backend.py +10 -7
  6. ophyd_async/core/_mock_signal_utils.py +14 -11
  7. ophyd_async/core/_readable.py +128 -129
  8. ophyd_async/core/_signal.py +22 -24
  9. ophyd_async/core/_soft_signal_backend.py +2 -0
  10. ophyd_async/core/_utils.py +64 -11
  11. ophyd_async/epics/adaravis/_aravis_io.py +1 -1
  12. ophyd_async/epics/adcore/_core_io.py +1 -1
  13. ophyd_async/epics/adcore/_single_trigger.py +6 -10
  14. ophyd_async/epics/adkinetix/_kinetix_io.py +1 -1
  15. ophyd_async/epics/adpilatus/_pilatus_io.py +1 -1
  16. ophyd_async/epics/advimba/_vimba_io.py +1 -1
  17. ophyd_async/epics/core/__init__.py +26 -0
  18. ophyd_async/epics/{signal → core}/_aioca.py +3 -6
  19. ophyd_async/epics/core/_epics_connector.py +53 -0
  20. ophyd_async/epics/core/_epics_device.py +13 -0
  21. ophyd_async/epics/{signal → core}/_p4p.py +3 -6
  22. ophyd_async/epics/core/_pvi_connector.py +91 -0
  23. ophyd_async/epics/{signal → core}/_signal.py +31 -16
  24. ophyd_async/epics/{signal/_common.py → core/_util.py} +19 -1
  25. ophyd_async/epics/demo/_mover.py +4 -5
  26. ophyd_async/epics/demo/_sensor.py +9 -12
  27. ophyd_async/epics/eiger/_eiger_io.py +1 -1
  28. ophyd_async/epics/eiger/_odin_io.py +1 -1
  29. ophyd_async/epics/motor.py +4 -5
  30. ophyd_async/epics/signal.py +11 -0
  31. ophyd_async/fastcs/core.py +2 -2
  32. ophyd_async/plan_stubs/_ensure_connected.py +2 -4
  33. ophyd_async/sim/demo/_sim_motor.py +3 -4
  34. ophyd_async/tango/base_devices/_base_device.py +48 -48
  35. ophyd_async/tango/demo/_counter.py +6 -16
  36. ophyd_async/tango/demo/_mover.py +3 -4
  37. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/METADATA +1 -1
  38. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/RECORD +42 -40
  39. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/WHEEL +1 -1
  40. ophyd_async/epics/pvi/__init__.py +0 -3
  41. ophyd_async/epics/pvi/_pvi.py +0 -73
  42. ophyd_async/epics/signal/__init__.py +0 -20
  43. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/LICENSE +0 -0
  44. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/entry_points.txt +0 -0
  45. {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.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.0a4'
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,
@@ -78,6 +83,7 @@ from ._utils import (
78
83
  DEFAULT_TIMEOUT,
79
84
  CalculatableTimeout,
80
85
  Callback,
86
+ LazyMock,
81
87
  NotConnected,
82
88
  Reference,
83
89
  StrictEnum,
@@ -141,6 +147,7 @@ __all__ = [
141
147
  "ConfigSignal",
142
148
  "HintedSignal",
143
149
  "StandardReadable",
150
+ "StandardReadableFormat",
144
151
  "Signal",
145
152
  "SignalR",
146
153
  "SignalRW",
@@ -170,6 +177,7 @@ __all__ = [
170
177
  "DEFAULT_TIMEOUT",
171
178
  "CalculatableTimeout",
172
179
  "Callback",
180
+ "LazyMock",
173
181
  "CALCULATE_TIMEOUT",
174
182
  "NotConnected",
175
183
  "Reference",
@@ -3,17 +3,15 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import sys
5
5
  from collections.abc import Coroutine, Iterator, Mapping, MutableMapping
6
+ from functools import cached_property
6
7
  from logging import LoggerAdapter, getLogger
7
8
  from typing import Any, TypeVar
8
- from unittest.mock import Mock
9
9
 
10
10
  from bluesky.protocols import HasName
11
11
  from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
12
12
 
13
13
  from ._protocol import Connectable
14
- from ._utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
15
-
16
- _device_mocks: dict[Device, Mock] = {}
14
+ from ._utils import DEFAULT_TIMEOUT, LazyMock, NotConnected, wait_for_connection
17
15
 
18
16
 
19
17
  class DeviceConnector:
@@ -37,25 +35,23 @@ class DeviceConnector:
37
35
  during ``__init__``.
38
36
  """
39
37
 
40
- async def connect(
41
- self,
42
- device: Device,
43
- mock: bool | Mock,
44
- timeout: float,
45
- force_reconnect: bool,
46
- ):
38
+ async def connect_mock(self, device: Device, mock: LazyMock):
39
+ # Connect serially, no errors to gather up as in mock mode
40
+ for name, child_device in device.children():
41
+ await child_device.connect(mock=mock.child(name))
42
+
43
+ async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
47
44
  """Used during ``Device.connect``.
48
45
 
49
46
  This is called when a previous connect has not been done, or has been
50
47
  done in a different mock more. It should connect the Device and all its
51
48
  children.
52
49
  """
53
- coros = {}
54
- for name, child_device in device.children():
55
- child_mock = getattr(mock, name) if mock else mock # Mock() or False
56
- coros[name] = child_device.connect(
57
- mock=child_mock, timeout=timeout, force_reconnect=force_reconnect
58
- )
50
+ # Connect in parallel, gathering up NotConnected errors
51
+ coros = {
52
+ name: child_device.connect(timeout=timeout, force_reconnect=force_reconnect)
53
+ for name, child_device in device.children()
54
+ }
59
55
  await wait_for_connection(**coros)
60
56
 
61
57
 
@@ -67,14 +63,14 @@ class Device(HasName, Connectable):
67
63
  parent: Device | None = None
68
64
  # None if connect hasn't started, a Task if it has
69
65
  _connect_task: asyncio.Task | None = None
70
- # If not None, then this is the mock arg of the previous connect
71
- # to let us know if we can reuse an existing connection
72
- _connect_mock_arg: bool | None = None
66
+ # The mock if we have connected in mock mode
67
+ _mock: LazyMock | None = None
73
68
 
74
69
  def __init__(
75
70
  self, name: str = "", connector: DeviceConnector | None = None
76
71
  ) -> None:
77
72
  self._connector = connector or DeviceConnector()
73
+ self._connector.create_children_from_annotations(self)
78
74
  self.set_name(name)
79
75
 
80
76
  @property
@@ -82,10 +78,18 @@ class Device(HasName, Connectable):
82
78
  """Return the name of the Device"""
83
79
  return self._name
84
80
 
81
+ @cached_property
82
+ def _child_devices(self) -> dict[str, Device]:
83
+ return {}
84
+
85
85
  def children(self) -> Iterator[tuple[str, Device]]:
86
- for attr_name, attr in self.__dict__.items():
87
- if attr_name != "parent" and isinstance(attr, Device):
88
- yield attr_name, attr
86
+ yield from self._child_devices.items()
87
+
88
+ @cached_property
89
+ def log(self) -> LoggerAdapter:
90
+ return LoggerAdapter(
91
+ getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
92
+ )
89
93
 
90
94
  def set_name(self, name: str):
91
95
  """Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
@@ -96,28 +100,33 @@ class Device(HasName, Connectable):
96
100
  New name to set
97
101
  """
98
102
  self._name = name
99
- # Ensure self.log is recreated after a name change
100
- self.log = LoggerAdapter(
101
- getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
102
- )
103
+ # Ensure logger is recreated after a name change
104
+ if "log" in self.__dict__:
105
+ del self.log
103
106
  for child_name, child in self.children():
104
107
  child_name = f"{self.name}-{child_name.strip('_')}" if self.name else ""
105
108
  child.set_name(child_name)
106
109
 
107
110
  def __setattr__(self, name: str, value: Any) -> None:
111
+ # Bear in mind that this function is called *a lot*, so
112
+ # we need to make sure nothing expensive happens in it...
108
113
  if name == "parent":
109
114
  if self.parent not in (value, None):
110
115
  raise TypeError(
111
116
  f"Cannot set the parent of {self} to be {value}: "
112
117
  f"it is already a child of {self.parent}"
113
118
  )
114
- elif isinstance(value, Device):
119
+ # ...hence not doing an isinstance check for attributes we
120
+ # know not to be Devices
121
+ elif name not in _not_device_attrs and isinstance(value, Device):
115
122
  value.parent = self
116
- return super().__setattr__(name, value)
123
+ self._child_devices[name] = value
124
+ # ...and avoiding the super call as we know it resolves to `object`
125
+ return object.__setattr__(self, name, value)
117
126
 
118
127
  async def connect(
119
128
  self,
120
- mock: bool | Mock = False,
129
+ mock: bool | LazyMock = False,
121
130
  timeout: float = DEFAULT_TIMEOUT,
122
131
  force_reconnect: bool = False,
123
132
  ) -> None:
@@ -132,26 +141,39 @@ class Device(HasName, Connectable):
132
141
  timeout:
133
142
  Time to wait before failing with a TimeoutError.
134
143
  """
135
- uses_mock = bool(mock)
136
- can_use_previous_connect = (
137
- uses_mock is self._connect_mock_arg
138
- and self._connect_task
139
- and not (self._connect_task.done() and self._connect_task.exception())
140
- )
141
- if mock is True:
142
- mock = Mock() # create a new Mock if one not provided
143
- if force_reconnect or not can_use_previous_connect:
144
- self._connect_mock_arg = uses_mock
145
- if self._connect_mock_arg:
146
- _device_mocks[self] = mock
147
- coro = self._connector.connect(
148
- device=self, mock=mock, timeout=timeout, force_reconnect=force_reconnect
144
+ if mock:
145
+ # Always connect in mock mode serially
146
+ if isinstance(mock, LazyMock):
147
+ # Use the provided mock
148
+ self._mock = mock
149
+ elif not self._mock:
150
+ # Make one
151
+ self._mock = LazyMock()
152
+ await self._connector.connect_mock(self, self._mock)
153
+ else:
154
+ # Try to cache the connect in real mode
155
+ can_use_previous_connect = (
156
+ self._mock is None
157
+ and self._connect_task
158
+ and not (self._connect_task.done() and self._connect_task.exception())
149
159
  )
150
- self._connect_task = asyncio.create_task(coro)
151
-
152
- assert self._connect_task, "Connect task not created, this shouldn't happen"
153
- # Wait for it to complete
154
- await self._connect_task
160
+ if force_reconnect or not can_use_previous_connect:
161
+ self._mock = None
162
+ coro = self._connector.connect_real(self, timeout, force_reconnect)
163
+ self._connect_task = asyncio.create_task(coro)
164
+ assert self._connect_task, "Connect task not created, this shouldn't happen"
165
+ # Wait for it to complete
166
+ await self._connect_task
167
+
168
+
169
+ _not_device_attrs = {
170
+ "_name",
171
+ "_children",
172
+ "_connector",
173
+ "_timeout",
174
+ "_mock",
175
+ "_connect_task",
176
+ }
155
177
 
156
178
 
157
179
  DeviceT = TypeVar("DeviceT", bound=Device)
@@ -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}")