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
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections.abc import Callable
|
|
3
3
|
from functools import cached_property
|
|
4
|
-
from unittest.mock import AsyncMock
|
|
4
|
+
from unittest.mock import AsyncMock
|
|
5
5
|
|
|
6
6
|
from bluesky.protocols import Descriptor, Reading
|
|
7
7
|
|
|
8
8
|
from ._signal_backend import SignalBackend, SignalDatatypeT
|
|
9
9
|
from ._soft_signal_backend import SoftSignalBackend
|
|
10
|
-
from ._utils import Callback
|
|
10
|
+
from ._utils import Callback, LazyMock
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class MockSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
@@ -16,7 +16,7 @@ class MockSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
16
16
|
def __init__(
|
|
17
17
|
self,
|
|
18
18
|
initial_backend: SignalBackend[SignalDatatypeT],
|
|
19
|
-
mock:
|
|
19
|
+
mock: LazyMock,
|
|
20
20
|
) -> None:
|
|
21
21
|
if isinstance(initial_backend, MockSignalBackend):
|
|
22
22
|
raise ValueError("Cannot make a MockSignalBackend for a MockSignalBackend")
|
|
@@ -34,11 +34,14 @@ class MockSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
34
34
|
|
|
35
35
|
# use existing Mock if provided
|
|
36
36
|
self.mock = mock
|
|
37
|
-
self.put_mock = AsyncMock(name="put", spec=Callable)
|
|
38
|
-
self.mock.attach_mock(self.put_mock, "put")
|
|
39
|
-
|
|
40
37
|
super().__init__(datatype=self.initial_backend.datatype)
|
|
41
38
|
|
|
39
|
+
@cached_property
|
|
40
|
+
def put_mock(self) -> AsyncMock:
|
|
41
|
+
put_mock = AsyncMock(name="put", spec=Callable)
|
|
42
|
+
self.mock().attach_mock(put_mock, "put")
|
|
43
|
+
return put_mock
|
|
44
|
+
|
|
42
45
|
def set_value(self, value: SignalDatatypeT):
|
|
43
46
|
self.soft_backend.set_value(value)
|
|
44
47
|
|
|
@@ -46,7 +49,7 @@ class MockSignalBackend(SignalBackend[SignalDatatypeT]):
|
|
|
46
49
|
return f"mock+{self.initial_backend.source(name, read)}"
|
|
47
50
|
|
|
48
51
|
async def connect(self, timeout: float) -> None:
|
|
49
|
-
|
|
52
|
+
raise RuntimeError("It is not possible to connect a MockSignalBackend")
|
|
50
53
|
|
|
51
54
|
@cached_property
|
|
52
55
|
def put_proceeds(self) -> asyncio.Event:
|
|
@@ -2,17 +2,26 @@ from collections.abc import Awaitable, Callable, Iterable
|
|
|
2
2
|
from contextlib import asynccontextmanager, contextmanager
|
|
3
3
|
from unittest.mock import AsyncMock, Mock
|
|
4
4
|
|
|
5
|
-
from ._device import Device
|
|
5
|
+
from ._device import Device
|
|
6
6
|
from ._mock_signal_backend import MockSignalBackend
|
|
7
|
-
from ._signal import Signal,
|
|
7
|
+
from ._signal import Signal, SignalConnector, SignalR
|
|
8
8
|
from ._soft_signal_backend import SignalDatatypeT
|
|
9
|
+
from ._utils import LazyMock
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_mock(device: Device | Signal) -> Mock:
|
|
13
|
+
mock = device._mock # noqa: SLF001
|
|
14
|
+
assert isinstance(mock, LazyMock), f"Device {device} not connected in mock mode"
|
|
15
|
+
return mock()
|
|
9
16
|
|
|
10
17
|
|
|
11
18
|
def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend:
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
connector = signal._connector # noqa: SLF001
|
|
20
|
+
assert isinstance(connector, SignalConnector), f"Expected Signal, got {signal}"
|
|
21
|
+
assert isinstance(
|
|
22
|
+
connector.backend, MockSignalBackend
|
|
14
23
|
), f"Signal {signal} not connected in mock mode"
|
|
15
|
-
return
|
|
24
|
+
return connector.backend
|
|
16
25
|
|
|
17
26
|
|
|
18
27
|
def set_mock_value(signal: Signal[SignalDatatypeT], value: SignalDatatypeT):
|
|
@@ -45,12 +54,6 @@ def get_mock_put(signal: Signal) -> AsyncMock:
|
|
|
45
54
|
return _get_mock_signal_backend(signal).put_mock
|
|
46
55
|
|
|
47
56
|
|
|
48
|
-
def get_mock(device: Device | Signal) -> Mock:
|
|
49
|
-
if isinstance(device, Signal):
|
|
50
|
-
return _get_mock_signal_backend(device).mock
|
|
51
|
-
return _device_mocks[device]
|
|
52
|
-
|
|
53
|
-
|
|
54
57
|
def reset_mock_put_calls(signal: Signal):
|
|
55
58
|
backend = _get_mock_signal_backend(signal)
|
|
56
59
|
backend.put_mock.reset_mock()
|
ophyd_async/core/_readable.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import warnings
|
|
2
|
-
from collections.abc import Callable, Generator, Sequence
|
|
2
|
+
from collections.abc import Awaitable, Callable, Generator, Sequence
|
|
3
3
|
from contextlib import contextmanager
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, cast
|
|
4
6
|
|
|
5
7
|
from bluesky.protocols import HasHints, Hints, Reading
|
|
6
8
|
from event_model import DataKey
|
|
@@ -11,11 +13,61 @@ from ._signal import SignalR
|
|
|
11
13
|
from ._status import AsyncStatus
|
|
12
14
|
from ._utils import merge_gathered_dicts
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
|
|
17
|
+
class StandardReadableFormat(Enum):
|
|
18
|
+
"""Declare how a `Device` should contribute to the `StandardReadable` verbs."""
|
|
19
|
+
|
|
20
|
+
#: Detect which verbs the child supports and contribute to:
|
|
21
|
+
#:
|
|
22
|
+
#: - ``read()``, ``describe()`` if it is `bluesky.protocols.Readable`
|
|
23
|
+
#: - ``read_configuration()``, ``describe_configuration()`` if it is
|
|
24
|
+
#: `bluesky.protocols.Configurable`
|
|
25
|
+
#: - ``stage()``, ``unstage()`` if it is `bluesky.protocols.Stageable`
|
|
26
|
+
#: - ``hints`` if it `bluesky.protocols.HasHints`
|
|
27
|
+
CHILD = "CHILD"
|
|
28
|
+
#: Contribute the `Signal` value to ``read_configuration()`` and
|
|
29
|
+
#: ``describe_configuration()``
|
|
30
|
+
CONFIG_SIGNAL = "CONFIG_SIGNAL"
|
|
31
|
+
#: Contribute the monitored `Signal` value to ``read()`` and ``describe()``` and
|
|
32
|
+
#: put the signal name in ``hints``
|
|
33
|
+
HINTED_SIGNAL = "HINTED_SIGNAL"
|
|
34
|
+
#: Contribute the uncached `Signal` value to ``read()`` and ``describe()```
|
|
35
|
+
UNCACHED_SIGNAL = "UNCACHED_SIGNAL"
|
|
36
|
+
#: Contribute the uncached `Signal` value to ``read()`` and ``describe()``` and
|
|
37
|
+
#: put the signal name in ``hints``
|
|
38
|
+
HINTED_UNCACHED_SIGNAL = "HINTED_UNCACHED_SIGNAL"
|
|
39
|
+
|
|
40
|
+
def __call__(self, parent: Device, child: Device):
|
|
41
|
+
if not isinstance(parent, StandardReadable):
|
|
42
|
+
raise TypeError(f"Expected parent to be StandardReadable, got {parent}")
|
|
43
|
+
parent.add_readables([child], self)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Back compat
|
|
47
|
+
class _WarningMatcher:
|
|
48
|
+
def __init__(self, name: str, target: StandardReadableFormat):
|
|
49
|
+
self._name = name
|
|
50
|
+
self._target = target
|
|
51
|
+
|
|
52
|
+
def __eq__(self, value: object) -> bool:
|
|
53
|
+
warnings.warn(
|
|
54
|
+
DeprecationWarning(
|
|
55
|
+
f"Use `StandardReadableFormat.{self._target.name}` "
|
|
56
|
+
f"instead of `{self._name}`"
|
|
57
|
+
),
|
|
58
|
+
stacklevel=2,
|
|
59
|
+
)
|
|
60
|
+
return value == self._target
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _compat_format(name: str, target: StandardReadableFormat) -> StandardReadableFormat:
|
|
64
|
+
return cast(StandardReadableFormat, _WarningMatcher(name, target))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
ConfigSignal = _compat_format("ConfigSignal", StandardReadableFormat.CONFIG_SIGNAL)
|
|
68
|
+
HintedSignal: Any = _compat_format("HintedSignal", StandardReadableFormat.HINTED_SIGNAL)
|
|
69
|
+
HintedSignal.uncached = _compat_format(
|
|
70
|
+
"HintedSignal.uncached", StandardReadableFormat.HINTED_UNCACHED_SIGNAL
|
|
19
71
|
)
|
|
20
72
|
|
|
21
73
|
|
|
@@ -31,38 +83,13 @@ class StandardReadable(
|
|
|
31
83
|
|
|
32
84
|
# These must be immutable types to avoid accidental sharing between
|
|
33
85
|
# different instances of the class
|
|
34
|
-
|
|
35
|
-
|
|
86
|
+
_describe_config_funcs: tuple[Callable[[], Awaitable[dict[str, DataKey]]], ...] = ()
|
|
87
|
+
_read_config_funcs: tuple[Callable[[], Awaitable[dict[str, Reading]]], ...] = ()
|
|
88
|
+
_describe_funcs: tuple[Callable[[], Awaitable[dict[str, DataKey]]], ...] = ()
|
|
89
|
+
_read_funcs: tuple[Callable[[], Awaitable[dict[str, Reading]]], ...] = ()
|
|
36
90
|
_stageables: tuple[AsyncStageable, ...] = ()
|
|
37
91
|
_has_hints: tuple[HasHints, ...] = ()
|
|
38
92
|
|
|
39
|
-
def set_readable_signals(
|
|
40
|
-
self,
|
|
41
|
-
read: Sequence[SignalR] = (),
|
|
42
|
-
config: Sequence[SignalR] = (),
|
|
43
|
-
read_uncached: Sequence[SignalR] = (),
|
|
44
|
-
):
|
|
45
|
-
"""
|
|
46
|
-
Parameters
|
|
47
|
-
----------
|
|
48
|
-
read:
|
|
49
|
-
Signals to make up :meth:`~StandardReadable.read`
|
|
50
|
-
conf:
|
|
51
|
-
Signals to make up :meth:`~StandardReadable.read_configuration`
|
|
52
|
-
read_uncached:
|
|
53
|
-
Signals to make up :meth:`~StandardReadable.read` that won't be cached
|
|
54
|
-
"""
|
|
55
|
-
warnings.warn(
|
|
56
|
-
DeprecationWarning(
|
|
57
|
-
"Migrate to `add_children_as_readables` context manager or "
|
|
58
|
-
"`add_readables` method"
|
|
59
|
-
),
|
|
60
|
-
stacklevel=2,
|
|
61
|
-
)
|
|
62
|
-
self.add_readables(read, wrapper=HintedSignal)
|
|
63
|
-
self.add_readables(config, wrapper=ConfigSignal)
|
|
64
|
-
self.add_readables(read_uncached, wrapper=HintedSignal.uncached)
|
|
65
|
-
|
|
66
93
|
@AsyncStatus.wrap
|
|
67
94
|
async def stage(self) -> None:
|
|
68
95
|
for sig in self._stageables:
|
|
@@ -75,19 +102,17 @@ class StandardReadable(
|
|
|
75
102
|
|
|
76
103
|
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
77
104
|
return await merge_gathered_dicts(
|
|
78
|
-
[
|
|
105
|
+
[func() for func in self._describe_config_funcs]
|
|
79
106
|
)
|
|
80
107
|
|
|
81
108
|
async def read_configuration(self) -> dict[str, Reading]:
|
|
82
|
-
return await merge_gathered_dicts(
|
|
83
|
-
[sig.read_configuration() for sig in self._configurables]
|
|
84
|
-
)
|
|
109
|
+
return await merge_gathered_dicts([func() for func in self._read_config_funcs])
|
|
85
110
|
|
|
86
111
|
async def describe(self) -> dict[str, DataKey]:
|
|
87
|
-
return await merge_gathered_dicts([
|
|
112
|
+
return await merge_gathered_dicts([func() for func in self._describe_funcs])
|
|
88
113
|
|
|
89
114
|
async def read(self) -> dict[str, Reading]:
|
|
90
|
-
return await merge_gathered_dicts([
|
|
115
|
+
return await merge_gathered_dicts([func() for func in self._read_funcs])
|
|
91
116
|
|
|
92
117
|
@property
|
|
93
118
|
def hints(self) -> Hints:
|
|
@@ -127,27 +152,13 @@ class StandardReadable(
|
|
|
127
152
|
@contextmanager
|
|
128
153
|
def add_children_as_readables(
|
|
129
154
|
self,
|
|
130
|
-
|
|
155
|
+
format: StandardReadableFormat = StandardReadableFormat.CHILD,
|
|
131
156
|
) -> Generator[None, None, None]:
|
|
132
|
-
"""Context manager
|
|
157
|
+
"""Context manager that calls `add_readables` on child Devices added within.
|
|
133
158
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
The provided wrapper class will be applied to all Devices and can be used to
|
|
138
|
-
specify their behaviour.
|
|
139
|
-
|
|
140
|
-
Parameters
|
|
141
|
-
----------
|
|
142
|
-
wrapper:
|
|
143
|
-
Wrapper class to apply to all Devices created inside the context manager.
|
|
144
|
-
|
|
145
|
-
See Also
|
|
146
|
-
--------
|
|
147
|
-
:func:`~StandardReadable.add_readables`
|
|
148
|
-
:class:`ConfigSignal`
|
|
149
|
-
:class:`HintedSignal`
|
|
150
|
-
:meth:`HintedSignal.uncached`
|
|
159
|
+
Scans ``self.children()`` on entry and exit to context manager, and calls
|
|
160
|
+
`add_readables` on any that are added with the provided
|
|
161
|
+
`StandardReadableFormat`.
|
|
151
162
|
"""
|
|
152
163
|
|
|
153
164
|
dict_copy = dict(self.children())
|
|
@@ -167,95 +178,83 @@ class StandardReadable(
|
|
|
167
178
|
flattened_values.append(value)
|
|
168
179
|
|
|
169
180
|
new_devices = list(filter(lambda x: isinstance(x, Device), flattened_values))
|
|
170
|
-
self.add_readables(new_devices,
|
|
181
|
+
self.add_readables(new_devices, format)
|
|
171
182
|
|
|
172
183
|
def add_readables(
|
|
173
184
|
self,
|
|
174
|
-
devices: Sequence[
|
|
175
|
-
|
|
185
|
+
devices: Sequence[Device],
|
|
186
|
+
format: StandardReadableFormat = StandardReadableFormat.CHILD,
|
|
176
187
|
) -> None:
|
|
177
|
-
"""Add
|
|
188
|
+
"""Add devices to contribute to various bluesky verbs.
|
|
178
189
|
|
|
179
|
-
|
|
180
|
-
interfaces
|
|
190
|
+
Use output from the given devices to contribute to the verbs of the following
|
|
191
|
+
interfaces:
|
|
181
192
|
|
|
182
|
-
|
|
183
|
-
|
|
193
|
+
- `bluesky.protocols.Readable`
|
|
194
|
+
- `bluesky.protocols.Configurable`
|
|
195
|
+
- `bluesky.protocols.Stageable`
|
|
196
|
+
- `bluesky.protocols.HasHints`
|
|
184
197
|
|
|
185
198
|
Parameters
|
|
186
199
|
----------
|
|
187
200
|
devices:
|
|
188
201
|
The devices to be added
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
See Also
|
|
193
|
-
--------
|
|
194
|
-
:func:`~StandardReadable.add_children_as_readables`
|
|
195
|
-
:class:`ConfigSignal`
|
|
196
|
-
:class:`HintedSignal`
|
|
197
|
-
:meth:`HintedSignal.uncached`
|
|
202
|
+
format:
|
|
203
|
+
Determines which of the devices functions are added to which verb as per the
|
|
204
|
+
`StandardReadableFormat` documentation
|
|
198
205
|
"""
|
|
199
206
|
|
|
200
|
-
for
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
207
|
+
for device in devices:
|
|
208
|
+
match format:
|
|
209
|
+
case StandardReadableFormat.CHILD:
|
|
210
|
+
if isinstance(device, AsyncConfigurable):
|
|
211
|
+
self._describe_config_funcs += (device.describe_configuration,)
|
|
212
|
+
self._read_config_funcs += (device.read_configuration,)
|
|
213
|
+
if isinstance(device, AsyncReadable):
|
|
214
|
+
self._describe_funcs += (device.describe,)
|
|
215
|
+
self._read_funcs += (device.read,)
|
|
216
|
+
if isinstance(device, AsyncStageable):
|
|
217
|
+
self._stageables += (device,)
|
|
218
|
+
if isinstance(device, HasHints):
|
|
219
|
+
self._has_hints += (device,)
|
|
220
|
+
case StandardReadableFormat.CONFIG_SIGNAL:
|
|
221
|
+
assert isinstance(device, SignalR), f"{device} is not a SignalR"
|
|
222
|
+
self._describe_config_funcs += (device.describe,)
|
|
223
|
+
self._read_config_funcs += (device.read,)
|
|
224
|
+
case StandardReadableFormat.HINTED_SIGNAL:
|
|
225
|
+
assert isinstance(device, SignalR), f"{device} is not a SignalR"
|
|
226
|
+
self._describe_funcs += (device.describe,)
|
|
227
|
+
self._read_funcs += (device.read,)
|
|
228
|
+
self._stageables += (device,)
|
|
229
|
+
self._has_hints += (_HintsFromName(device),)
|
|
230
|
+
case StandardReadableFormat.UNCACHED_SIGNAL:
|
|
231
|
+
assert isinstance(device, SignalR), f"{device} is not a SignalR"
|
|
232
|
+
self._describe_funcs += (device.describe,)
|
|
233
|
+
self._read_funcs += (_UncachedRead(device),)
|
|
234
|
+
case StandardReadableFormat.HINTED_UNCACHED_SIGNAL:
|
|
235
|
+
assert isinstance(device, SignalR), f"{device} is not a SignalR"
|
|
236
|
+
self._describe_funcs += (device.describe,)
|
|
237
|
+
self._read_funcs += (_UncachedRead(device),)
|
|
238
|
+
self._has_hints += (_HintsFromName(device),)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class _UncachedRead:
|
|
242
|
+
def __init__(self, signal: SignalR) -> None:
|
|
221
243
|
self.signal = signal
|
|
222
244
|
|
|
223
|
-
async def
|
|
224
|
-
return await self.signal.read()
|
|
225
|
-
|
|
226
|
-
async def describe_configuration(self) -> dict[str, DataKey]:
|
|
227
|
-
return await self.signal.describe()
|
|
228
|
-
|
|
229
|
-
@property
|
|
230
|
-
def name(self) -> str:
|
|
231
|
-
return self.signal.name
|
|
245
|
+
async def __call__(self) -> dict[str, Reading]:
|
|
246
|
+
return await self.signal.read(cached=False)
|
|
232
247
|
|
|
233
248
|
|
|
234
|
-
class
|
|
235
|
-
def __init__(self,
|
|
236
|
-
|
|
237
|
-
self.signal = signal
|
|
238
|
-
self.cached = None if allow_cache else allow_cache
|
|
239
|
-
if allow_cache:
|
|
240
|
-
self.stage = signal.stage
|
|
241
|
-
self.unstage = signal.unstage
|
|
242
|
-
|
|
243
|
-
async def read(self) -> dict[str, Reading]:
|
|
244
|
-
return await self.signal.read(cached=self.cached)
|
|
245
|
-
|
|
246
|
-
async def describe(self) -> dict[str, DataKey]:
|
|
247
|
-
return await self.signal.describe()
|
|
249
|
+
class _HintsFromName(HasHints):
|
|
250
|
+
def __init__(self, device: Device) -> None:
|
|
251
|
+
self.device = device
|
|
248
252
|
|
|
249
253
|
@property
|
|
250
254
|
def name(self) -> str:
|
|
251
|
-
return self.
|
|
255
|
+
return self.device.name
|
|
252
256
|
|
|
253
257
|
@property
|
|
254
258
|
def hints(self) -> Hints:
|
|
255
|
-
if self.
|
|
256
|
-
|
|
257
|
-
return {"fields": [self.signal.name]}
|
|
258
|
-
|
|
259
|
-
@classmethod
|
|
260
|
-
def uncached(cls, signal: ReadableChild) -> "HintedSignal":
|
|
261
|
-
return cls(signal, allow_cache=False)
|
|
259
|
+
fields = [self.name] if self.name else []
|
|
260
|
+
return {"fields": fields}
|
ophyd_async/core/_signal.py
CHANGED
|
@@ -4,7 +4,6 @@ import asyncio
|
|
|
4
4
|
import functools
|
|
5
5
|
from collections.abc import AsyncGenerator, Awaitable, Callable, Mapping
|
|
6
6
|
from typing import Any, Generic, cast
|
|
7
|
-
from unittest.mock import Mock
|
|
8
7
|
|
|
9
8
|
from bluesky.protocols import (
|
|
10
9
|
Locatable,
|
|
@@ -30,9 +29,14 @@ from ._signal_backend import (
|
|
|
30
29
|
)
|
|
31
30
|
from ._soft_signal_backend import SoftSignalBackend
|
|
32
31
|
from ._status import AsyncStatus
|
|
33
|
-
from ._utils import
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
from ._utils import (
|
|
33
|
+
CALCULATE_TIMEOUT,
|
|
34
|
+
DEFAULT_TIMEOUT,
|
|
35
|
+
CalculatableTimeout,
|
|
36
|
+
Callback,
|
|
37
|
+
LazyMock,
|
|
38
|
+
T,
|
|
39
|
+
)
|
|
36
40
|
|
|
37
41
|
|
|
38
42
|
async def _wait_for(coro: Awaitable[T], timeout: float | None, source: str) -> T:
|
|
@@ -54,26 +58,28 @@ class SignalConnector(DeviceConnector):
|
|
|
54
58
|
def __init__(self, backend: SignalBackend):
|
|
55
59
|
self.backend = self._init_backend = backend
|
|
56
60
|
|
|
57
|
-
async def
|
|
58
|
-
self,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
force_reconnect: bool,
|
|
63
|
-
):
|
|
64
|
-
if mock:
|
|
65
|
-
self.backend = MockSignalBackend(self._init_backend, mock)
|
|
66
|
-
_mock_signal_backends[device] = self.backend
|
|
67
|
-
else:
|
|
68
|
-
self.backend = self._init_backend
|
|
61
|
+
async def connect_mock(self, device: Device, mock: LazyMock):
|
|
62
|
+
self.backend = MockSignalBackend(self._init_backend, mock)
|
|
63
|
+
|
|
64
|
+
async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
|
|
65
|
+
self.backend = self._init_backend
|
|
69
66
|
device.log.debug(f"Connecting to {self.backend.source(device.name, read=True)}")
|
|
70
67
|
await self.backend.connect(timeout)
|
|
71
68
|
|
|
72
69
|
|
|
70
|
+
class _ChildrenNotAllowed(dict[str, Device]):
|
|
71
|
+
def __setitem__(self, key: str, value: Device) -> None:
|
|
72
|
+
raise AttributeError(
|
|
73
|
+
f"Cannot add Device or Signal child {key}={value} of Signal, "
|
|
74
|
+
"make a subclass of Device instead"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
73
78
|
class Signal(Device, Generic[SignalDatatypeT]):
|
|
74
79
|
"""A Device with the concept of a value, with R, RW, W and X flavours"""
|
|
75
80
|
|
|
76
81
|
_connector: SignalConnector
|
|
82
|
+
_child_devices = _ChildrenNotAllowed() # type: ignore
|
|
77
83
|
|
|
78
84
|
def __init__(
|
|
79
85
|
self,
|
|
@@ -89,14 +95,6 @@ class Signal(Device, Generic[SignalDatatypeT]):
|
|
|
89
95
|
"""Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
|
|
90
96
|
return self._connector.backend.source(self.name, read=True)
|
|
91
97
|
|
|
92
|
-
def __setattr__(self, name: str, value: Any) -> None:
|
|
93
|
-
if name != "parent" and isinstance(value, Device):
|
|
94
|
-
raise AttributeError(
|
|
95
|
-
f"Cannot add Device or Signal {value} as a child of Signal {self}, "
|
|
96
|
-
"make a subclass of Device instead"
|
|
97
|
-
)
|
|
98
|
-
return super().__setattr__(name, value)
|
|
99
|
-
|
|
100
98
|
|
|
101
99
|
class _SignalCache(Generic[SignalDatatypeT]):
|
|
102
100
|
def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal):
|
|
@@ -4,6 +4,7 @@ import time
|
|
|
4
4
|
from abc import abstractmethod
|
|
5
5
|
from collections.abc import Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
+
from functools import lru_cache
|
|
7
8
|
from typing import Any, Generic, get_origin
|
|
8
9
|
|
|
9
10
|
import numpy as np
|
|
@@ -90,6 +91,7 @@ class TableSoftConverter(SoftConverter[TableT]):
|
|
|
90
91
|
raise TypeError(f"Cannot convert {value} to {self.datatype}")
|
|
91
92
|
|
|
92
93
|
|
|
94
|
+
@lru_cache
|
|
93
95
|
def make_converter(datatype: type[SignalDatatype]) -> SoftConverter:
|
|
94
96
|
enum_cls = get_enum_cls(datatype)
|
|
95
97
|
if datatype == Sequence[str]:
|
ophyd_async/core/_utils.py
CHANGED
|
@@ -5,7 +5,16 @@ import logging
|
|
|
5
5
|
from collections.abc import Awaitable, Callable, Iterable, Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from enum import Enum, EnumMeta
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Generic,
|
|
11
|
+
Literal,
|
|
12
|
+
ParamSpec,
|
|
13
|
+
TypeVar,
|
|
14
|
+
get_args,
|
|
15
|
+
get_origin,
|
|
16
|
+
)
|
|
17
|
+
from unittest.mock import Mock
|
|
9
18
|
|
|
10
19
|
import numpy as np
|
|
11
20
|
|
|
@@ -112,20 +121,29 @@ async def wait_for_connection(**coros: Awaitable[None]):
|
|
|
112
121
|
|
|
113
122
|
Expected kwargs should be a mapping of names to coroutine tasks to execute.
|
|
114
123
|
"""
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
exceptions: dict[str, Exception] = {}
|
|
125
|
+
if len(coros) == 1:
|
|
126
|
+
# Single device optimization
|
|
127
|
+
name, coro = coros.popitem()
|
|
128
|
+
try:
|
|
129
|
+
await coro
|
|
130
|
+
except Exception as e:
|
|
131
|
+
exceptions[name] = e
|
|
132
|
+
else:
|
|
133
|
+
# Use gather to connect in parallel
|
|
134
|
+
results = await asyncio.gather(*coros.values(), return_exceptions=True)
|
|
135
|
+
for name, result in zip(coros, results, strict=False):
|
|
136
|
+
if isinstance(result, Exception):
|
|
137
|
+
exceptions[name] = result
|
|
117
138
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if not isinstance(result, NotConnected):
|
|
139
|
+
if exceptions:
|
|
140
|
+
for name, exception in exceptions.items():
|
|
141
|
+
if not isinstance(exception, NotConnected):
|
|
122
142
|
logging.exception(
|
|
123
143
|
f"device `{name}` raised unexpected exception "
|
|
124
|
-
f"{type(
|
|
125
|
-
exc_info=
|
|
144
|
+
f"{type(exception).__name__}",
|
|
145
|
+
exc_info=exception,
|
|
126
146
|
)
|
|
127
|
-
|
|
128
|
-
if exceptions:
|
|
129
147
|
raise NotConnected(exceptions)
|
|
130
148
|
|
|
131
149
|
|
|
@@ -244,3 +262,38 @@ class Reference(Generic[T]):
|
|
|
244
262
|
|
|
245
263
|
def __call__(self) -> T:
|
|
246
264
|
return self._obj
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class LazyMock:
|
|
268
|
+
"""A lazily created Mock to be used when connecting in mock mode.
|
|
269
|
+
|
|
270
|
+
Creating Mocks is reasonably expensive when each Device (and Signal)
|
|
271
|
+
requires its own, and the tree is only used when ``Signal.set()`` is
|
|
272
|
+
called. This class allows a tree of lazily connected Mocks to be
|
|
273
|
+
constructed so that when the leaf is created, so are its parents.
|
|
274
|
+
Any calls to the child are then accessible from the parent mock.
|
|
275
|
+
|
|
276
|
+
>>> parent = LazyMock()
|
|
277
|
+
>>> child = parent.child("child")
|
|
278
|
+
>>> child_mock = child()
|
|
279
|
+
>>> child_mock() # doctest: +ELLIPSIS
|
|
280
|
+
<Mock name='mock.child()' id='...'>
|
|
281
|
+
>>> parent_mock = parent()
|
|
282
|
+
>>> parent_mock.mock_calls
|
|
283
|
+
[call.child()]
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
def __init__(self, name: str = "", parent: LazyMock | None = None) -> None:
|
|
287
|
+
self.parent = parent
|
|
288
|
+
self.name = name
|
|
289
|
+
self._mock: Mock | None = None
|
|
290
|
+
|
|
291
|
+
def child(self, name: str) -> LazyMock:
|
|
292
|
+
return LazyMock(name, self)
|
|
293
|
+
|
|
294
|
+
def __call__(self) -> Mock:
|
|
295
|
+
if self._mock is None:
|
|
296
|
+
self._mock = Mock(spec=object)
|
|
297
|
+
if self.parent is not None:
|
|
298
|
+
self.parent().attach_mock(self._mock, self.name)
|
|
299
|
+
return self._mock
|
|
@@ -3,13 +3,8 @@ from collections.abc import Sequence
|
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import Triggerable
|
|
5
5
|
|
|
6
|
-
from ophyd_async.core import
|
|
7
|
-
|
|
8
|
-
ConfigSignal,
|
|
9
|
-
HintedSignal,
|
|
10
|
-
SignalR,
|
|
11
|
-
StandardReadable,
|
|
12
|
-
)
|
|
6
|
+
from ophyd_async.core import AsyncStatus, SignalR, StandardReadable
|
|
7
|
+
from ophyd_async.core import StandardReadableFormat as Format
|
|
13
8
|
|
|
14
9
|
from ._core_io import ADBaseIO, NDPluginBaseIO
|
|
15
10
|
from ._utils import ImageMode
|
|
@@ -24,14 +19,15 @@ class SingleTriggerDetector(StandardReadable, Triggerable):
|
|
|
24
19
|
**plugins: NDPluginBaseIO,
|
|
25
20
|
) -> None:
|
|
26
21
|
self.drv = drv
|
|
27
|
-
|
|
22
|
+
for k, v in plugins.items():
|
|
23
|
+
setattr(self, k, v)
|
|
28
24
|
|
|
29
25
|
self.add_readables(
|
|
30
26
|
[self.drv.array_counter, *read_uncached],
|
|
31
|
-
|
|
27
|
+
Format.HINTED_UNCACHED_SIGNAL,
|
|
32
28
|
)
|
|
33
29
|
|
|
34
|
-
self.add_readables([self.drv.acquire_time],
|
|
30
|
+
self.add_readables([self.drv.acquire_time], Format.CONFIG_SIGNAL)
|
|
35
31
|
|
|
36
32
|
super().__init__(name=name)
|
|
37
33
|
|