ophyd-async 0.7.0__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.
- ophyd_async/_version.py +2 -2
- ophyd_async/core/__init__.py +23 -8
- ophyd_async/core/_detector.py +5 -10
- ophyd_async/core/_device.py +139 -66
- ophyd_async/core/_device_filler.py +191 -0
- ophyd_async/core/_device_save_loader.py +6 -7
- ophyd_async/core/_mock_signal_backend.py +32 -40
- ophyd_async/core/_mock_signal_utils.py +22 -16
- ophyd_async/core/_protocol.py +28 -8
- ophyd_async/core/_readable.py +5 -5
- ophyd_async/core/_signal.py +140 -152
- ophyd_async/core/_signal_backend.py +131 -64
- ophyd_async/core/_soft_signal_backend.py +125 -194
- ophyd_async/core/_status.py +22 -6
- ophyd_async/core/_table.py +97 -100
- ophyd_async/core/_utils.py +71 -18
- ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
- ophyd_async/epics/adaravis/_aravis_io.py +7 -5
- ophyd_async/epics/adcore/_core_io.py +4 -6
- ophyd_async/epics/adcore/_hdf_writer.py +2 -2
- ophyd_async/epics/adcore/_utils.py +15 -10
- ophyd_async/epics/adkinetix/__init__.py +2 -1
- ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
- ophyd_async/epics/adkinetix/_kinetix_io.py +3 -4
- ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
- ophyd_async/epics/adpilatus/_pilatus_io.py +2 -3
- ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
- ophyd_async/epics/advimba/__init__.py +4 -1
- ophyd_async/epics/advimba/_vimba_controller.py +6 -3
- ophyd_async/epics/advimba/_vimba_io.py +7 -8
- ophyd_async/epics/demo/_sensor.py +8 -4
- ophyd_async/epics/eiger/_eiger.py +1 -2
- ophyd_async/epics/eiger/_eiger_controller.py +1 -1
- ophyd_async/epics/eiger/_eiger_io.py +2 -4
- ophyd_async/epics/eiger/_odin_io.py +4 -4
- ophyd_async/epics/pvi/__init__.py +2 -2
- ophyd_async/epics/pvi/_pvi.py +56 -321
- ophyd_async/epics/signal/__init__.py +3 -4
- ophyd_async/epics/signal/_aioca.py +184 -236
- ophyd_async/epics/signal/_common.py +35 -49
- ophyd_async/epics/signal/_p4p.py +254 -387
- ophyd_async/epics/signal/_signal.py +63 -21
- ophyd_async/fastcs/core.py +9 -0
- ophyd_async/fastcs/panda/__init__.py +4 -4
- ophyd_async/fastcs/panda/_block.py +18 -13
- ophyd_async/fastcs/panda/_control.py +3 -5
- ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
- ophyd_async/fastcs/panda/_table.py +29 -51
- ophyd_async/fastcs/panda/_trigger.py +8 -8
- ophyd_async/fastcs/panda/_writer.py +2 -5
- ophyd_async/plan_stubs/_ensure_connected.py +3 -1
- ophyd_async/plan_stubs/_fly.py +2 -2
- ophyd_async/plan_stubs/_nd_attributes.py +5 -4
- ophyd_async/py.typed +0 -0
- ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
- ophyd_async/tango/__init__.py +2 -4
- ophyd_async/tango/base_devices/_base_device.py +76 -143
- ophyd_async/tango/demo/_counter.py +2 -2
- ophyd_async/tango/demo/_mover.py +2 -2
- ophyd_async/tango/signal/__init__.py +2 -4
- ophyd_async/tango/signal/_signal.py +29 -50
- ophyd_async/tango/signal/_tango_transport.py +38 -40
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/METADATA +8 -12
- ophyd_async-0.8.0a2.dist-info/RECORD +110 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/WHEEL +1 -1
- ophyd_async/epics/signal/_epics_transport.py +0 -34
- ophyd_async-0.7.0.dist-info/RECORD +0 -108
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/LICENSE +0 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/entry_points.txt +0 -0
- {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0a2.dist-info}/top_level.txt +0 -0
ophyd_async/_version.py
CHANGED
ophyd_async/core/__init__.py
CHANGED
|
@@ -5,7 +5,8 @@ from ._detector import (
|
|
|
5
5
|
StandardDetector,
|
|
6
6
|
TriggerInfo,
|
|
7
7
|
)
|
|
8
|
-
from ._device import Device, DeviceCollector, DeviceVector
|
|
8
|
+
from ._device import Device, DeviceCollector, DeviceConnector, DeviceVector
|
|
9
|
+
from ._device_filler import DeviceFiller
|
|
9
10
|
from ._device_save_loader import (
|
|
10
11
|
all_at_once,
|
|
11
12
|
get_signal_values,
|
|
@@ -22,6 +23,7 @@ from ._log import config_ophyd_async_logging
|
|
|
22
23
|
from ._mock_signal_backend import MockSignalBackend
|
|
23
24
|
from ._mock_signal_utils import (
|
|
24
25
|
callback_on_mock_put,
|
|
26
|
+
get_mock,
|
|
25
27
|
get_mock_put,
|
|
26
28
|
mock_puts_blocked,
|
|
27
29
|
reset_mock_put_calls,
|
|
@@ -62,9 +64,11 @@ from ._signal import (
|
|
|
62
64
|
wait_for_value,
|
|
63
65
|
)
|
|
64
66
|
from ._signal_backend import (
|
|
65
|
-
|
|
67
|
+
Array1D,
|
|
66
68
|
SignalBackend,
|
|
67
|
-
|
|
69
|
+
SignalDatatype,
|
|
70
|
+
SignalDatatypeT,
|
|
71
|
+
make_datakey,
|
|
68
72
|
)
|
|
69
73
|
from ._soft_signal_backend import SignalMetadata, SoftSignalBackend
|
|
70
74
|
from ._status import AsyncStatus, WatchableAsyncStatus, completed_status
|
|
@@ -73,14 +77,17 @@ from ._utils import (
|
|
|
73
77
|
CALCULATE_TIMEOUT,
|
|
74
78
|
DEFAULT_TIMEOUT,
|
|
75
79
|
CalculatableTimeout,
|
|
80
|
+
Callback,
|
|
76
81
|
NotConnected,
|
|
77
|
-
|
|
82
|
+
Reference,
|
|
83
|
+
StrictEnum,
|
|
84
|
+
SubsetEnum,
|
|
78
85
|
T,
|
|
79
86
|
WatcherUpdate,
|
|
80
87
|
get_dtype,
|
|
88
|
+
get_enum_cls,
|
|
81
89
|
get_unique,
|
|
82
90
|
in_micros,
|
|
83
|
-
is_pydantic_model,
|
|
84
91
|
wait_for_connection,
|
|
85
92
|
)
|
|
86
93
|
|
|
@@ -91,8 +98,10 @@ __all__ = [
|
|
|
91
98
|
"StandardDetector",
|
|
92
99
|
"TriggerInfo",
|
|
93
100
|
"Device",
|
|
101
|
+
"DeviceConnector",
|
|
94
102
|
"DeviceCollector",
|
|
95
103
|
"DeviceVector",
|
|
104
|
+
"DeviceFiller",
|
|
96
105
|
"all_at_once",
|
|
97
106
|
"get_signal_values",
|
|
98
107
|
"load_device",
|
|
@@ -108,6 +117,7 @@ __all__ = [
|
|
|
108
117
|
"config_ophyd_async_logging",
|
|
109
118
|
"MockSignalBackend",
|
|
110
119
|
"callback_on_mock_put",
|
|
120
|
+
"get_mock",
|
|
111
121
|
"get_mock_put",
|
|
112
122
|
"mock_puts_blocked",
|
|
113
123
|
"reset_mock_put_calls",
|
|
@@ -146,25 +156,30 @@ __all__ = [
|
|
|
146
156
|
"soft_signal_r_and_setter",
|
|
147
157
|
"soft_signal_rw",
|
|
148
158
|
"wait_for_value",
|
|
149
|
-
"
|
|
159
|
+
"Array1D",
|
|
150
160
|
"SignalBackend",
|
|
161
|
+
"make_datakey",
|
|
162
|
+
"StrictEnum",
|
|
151
163
|
"SubsetEnum",
|
|
164
|
+
"SignalDatatype",
|
|
165
|
+
"SignalDatatypeT",
|
|
152
166
|
"SignalMetadata",
|
|
153
167
|
"SoftSignalBackend",
|
|
154
168
|
"AsyncStatus",
|
|
155
169
|
"WatchableAsyncStatus",
|
|
156
170
|
"DEFAULT_TIMEOUT",
|
|
157
171
|
"CalculatableTimeout",
|
|
172
|
+
"Callback",
|
|
158
173
|
"CALCULATE_TIMEOUT",
|
|
159
174
|
"NotConnected",
|
|
160
|
-
"
|
|
175
|
+
"Reference",
|
|
161
176
|
"Table",
|
|
162
177
|
"T",
|
|
163
178
|
"WatcherUpdate",
|
|
164
179
|
"get_dtype",
|
|
180
|
+
"get_enum_cls",
|
|
165
181
|
"get_unique",
|
|
166
182
|
"in_micros",
|
|
167
|
-
"is_pydantic_model",
|
|
168
183
|
"wait_for_connection",
|
|
169
184
|
"completed_status",
|
|
170
185
|
]
|
ophyd_async/core/_detector.py
CHANGED
|
@@ -4,11 +4,7 @@ import asyncio
|
|
|
4
4
|
import time
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Iterator, Sequence
|
|
7
|
-
from enum import Enum
|
|
8
7
|
from functools import cached_property
|
|
9
|
-
from typing import (
|
|
10
|
-
Generic,
|
|
11
|
-
)
|
|
12
8
|
|
|
13
9
|
from bluesky.protocols import (
|
|
14
10
|
Collectable,
|
|
@@ -23,14 +19,14 @@ from bluesky.protocols import (
|
|
|
23
19
|
from event_model import DataKey
|
|
24
20
|
from pydantic import BaseModel, Field, NonNegativeInt, computed_field
|
|
25
21
|
|
|
26
|
-
from ._device import Device
|
|
22
|
+
from ._device import Device, DeviceConnector
|
|
27
23
|
from ._protocol import AsyncConfigurable, AsyncReadable
|
|
28
24
|
from ._signal import SignalR
|
|
29
25
|
from ._status import AsyncStatus, WatchableAsyncStatus
|
|
30
|
-
from ._utils import DEFAULT_TIMEOUT,
|
|
26
|
+
from ._utils import DEFAULT_TIMEOUT, StrictEnum, WatcherUpdate, merge_gathered_dicts
|
|
31
27
|
|
|
32
28
|
|
|
33
|
-
class DetectorTrigger(
|
|
29
|
+
class DetectorTrigger(StrictEnum):
|
|
34
30
|
"""Type of mechanism for triggering a detector to take frames"""
|
|
35
31
|
|
|
36
32
|
#: Detector generates internal trigger for given rate
|
|
@@ -172,7 +168,6 @@ class StandardDetector(
|
|
|
172
168
|
Flyable,
|
|
173
169
|
Collectable,
|
|
174
170
|
WritesStreamAssets,
|
|
175
|
-
Generic[T],
|
|
176
171
|
):
|
|
177
172
|
"""
|
|
178
173
|
Useful detector base class for step and fly scanning detectors.
|
|
@@ -185,6 +180,7 @@ class StandardDetector(
|
|
|
185
180
|
writer: DetectorWriter,
|
|
186
181
|
config_sigs: Sequence[SignalR] = (),
|
|
187
182
|
name: str = "",
|
|
183
|
+
connector: DeviceConnector | None = None,
|
|
188
184
|
) -> None:
|
|
189
185
|
"""
|
|
190
186
|
Constructor
|
|
@@ -213,8 +209,7 @@ class StandardDetector(
|
|
|
213
209
|
self._completable_frames: int = 0
|
|
214
210
|
self._number_of_triggers_iter: Iterator[int] | None = None
|
|
215
211
|
self._initial_frame: int = 0
|
|
216
|
-
|
|
217
|
-
super().__init__(name)
|
|
212
|
+
super().__init__(name, connector=connector)
|
|
218
213
|
|
|
219
214
|
@property
|
|
220
215
|
def controller(self) -> DetectorController:
|
ophyd_async/core/_device.py
CHANGED
|
@@ -1,39 +1,80 @@
|
|
|
1
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import sys
|
|
5
|
-
from collections.abc import Coroutine,
|
|
6
|
-
from functools import cached_property
|
|
5
|
+
from collections.abc import Coroutine, Iterator, Mapping, MutableMapping
|
|
7
6
|
from logging import LoggerAdapter, getLogger
|
|
8
|
-
from typing import
|
|
9
|
-
|
|
10
|
-
Optional,
|
|
11
|
-
TypeVar,
|
|
12
|
-
)
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
from unittest.mock import Mock
|
|
13
9
|
|
|
14
10
|
from bluesky.protocols import HasName
|
|
15
11
|
from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
|
|
16
12
|
|
|
13
|
+
from ._protocol import Connectable
|
|
17
14
|
from ._utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
|
|
18
15
|
|
|
16
|
+
_device_mocks: dict[Device, Mock] = {}
|
|
19
17
|
|
|
20
|
-
class Device(HasName):
|
|
21
|
-
"""Common base class for all Ophyd Async Devices.
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
"""
|
|
19
|
+
class DeviceConnector:
|
|
20
|
+
"""Defines how a `Device` should be connected and type hints processed."""
|
|
21
|
+
|
|
22
|
+
def create_children_from_annotations(self, device: Device):
|
|
23
|
+
"""Used when children can be created from introspecting the hardware.
|
|
24
|
+
|
|
25
|
+
Some control systems allow introspection of a device to determine what
|
|
26
|
+
children it has. To allow this to work nicely with typing we add these
|
|
27
|
+
hints to the Device like so::
|
|
28
|
+
|
|
29
|
+
my_signal: SignalRW[int]
|
|
30
|
+
my_device: MyDevice
|
|
31
|
+
|
|
32
|
+
This method will be run during ``Device.__init__``, and is responsible
|
|
33
|
+
for turning all of those type hints into real Signal and Device instances.
|
|
34
|
+
|
|
35
|
+
Subsequent runs of this function should do nothing, to allow it to be
|
|
36
|
+
called early in Devices that need to pass references to their children
|
|
37
|
+
during ``__init__``.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
async def connect(
|
|
41
|
+
self,
|
|
42
|
+
device: Device,
|
|
43
|
+
mock: bool | Mock,
|
|
44
|
+
timeout: float,
|
|
45
|
+
force_reconnect: bool,
|
|
46
|
+
):
|
|
47
|
+
"""Used during ``Device.connect``.
|
|
48
|
+
|
|
49
|
+
This is called when a previous connect has not been done, or has been
|
|
50
|
+
done in a different mock more. It should connect the Device and all its
|
|
51
|
+
children.
|
|
52
|
+
"""
|
|
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
|
+
)
|
|
59
|
+
await wait_for_connection(**coros)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Device(HasName, Connectable):
|
|
63
|
+
"""Common base class for all Ophyd Async Devices."""
|
|
25
64
|
|
|
26
65
|
_name: str = ""
|
|
27
66
|
#: The parent Device if it exists
|
|
28
|
-
parent:
|
|
67
|
+
parent: Device | None = None
|
|
29
68
|
# None if connect hasn't started, a Task if it has
|
|
30
69
|
_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
|
|
31
73
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def __init__(self, name: str = "") -> None:
|
|
74
|
+
def __init__(
|
|
75
|
+
self, name: str = "", connector: DeviceConnector | None = None
|
|
76
|
+
) -> None:
|
|
77
|
+
self._connector = connector or DeviceConnector()
|
|
37
78
|
self.set_name(name)
|
|
38
79
|
|
|
39
80
|
@property
|
|
@@ -41,13 +82,7 @@ class Device(HasName):
|
|
|
41
82
|
"""Return the name of the Device"""
|
|
42
83
|
return self._name
|
|
43
84
|
|
|
44
|
-
|
|
45
|
-
def log(self):
|
|
46
|
-
return LoggerAdapter(
|
|
47
|
-
getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
def children(self) -> Iterator[tuple[str, "Device"]]:
|
|
85
|
+
def children(self) -> Iterator[tuple[str, Device]]:
|
|
51
86
|
for attr_name, attr in self.__dict__.items():
|
|
52
87
|
if attr_name != "parent" and isinstance(attr, Device):
|
|
53
88
|
yield attr_name, attr
|
|
@@ -60,23 +95,32 @@ class Device(HasName):
|
|
|
60
95
|
name:
|
|
61
96
|
New name to set
|
|
62
97
|
"""
|
|
63
|
-
|
|
64
|
-
# Ensure self.log is recreated after a name change
|
|
65
|
-
if hasattr(self, "log"):
|
|
66
|
-
del self.log
|
|
67
|
-
|
|
68
98
|
self._name = name
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
for child_name, child in self.children():
|
|
104
|
+
child_name = f"{self.name}-{child_name.strip('_')}" if self.name else ""
|
|
71
105
|
child.set_name(child_name)
|
|
72
|
-
|
|
106
|
+
|
|
107
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
108
|
+
if name == "parent":
|
|
109
|
+
if self.parent not in (value, None):
|
|
110
|
+
raise TypeError(
|
|
111
|
+
f"Cannot set the parent of {self} to be {value}: "
|
|
112
|
+
f"it is already a child of {self.parent}"
|
|
113
|
+
)
|
|
114
|
+
elif isinstance(value, Device):
|
|
115
|
+
value.parent = self
|
|
116
|
+
return super().__setattr__(name, value)
|
|
73
117
|
|
|
74
118
|
async def connect(
|
|
75
119
|
self,
|
|
76
|
-
mock: bool = False,
|
|
120
|
+
mock: bool | Mock = False,
|
|
77
121
|
timeout: float = DEFAULT_TIMEOUT,
|
|
78
122
|
force_reconnect: bool = False,
|
|
79
|
-
):
|
|
123
|
+
) -> None:
|
|
80
124
|
"""Connect self and all child Devices.
|
|
81
125
|
|
|
82
126
|
Contains a timeout that gets propagated to child.connect methods.
|
|
@@ -88,41 +132,32 @@ class Device(HasName):
|
|
|
88
132
|
timeout:
|
|
89
133
|
Time to wait before failing with a TimeoutError.
|
|
90
134
|
"""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
and self.
|
|
95
|
-
|
|
96
|
-
raise RuntimeError(
|
|
97
|
-
f"`connect(mock={mock})` called on a `Device` where the previous "
|
|
98
|
-
f"connect was `mock={self._previous_connect_was_mock}`. Changing mock "
|
|
99
|
-
"value between connects is not permitted."
|
|
100
|
-
)
|
|
101
|
-
self._previous_connect_was_mock = mock
|
|
102
|
-
|
|
103
|
-
# If previous connect with same args has started and not errored, can use it
|
|
104
|
-
can_use_previous_connect = self._connect_task and not (
|
|
105
|
-
self._connect_task.done() and self._connect_task.exception()
|
|
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())
|
|
106
140
|
)
|
|
141
|
+
if mock is True:
|
|
142
|
+
mock = Mock() # create a new Mock if one not provided
|
|
107
143
|
if force_reconnect or not can_use_previous_connect:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
self._connect_task = asyncio.create_task(wait_for_connection(**coros))
|
|
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
|
|
149
|
+
)
|
|
150
|
+
self._connect_task = asyncio.create_task(coro)
|
|
116
151
|
|
|
117
152
|
assert self._connect_task, "Connect task not created, this shouldn't happen"
|
|
118
153
|
# Wait for it to complete
|
|
119
154
|
await self._connect_task
|
|
120
155
|
|
|
121
156
|
|
|
122
|
-
|
|
157
|
+
DeviceT = TypeVar("DeviceT", bound=Device)
|
|
123
158
|
|
|
124
159
|
|
|
125
|
-
class DeviceVector(
|
|
160
|
+
class DeviceVector(MutableMapping[int, DeviceT], Device):
|
|
126
161
|
"""
|
|
127
162
|
Defines device components with indices.
|
|
128
163
|
|
|
@@ -131,10 +166,48 @@ class DeviceVector(dict[int, VT], Device):
|
|
|
131
166
|
:class:`~ophyd_async.epics.demo.DynamicSensorGroup`
|
|
132
167
|
"""
|
|
133
168
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
children: Mapping[int, DeviceT],
|
|
172
|
+
name: str = "",
|
|
173
|
+
) -> None:
|
|
174
|
+
self._children = dict(children)
|
|
175
|
+
super().__init__(name=name)
|
|
176
|
+
|
|
177
|
+
def __setattr__(self, name: str, child: Any) -> None:
|
|
178
|
+
if name != "parent" and isinstance(child, Device):
|
|
179
|
+
raise AttributeError(
|
|
180
|
+
"DeviceVector can only have integer named children, "
|
|
181
|
+
"set via device_vector[i] = child"
|
|
182
|
+
)
|
|
183
|
+
super().__setattr__(name, child)
|
|
184
|
+
|
|
185
|
+
def __getitem__(self, key: int) -> DeviceT:
|
|
186
|
+
return self._children[key]
|
|
187
|
+
|
|
188
|
+
def __setitem__(self, key: int, value: DeviceT) -> None:
|
|
189
|
+
# Check the types on entry to dict to make sure we can't accidentally
|
|
190
|
+
# make a non-integer named child
|
|
191
|
+
assert isinstance(key, int), f"Expected int, got {key}"
|
|
192
|
+
assert isinstance(value, Device), f"Expected Device, got {value}"
|
|
193
|
+
self._children[key] = value
|
|
194
|
+
value.parent = self
|
|
195
|
+
|
|
196
|
+
def __delitem__(self, key: int) -> None:
|
|
197
|
+
del self._children[key]
|
|
198
|
+
|
|
199
|
+
def __iter__(self) -> Iterator[int]:
|
|
200
|
+
yield from self._children
|
|
201
|
+
|
|
202
|
+
def __len__(self) -> int:
|
|
203
|
+
return len(self._children)
|
|
204
|
+
|
|
205
|
+
def children(self) -> Iterator[tuple[str, Device]]:
|
|
206
|
+
for key, child in self._children.items():
|
|
207
|
+
yield str(key), child
|
|
208
|
+
|
|
209
|
+
def __hash__(self): # to allow DeviceVector to be used as dict keys and in sets
|
|
210
|
+
return hash(id(self))
|
|
138
211
|
|
|
139
212
|
|
|
140
213
|
class DeviceCollector:
|
|
@@ -195,12 +268,12 @@ class DeviceCollector:
|
|
|
195
268
|
), "No previous frame to the one with self in it, this shouldn't happen"
|
|
196
269
|
return caller_frame.f_locals
|
|
197
270
|
|
|
198
|
-
def __enter__(self) ->
|
|
271
|
+
def __enter__(self) -> DeviceCollector:
|
|
199
272
|
# Stash the names that were defined before we were called
|
|
200
273
|
self._names_on_enter = set(self._caller_locals())
|
|
201
274
|
return self
|
|
202
275
|
|
|
203
|
-
async def __aenter__(self) ->
|
|
276
|
+
async def __aenter__(self) -> DeviceCollector:
|
|
204
277
|
return self.__enter__()
|
|
205
278
|
|
|
206
279
|
async def _on_exit(self) -> None:
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import (
|
|
6
|
+
Generic,
|
|
7
|
+
NoReturn,
|
|
8
|
+
TypeVar,
|
|
9
|
+
get_args,
|
|
10
|
+
get_origin,
|
|
11
|
+
get_type_hints,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from ._device import Device, DeviceConnector, DeviceVector
|
|
15
|
+
from ._signal import Signal, SignalX
|
|
16
|
+
from ._signal_backend import SignalBackend, SignalDatatype
|
|
17
|
+
from ._utils import get_origin_class
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _strip_number_from_string(string: str) -> tuple[str, int | None]:
|
|
21
|
+
match = re.match(r"(.*?)(\d*)$", string)
|
|
22
|
+
assert match
|
|
23
|
+
|
|
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)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
SignalBackendT = TypeVar("SignalBackendT", bound=SignalBackend)
|
|
33
|
+
DeviceConnectorT = TypeVar("DeviceConnectorT", bound=DeviceConnector)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
device: Device,
|
|
40
|
+
signal_backend_factory: Callable[[type[SignalDatatype] | None], SignalBackendT],
|
|
41
|
+
device_connector_factory: Callable[[], DeviceConnectorT],
|
|
42
|
+
):
|
|
43
|
+
self._device = device
|
|
44
|
+
self._signal_backend_factory = signal_backend_factory
|
|
45
|
+
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] = {}
|
|
50
|
+
# Get type hints on the class, not the instance
|
|
51
|
+
# 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("_")
|
|
57
|
+
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
|
+
)
|
|
73
|
+
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]
|
|
77
|
+
if child_origin is None or not issubclass(child_origin, Device):
|
|
78
|
+
self._raise(
|
|
79
|
+
name,
|
|
80
|
+
f"Expected DeviceVector[SomeDevice], got {annotation}",
|
|
81
|
+
)
|
|
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
|
+
)
|
|
87
|
+
|
|
88
|
+
def unfilled(self) -> set[str]:
|
|
89
|
+
return set(self._device_connectors).union(self._signal_backends)
|
|
90
|
+
|
|
91
|
+
def _raise(self, name: str, error: str) -> NoReturn:
|
|
92
|
+
raise TypeError(f"{type(self._device).__name__}.{name}: {error}")
|
|
93
|
+
|
|
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:
|
|
129
|
+
# We made it above
|
|
130
|
+
backend, expected_signal_type = self._signal_backends.pop(name)
|
|
131
|
+
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}")
|
|
147
|
+
if signal_type is not expected_signal_type:
|
|
148
|
+
self._raise(
|
|
149
|
+
name,
|
|
150
|
+
f"is a {signal_type.__name__} not a {expected_signal_type.__name__}",
|
|
151
|
+
)
|
|
152
|
+
return backend
|
|
153
|
+
|
|
154
|
+
def make_child_device(
|
|
155
|
+
self, name: str, device_type: type[Device] = Device
|
|
156
|
+
) -> 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):
|
|
160
|
+
# 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
|
|
165
|
+
assert issubclass(
|
|
166
|
+
vector_device_type, Device
|
|
167
|
+
), f"{vector_device_type} is not a Device"
|
|
168
|
+
connector = self._device_connector_factory()
|
|
169
|
+
device = vector_device_type(connector=connector)
|
|
170
|
+
self._vectors[basename][number] = device
|
|
171
|
+
elif child is None:
|
|
172
|
+
# We need to add a new child to the top level Device
|
|
173
|
+
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)
|
|
179
|
+
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}")
|
|
@@ -27,11 +27,8 @@ def pydantic_model_abstraction_representer(
|
|
|
27
27
|
return dumper.represent_data(model.model_dump(mode="python"))
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if isinstance(data, Enum):
|
|
33
|
-
return self.represent_data(data.value)
|
|
34
|
-
return super().represent_data(data)
|
|
30
|
+
def enum_representer(dumper: yaml.Dumper, enum: Enum) -> yaml.Node:
|
|
31
|
+
return dumper.represent_data(enum.value)
|
|
35
32
|
|
|
36
33
|
|
|
37
34
|
def get_signal_values(
|
|
@@ -74,7 +71,7 @@ def get_signal_values(
|
|
|
74
71
|
for key, value in zip(selected_signals, selected_values, strict=False)
|
|
75
72
|
}
|
|
76
73
|
# Ignored values place in with value None so we know which ones were ignored
|
|
77
|
-
named_values.update(
|
|
74
|
+
named_values.update(dict.fromkeys(ignore))
|
|
78
75
|
return named_values
|
|
79
76
|
|
|
80
77
|
|
|
@@ -111,6 +108,7 @@ def walk_rw_signals(
|
|
|
111
108
|
path_prefix = ""
|
|
112
109
|
|
|
113
110
|
signals: dict[str, SignalRW[Any]] = {}
|
|
111
|
+
|
|
114
112
|
for attr_name, attr in device.children():
|
|
115
113
|
dot_path = f"{path_prefix}{attr_name}"
|
|
116
114
|
if type(attr) is SignalRW:
|
|
@@ -145,9 +143,10 @@ def save_to_yaml(phases: Sequence[dict[str, Any]], save_path: str | Path) -> Non
|
|
|
145
143
|
pydantic_model_abstraction_representer,
|
|
146
144
|
Dumper=yaml.Dumper,
|
|
147
145
|
)
|
|
146
|
+
yaml.add_multi_representer(Enum, enum_representer, Dumper=yaml.Dumper)
|
|
148
147
|
|
|
149
148
|
with open(save_path, "w") as file:
|
|
150
|
-
yaml.dump(phases, file
|
|
149
|
+
yaml.dump(phases, file)
|
|
151
150
|
|
|
152
151
|
|
|
153
152
|
def load_from_yaml(save_path: str) -> Sequence[dict[str, Any]]:
|