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.
- ophyd_async/_version.py +1 -1
- ophyd_async/core/__init__.py +9 -1
- ophyd_async/core/_device.py +71 -49
- ophyd_async/core/_device_filler.py +208 -130
- ophyd_async/core/_mock_signal_backend.py +10 -7
- ophyd_async/core/_mock_signal_utils.py +14 -11
- ophyd_async/core/_readable.py +128 -129
- ophyd_async/core/_signal.py +22 -24
- ophyd_async/core/_soft_signal_backend.py +2 -0
- ophyd_async/core/_utils.py +64 -11
- ophyd_async/epics/adaravis/_aravis_io.py +1 -1
- ophyd_async/epics/adcore/_core_io.py +1 -1
- ophyd_async/epics/adcore/_single_trigger.py +6 -10
- ophyd_async/epics/adkinetix/_kinetix_io.py +1 -1
- ophyd_async/epics/adpilatus/_pilatus_io.py +1 -1
- ophyd_async/epics/advimba/_vimba_io.py +1 -1
- ophyd_async/epics/core/__init__.py +26 -0
- ophyd_async/epics/{signal → core}/_aioca.py +3 -6
- ophyd_async/epics/core/_epics_connector.py +53 -0
- ophyd_async/epics/core/_epics_device.py +13 -0
- ophyd_async/epics/{signal → core}/_p4p.py +3 -6
- ophyd_async/epics/core/_pvi_connector.py +91 -0
- ophyd_async/epics/{signal → core}/_signal.py +31 -16
- ophyd_async/epics/{signal/_common.py → core/_util.py} +19 -1
- ophyd_async/epics/demo/_mover.py +4 -5
- ophyd_async/epics/demo/_sensor.py +9 -12
- ophyd_async/epics/eiger/_eiger_io.py +1 -1
- ophyd_async/epics/eiger/_odin_io.py +1 -1
- ophyd_async/epics/motor.py +4 -5
- ophyd_async/epics/signal.py +11 -0
- ophyd_async/fastcs/core.py +2 -2
- ophyd_async/plan_stubs/_ensure_connected.py +2 -4
- ophyd_async/sim/demo/_sim_motor.py +3 -4
- ophyd_async/tango/base_devices/_base_device.py +48 -48
- ophyd_async/tango/demo/_counter.py +6 -16
- ophyd_async/tango/demo/_mover.py +3 -4
- {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/METADATA +1 -1
- {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/RECORD +42 -40
- {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/WHEEL +1 -1
- ophyd_async/epics/pvi/__init__.py +0 -3
- ophyd_async/epics/pvi/_pvi.py +0 -73
- ophyd_async/epics/signal/__init__.py +0 -20
- {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/LICENSE +0 -0
- {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.8.0a2.dist-info → ophyd_async-0.8.0a4.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py
CHANGED
ophyd_async/core/__init__.py
CHANGED
|
@@ -45,7 +45,12 @@ from ._providers import (
|
|
|
45
45
|
UUIDFilenameProvider,
|
|
46
46
|
YMDPathProvider,
|
|
47
47
|
)
|
|
48
|
-
from ._readable import
|
|
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",
|
ophyd_async/core/_device.py
CHANGED
|
@@ -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
|
|
41
|
-
|
|
42
|
-
device:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
#
|
|
71
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
47
|
-
self.
|
|
48
|
-
self.
|
|
49
|
-
self.
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
self.
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
self.
|
|
85
|
-
|
|
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
|
|
89
|
-
|
|
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
|
|
92
|
-
|
|
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
|
|
95
|
-
self
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
155
|
-
self,
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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}")
|