ophyd-async 0.8.0a3__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 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.0a3'
15
+ __version__ = version = '0.8.0a4'
16
16
  __version_tuple__ = version_tuple = (0, 8, 0)
@@ -83,6 +83,7 @@ from ._utils import (
83
83
  DEFAULT_TIMEOUT,
84
84
  CalculatableTimeout,
85
85
  Callback,
86
+ LazyMock,
86
87
  NotConnected,
87
88
  Reference,
88
89
  StrictEnum,
@@ -176,6 +177,7 @@ __all__ = [
176
177
  "DEFAULT_TIMEOUT",
177
178
  "CalculatableTimeout",
178
179
  "Callback",
180
+ "LazyMock",
179
181
  "CALCULATE_TIMEOUT",
180
182
  "NotConnected",
181
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,9 +63,8 @@ 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
@@ -83,10 +78,18 @@ class Device(HasName, Connectable):
83
78
  """Return the name of the Device"""
84
79
  return self._name
85
80
 
81
+ @cached_property
82
+ def _child_devices(self) -> dict[str, Device]:
83
+ return {}
84
+
86
85
  def children(self) -> Iterator[tuple[str, Device]]:
87
- for attr_name, attr in self.__dict__.items():
88
- if attr_name != "parent" and isinstance(attr, Device):
89
- 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
+ )
90
93
 
91
94
  def set_name(self, name: str):
92
95
  """Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
@@ -97,28 +100,33 @@ class Device(HasName, Connectable):
97
100
  New name to set
98
101
  """
99
102
  self._name = name
100
- # Ensure self.log is recreated after a name change
101
- self.log = LoggerAdapter(
102
- getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
103
- )
103
+ # Ensure logger is recreated after a name change
104
+ if "log" in self.__dict__:
105
+ del self.log
104
106
  for child_name, child in self.children():
105
107
  child_name = f"{self.name}-{child_name.strip('_')}" if self.name else ""
106
108
  child.set_name(child_name)
107
109
 
108
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...
109
113
  if name == "parent":
110
114
  if self.parent not in (value, None):
111
115
  raise TypeError(
112
116
  f"Cannot set the parent of {self} to be {value}: "
113
117
  f"it is already a child of {self.parent}"
114
118
  )
115
- 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):
116
122
  value.parent = self
117
- 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)
118
126
 
119
127
  async def connect(
120
128
  self,
121
- mock: bool | Mock = False,
129
+ mock: bool | LazyMock = False,
122
130
  timeout: float = DEFAULT_TIMEOUT,
123
131
  force_reconnect: bool = False,
124
132
  ) -> None:
@@ -133,26 +141,39 @@ class Device(HasName, Connectable):
133
141
  timeout:
134
142
  Time to wait before failing with a TimeoutError.
135
143
  """
136
- uses_mock = bool(mock)
137
- can_use_previous_connect = (
138
- uses_mock is self._connect_mock_arg
139
- and self._connect_task
140
- and not (self._connect_task.done() and self._connect_task.exception())
141
- )
142
- if mock is True:
143
- mock = Mock() # create a new Mock if one not provided
144
- if force_reconnect or not can_use_previous_connect:
145
- self._connect_mock_arg = uses_mock
146
- if self._connect_mock_arg:
147
- _device_mocks[self] = mock
148
- coro = self._connector.connect(
149
- 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())
150
159
  )
151
- self._connect_task = asyncio.create_task(coro)
152
-
153
- assert self._connect_task, "Connect task not created, this shouldn't happen"
154
- # Wait for it to complete
155
- 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
+ }
156
177
 
157
178
 
158
179
  DeviceT = TypeVar("DeviceT", bound=Device)
@@ -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, Mock
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: 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
- pass
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, _device_mocks
5
+ from ._device import Device
6
6
  from ._mock_signal_backend import MockSignalBackend
7
- from ._signal import Signal, SignalR, _mock_signal_backends
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
- assert (
13
- signal in _mock_signal_backends
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 _mock_signal_backends[signal]
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()
@@ -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 CALCULATE_TIMEOUT, DEFAULT_TIMEOUT, CalculatableTimeout, Callback, T
34
-
35
- _mock_signal_backends: dict[Device, MockSignalBackend] = {}
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 connect(
58
- self,
59
- device: Device,
60
- mock: bool | Mock,
61
- timeout: float,
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]:
@@ -14,6 +14,7 @@ from typing import (
14
14
  get_args,
15
15
  get_origin,
16
16
  )
17
+ from unittest.mock import Mock
17
18
 
18
19
  import numpy as np
19
20
 
@@ -120,20 +121,29 @@ async def wait_for_connection(**coros: Awaitable[None]):
120
121
 
121
122
  Expected kwargs should be a mapping of names to coroutine tasks to execute.
122
123
  """
123
- results = await asyncio.gather(*coros.values(), return_exceptions=True)
124
- exceptions = {}
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
125
138
 
126
- for name, result in zip(coros, results, strict=False):
127
- if isinstance(result, Exception):
128
- exceptions[name] = result
129
- if not isinstance(result, NotConnected):
139
+ if exceptions:
140
+ for name, exception in exceptions.items():
141
+ if not isinstance(exception, NotConnected):
130
142
  logging.exception(
131
143
  f"device `{name}` raised unexpected exception "
132
- f"{type(result).__name__}",
133
- exc_info=result,
144
+ f"{type(exception).__name__}",
145
+ exc_info=exception,
134
146
  )
135
-
136
- if exceptions:
137
147
  raise NotConnected(exceptions)
138
148
 
139
149
 
@@ -252,3 +262,38 @@ class Reference(Generic[T]):
252
262
 
253
263
  def __call__(self) -> T:
254
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
@@ -19,7 +19,8 @@ class SingleTriggerDetector(StandardReadable, Triggerable):
19
19
  **plugins: NDPluginBaseIO,
20
20
  ) -> None:
21
21
  self.drv = drv
22
- self.__dict__.update(plugins)
22
+ for k, v in plugins.items():
23
+ setattr(self, k, v)
23
24
 
24
25
  self.add_readables(
25
26
  [self.drv.array_counter, *read_uncached],
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from unittest.mock import Mock
4
-
5
3
  from ophyd_async.core import (
6
4
  Device,
7
5
  DeviceConnector,
@@ -11,6 +9,7 @@ from ophyd_async.core import (
11
9
  SignalRW,
12
10
  SignalX,
13
11
  )
12
+ from ophyd_async.core._utils import LazyMock
14
13
 
15
14
  from ._epics_connector import fill_backend_with_prefix
16
15
  from ._signal import PvaSignalBackend, pvget_with_timeout
@@ -64,29 +63,29 @@ class PviDeviceConnector(DeviceConnector):
64
63
  backend.read_pv = read_pv
65
64
  backend.write_pv = write_pv
66
65
 
67
- async def connect(
68
- self, device: Device, mock: bool | Mock, timeout: float, force_reconnect: bool
66
+ async def connect_mock(self, device: Device, mock: LazyMock):
67
+ self.filler.create_device_vector_entries_to_mock(2)
68
+ # Set the name of the device to name all children
69
+ device.set_name(device.name)
70
+ return await super().connect_mock(device, mock)
71
+
72
+ async def connect_real(
73
+ self, device: Device, timeout: float, force_reconnect: bool
69
74
  ) -> None:
70
- if mock:
71
- # Make 2 entries for each DeviceVector
72
- self.filler.create_device_vector_entries_to_mock(2)
73
- else:
74
- pvi_structure = await pvget_with_timeout(self.pvi_pv, timeout)
75
- entries: dict[str, Entry | list[Entry | None]] = pvi_structure[
76
- "value"
77
- ].todict()
78
- # Fill based on what PVI gives us
79
- for name, entry in entries.items():
80
- if isinstance(entry, dict):
81
- # This is a child
82
- self._fill_child(name, entry)
83
- else:
84
- # This is a DeviceVector of children
85
- for i, e in enumerate(entry):
86
- if e:
87
- self._fill_child(name, e, i)
88
- # Check that all the requested children have been filled
89
- self.filler.check_filled(f"{self.pvi_pv}: {entries}")
75
+ pvi_structure = await pvget_with_timeout(self.pvi_pv, timeout)
76
+ entries: dict[str, Entry | list[Entry | None]] = pvi_structure["value"].todict()
77
+ # Fill based on what PVI gives us
78
+ for name, entry in entries.items():
79
+ if isinstance(entry, dict):
80
+ # This is a child
81
+ self._fill_child(name, entry)
82
+ else:
83
+ # This is a DeviceVector of children
84
+ for i, e in enumerate(entry):
85
+ if e:
86
+ self._fill_child(name, e, i)
87
+ # Check that all the requested children have been filled
88
+ self.filler.check_filled(f"{self.pvi_pv}: {entries}")
90
89
  # Set the name of the device to name all children
91
90
  device.set_name(device.name)
92
- return await super().connect(device, mock, timeout, force_reconnect)
91
+ return await super().connect_real(device, timeout, force_reconnect)
@@ -1,13 +1,11 @@
1
- from unittest.mock import Mock
2
-
3
1
  import bluesky.plan_stubs as bps
4
2
 
5
- from ophyd_async.core import DEFAULT_TIMEOUT, Device, wait_for_connection
3
+ from ophyd_async.core import DEFAULT_TIMEOUT, Device, LazyMock, wait_for_connection
6
4
 
7
5
 
8
6
  def ensure_connected(
9
7
  *devices: Device,
10
- mock: bool | Mock = False,
8
+ mock: bool | LazyMock = False,
11
9
  timeout: float = DEFAULT_TIMEOUT,
12
10
  force_reconnect=False,
13
11
  ):
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import TypeVar
4
- from unittest.mock import Mock
5
4
 
6
5
  from ophyd_async.core import Device, DeviceConnector, DeviceFiller
6
+ from ophyd_async.core._utils import LazyMock
7
7
  from ophyd_async.tango.signal import (
8
8
  TangoSignalBackend,
9
9
  infer_python_type,
@@ -117,41 +117,42 @@ class TangoDeviceConnector(DeviceConnector):
117
117
  list(self.filler.create_signals_from_annotations(filled=False))
118
118
  self.filler.check_created()
119
119
 
120
- async def connect(
121
- self, device: Device, mock: bool | Mock, timeout: float, force_reconnect: bool
122
- ) -> None:
123
- if mock:
124
- # Make 2 entries for each DeviceVector
125
- self.filler.create_device_vector_entries_to_mock(2)
120
+ async def connect_mock(self, device: Device, mock: LazyMock):
121
+ # Make 2 entries for each DeviceVector
122
+ self.filler.create_device_vector_entries_to_mock(2)
123
+ # Set the name of the device to name all children
124
+ device.set_name(device.name)
125
+ return await super().connect_mock(device, mock)
126
+
127
+ async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
128
+ if self.trl and self.proxy is None:
129
+ self.proxy = await AsyncDeviceProxy(self.trl)
130
+ elif self.proxy and not self.trl:
131
+ self.trl = self.proxy.name()
126
132
  else:
127
- if self.trl and self.proxy is None:
128
- self.proxy = await AsyncDeviceProxy(self.trl)
129
- elif self.proxy and not self.trl:
130
- self.trl = self.proxy.name()
131
- else:
132
- raise TypeError("Neither proxy nor trl supplied")
133
-
134
- children = sorted(
135
- set()
136
- .union(self.proxy.get_attribute_list())
137
- .union(self.proxy.get_command_list())
138
- )
139
- for name in children:
140
- # TODO: strip attribute name
141
- full_trl = f"{self.trl}/{name}"
142
- signal_type = await infer_signal_type(full_trl, self.proxy)
143
- if signal_type:
144
- backend = self.filler.fill_child_signal(name, signal_type)
145
- backend.datatype = await infer_python_type(full_trl, self.proxy)
146
- backend.set_trl(full_trl)
147
- if polling := self._signal_polling.get(name, ()):
148
- backend.set_polling(*polling)
149
- backend.allow_events(False)
150
- elif self._polling[0]:
151
- backend.set_polling(*self._polling)
152
- backend.allow_events(False)
153
- # Check that all the requested children have been filled
154
- self.filler.check_filled(f"{self.trl}: {children}")
133
+ raise TypeError("Neither proxy nor trl supplied")
134
+
135
+ children = sorted(
136
+ set()
137
+ .union(self.proxy.get_attribute_list())
138
+ .union(self.proxy.get_command_list())
139
+ )
140
+ for name in children:
141
+ # TODO: strip attribute name
142
+ full_trl = f"{self.trl}/{name}"
143
+ signal_type = await infer_signal_type(full_trl, self.proxy)
144
+ if signal_type:
145
+ backend = self.filler.fill_child_signal(name, signal_type)
146
+ backend.datatype = await infer_python_type(full_trl, self.proxy)
147
+ backend.set_trl(full_trl)
148
+ if polling := self._signal_polling.get(name, ()):
149
+ backend.set_polling(*polling)
150
+ backend.allow_events(False)
151
+ elif self._polling[0]:
152
+ backend.set_polling(*self._polling)
153
+ backend.allow_events(False)
154
+ # Check that all the requested children have been filled
155
+ self.filler.check_filled(f"{self.trl}: {children}")
155
156
  # Set the name of the device to name all children
156
157
  device.set_name(device.name)
157
- return await super().connect(device, mock, timeout, force_reconnect)
158
+ return await super().connect_real(device, timeout, force_reconnect)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ophyd-async
3
- Version: 0.8.0a3
3
+ Version: 0.8.0a4
4
4
  Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
5
5
  Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
6
6
  License: BSD 3-Clause License
@@ -1,26 +1,26 @@
1
1
  ophyd_async/__init__.py,sha256=tEfgj45lRItQ-_u8SRFPM-mpBh3gWvHXr3emhiJJG_M,225
2
2
  ophyd_async/__main__.py,sha256=n_U4O9bgm97OuboUB_9eK7eFiwy8BZSgXJ0OzbE0DqU,481
3
- ophyd_async/_version.py,sha256=7HtuNUHmvgApFDFxIXlpPtAL7GP25idINU4Fd3AJSWE,413
3
+ ophyd_async/_version.py,sha256=_oysVx3AtIs7YHDdNWu47vFG8nzgweLVt7KUik2-u0Q,413
4
4
  ophyd_async/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- ophyd_async/core/__init__.py,sha256=0UUwdcqeXFVxGDBLIbZkA5cZx4rbqdud6n4rCfcrpIY,4253
5
+ ophyd_async/core/__init__.py,sha256=JBKq2w50jBjxg_2KRL1jXnIzETaj8dPOD47_prgtotU,4283
6
6
  ophyd_async/core/_detector.py,sha256=bKLekM2J3GzLXsKwe8qXQjNP_sAVsa8EtwFEWD-8MeA,14307
7
- ophyd_async/core/_device.py,sha256=AJvqPRfN4LOSVptPC9hyyabH2gBUFMLMNpugWsP501E,11019
7
+ ophyd_async/core/_device.py,sha256=AMuYtsWtce5rebeHvb5bntZkNLpfnJV9XgQLp0zd5Ks,11802
8
8
  ophyd_async/core/_device_filler.py,sha256=Nw-DUyuXYpvt4mmCAQaNVA0LFBBaPK84ubZo3bR39Ak,11407
9
9
  ophyd_async/core/_device_save_loader.py,sha256=OViN9_LWNOLuajzrHDKYEqd5I47u5npQACdGceKcIGY,8375
10
10
  ophyd_async/core/_flyer.py,sha256=us5z6MNGCvIfgPDTmFTxNERSP37g0WVRkRD0Z2JiMgM,1701
11
11
  ophyd_async/core/_hdf_dataset.py,sha256=wW_OL8OYLGOsE01ny3hGaapOrxK7BzhWTxKgz8CIXK0,2492
12
12
  ophyd_async/core/_log.py,sha256=UbL9AtnHVUg7r9LofzgmuKEtBESy03usCp7ejmDltG4,3679
13
- ophyd_async/core/_mock_signal_backend.py,sha256=E8YMLJJyAt8UAwX-5PMQCuAuHuVVs5ko7vJtY5qR9P8,2656
14
- ophyd_async/core/_mock_signal_utils.py,sha256=i7CCFeB6JkTpdAe32Jtap2nrvFL-gbz_3zrnlK7Lbow,5139
13
+ ophyd_async/core/_mock_signal_backend.py,sha256=8Upnz6QrSigeDXemjZ-jB4sV2yIPUzid-6GOfTZ-7Io,2805
14
+ ophyd_async/core/_mock_signal_utils.py,sha256=YeKjStClwp1etlmHMx1tb_VV1GjeFPg83Hkq7-YPkpg,5306
15
15
  ophyd_async/core/_protocol.py,sha256=MuYRqSfakdry9RllX7G9UTzp4lw3eDjtkdGPpnbNb34,4040
16
16
  ophyd_async/core/_providers.py,sha256=ff9ZT5-PZ6rhTTdE-q8w9l_k9DuZqLWLebsKZLeJ0Ds,7112
17
17
  ophyd_async/core/_readable.py,sha256=7FxqxhAT1wBQqOEivgnY731zA9QoK1Tt-ZGcH7GBOXM,10623
18
- ophyd_async/core/_signal.py,sha256=4Umj1EXgz89ZO1Ej-b5TEkmr72pdw3Jbyk-nIsYIYpo,20011
18
+ ophyd_async/core/_signal.py,sha256=HO3XkSvs_5t6yJcZAHCaxOfGy8AE0B9c9sDwlG4x21g,19847
19
19
  ophyd_async/core/_signal_backend.py,sha256=YWPgLSPbfPnWIUDHvP1ArCVK8zKXJxzzbloqQe_ucCI,5040
20
- ophyd_async/core/_soft_signal_backend.py,sha256=VBemKbSM397zhtjm6zLKozhJ8cY3XhDu42OagBc_d64,5663
20
+ ophyd_async/core/_soft_signal_backend.py,sha256=d74wML22E3H81W6xsPIj44ghw3jP51Jph4vCLGFwB2k,5706
21
21
  ophyd_async/core/_status.py,sha256=OUKhblRQ4KU5PDsWbpvYduM7G60JMk1NqeV4eqyPtKc,5131
22
22
  ophyd_async/core/_table.py,sha256=ZToBVmAPDmhrVDgjx0f8SErxVdKhvGdGwQ-fXxGCtN8,5386
23
- ophyd_async/core/_utils.py,sha256=agOw4xTv2NFPfywDUkY_NYT2GNXI6p7GGs08z0H51jk,7548
23
+ ophyd_async/core/_utils.py,sha256=230vayCyT1xsZDjpr7JRcax8zYTxImsf9gm5GiZtnZ8,9132
24
24
  ophyd_async/epics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  ophyd_async/epics/motor.py,sha256=pujJXV_vslvo3AxsVySTAEoFuduuv5Hp6sz8aRvIbeQ,8792
26
26
  ophyd_async/epics/signal.py,sha256=hJCGIIWjRVhjEHkeL1I_oPEaaN7dDFKmm7G7ZmgoTYQ,219
@@ -32,7 +32,7 @@ ophyd_async/epics/adcore/__init__.py,sha256=3wMOyFGaq1X61LqK4iY4pq-m_BjhOgYZD2-m
32
32
  ophyd_async/epics/adcore/_core_io.py,sha256=ZQjRLdpFMVS9kwEm5LAh60pxiy7XWyYtc2TzEvCEVYM,6076
33
33
  ophyd_async/epics/adcore/_core_logic.py,sha256=JjrSmKErRFSv1C98or1Upwi01k3NWDRMi2fPHVWMmWw,3561
34
34
  ophyd_async/epics/adcore/_hdf_writer.py,sha256=eWT9SH7uegf9rpCWRmVCZTsOxF1drPaOAMmoXm99mVk,7215
35
- ophyd_async/epics/adcore/_single_trigger.py,sha256=tu1kZ1zsqgL0PM3ibmnxabNUA3gXaluQ6JW5C093tYc,1177
35
+ ophyd_async/epics/adcore/_single_trigger.py,sha256=7SzmadatWk4zXIweRIhVX5odc__ZZKuGicL7vlW0JbY,1208
36
36
  ophyd_async/epics/adcore/_utils.py,sha256=MZBKeSPIRzyo6f84MpzPp28KwOLa9qgrkMIFc618wOE,3932
37
37
  ophyd_async/epics/adkinetix/__init__.py,sha256=cvnwOqbvEENf70eFp6bPGwayP0u14UTIhs3WiZEcF_Q,262
38
38
  ophyd_async/epics/adkinetix/_kinetix.py,sha256=JsQVc4d7lRCnVObJlM775hHLfp3rYRSRgOoIyRvrSek,1184
@@ -54,7 +54,7 @@ ophyd_async/epics/core/_aioca.py,sha256=318lw_dGWnckgyQ1f4K8QeX_KVOD4idzWX8sx2jb
54
54
  ophyd_async/epics/core/_epics_connector.py,sha256=n1FlQYui8HdobPxaX3VAflrzi2UT7QCe3cFasssmVLw,1789
55
55
  ophyd_async/epics/core/_epics_device.py,sha256=kshNiKQhevsL2OZXa-r093L_sQGvGK_0J4PWVLg3Eqw,437
56
56
  ophyd_async/epics/core/_p4p.py,sha256=Ap_WVWCa4Eb44i50bxjy8qS-n8AUKTaLbFNq5eZZk0w,14619
57
- ophyd_async/epics/core/_pvi_connector.py,sha256=D4IM_SiNkUEe01DsstTX-s2XEgT3X-jspGmZePSCYZE,3577
57
+ ophyd_async/epics/core/_pvi_connector.py,sha256=Rjc8g3Rdny_O-4JxhoCpD4L7XWIRq-lnGHXKpsIUrSU,3621
58
58
  ophyd_async/epics/core/_signal.py,sha256=jHdMXV1-0bd7PC8XV32Sso1xgubZVDhWFNsWV-UuamQ,4642
59
59
  ophyd_async/epics/core/_util.py,sha256=6CCWDfp54WeBIJdGjg_YBVZTKoNjponWyykMmLPrj7U,1820
60
60
  ophyd_async/epics/demo/__init__.py,sha256=wCrgemEo-zR4TTvaqCKnQ-AIUHorotV5jhftbq1tXz0,1368
@@ -79,7 +79,7 @@ ophyd_async/fastcs/panda/_trigger.py,sha256=forImtdnDnaZ0KKhqSxCqwHWXq13SJ4mn9wd
79
79
  ophyd_async/fastcs/panda/_utils.py,sha256=NdvzdKy0SOG1eCVMQo_nwRXpBo0wyi6lM5Xw3HvssOw,508
80
80
  ophyd_async/fastcs/panda/_writer.py,sha256=wDN6uWX1ENofmI3JBXJ7_CGooI7WsZP-JJQrRiSc6sM,6000
81
81
  ophyd_async/plan_stubs/__init__.py,sha256=wjpEj_BoBZJ9x2fhUPY6BzWMqyYH96JrBlJvV7frdN4,524
82
- ophyd_async/plan_stubs/_ensure_connected.py,sha256=MIn-aWKiaGI0k7ac-_Ca40uouoGsyRX1gAHY-A-ifGI,750
82
+ ophyd_async/plan_stubs/_ensure_connected.py,sha256=ofMDgOLc7SyR8SVA1hY_zvfkNLo1g5jbRU27W3ICSS0,732
83
83
  ophyd_async/plan_stubs/_fly.py,sha256=WxghBAHsF-8xFrILCm44jeHIu9udLhm-tj4JXd9kZjY,6208
84
84
  ophyd_async/plan_stubs/_nd_attributes.py,sha256=TVfy3bhnrLFBXZ6b2bREBj0LzEviEGzuGvgWK3I7tII,2198
85
85
  ophyd_async/sim/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -93,7 +93,7 @@ ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py,sha256=gP0Q1-1p_3KO
93
93
  ophyd_async/sim/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
94
  ophyd_async/tango/__init__.py,sha256=2XxSpJWvvAlCs0GLPv6sEnUUD40fWq9OzKuiBEZ_MEs,861
95
95
  ophyd_async/tango/base_devices/__init__.py,sha256=fbn1-rfK8MCErpSmjAJuQioDikbjOobd4sDvAB9Xupw,157
96
- ophyd_async/tango/base_devices/_base_device.py,sha256=5Jt3UXIK1wNlKGnlGcKthigHBoLQ-MX7tzq4J3QY-_8,5968
96
+ ophyd_async/tango/base_devices/_base_device.py,sha256=3GIkU1bUyunA9uTBsEtANmlJBo4WCgQmAWYbCRcjoXM,6016
97
97
  ophyd_async/tango/base_devices/_tango_readable.py,sha256=J-XeR2fmQU5RTdsNhRvzNPJD8xZRVJ6-qXt09vfpVtI,951
98
98
  ophyd_async/tango/demo/__init__.py,sha256=_j-UicTnckuIBp8PnieFMOMnLFGivnaKdmo9o0hYtzc,256
99
99
  ophyd_async/tango/demo/_counter.py,sha256=neKkuepWfpBxMOPnnHJ79SHgwepymG4gTDVacuHE6fA,1134
@@ -104,9 +104,9 @@ ophyd_async/tango/demo/_tango/_servers.py,sha256=MwkkoZWJQm_cgafCBBXeQfwyAiOgU8c
104
104
  ophyd_async/tango/signal/__init__.py,sha256=-_wBvhSPb58h_XSeGVaJ6gMFOY8TQNsVYfZxQuxGB1c,750
105
105
  ophyd_async/tango/signal/_signal.py,sha256=72iOxCt6HkyaYPgE402h5fd1KryyVUarR0exV2A3UbU,6277
106
106
  ophyd_async/tango/signal/_tango_transport.py,sha256=DVTdLu8C19k-QzYaKUzFK2WMbaSd6dIO77k99ugD8U4,28990
107
- ophyd_async-0.8.0a3.dist-info/LICENSE,sha256=pU5shZcsvWgz701EbT7yjFZ8rMvZcWgRH54CRt8ld_c,1517
108
- ophyd_async-0.8.0a3.dist-info/METADATA,sha256=tAf70XFrRZAkgiqGePl9EqzyGRpYsKEh3Jz2-_3qj5w,6708
109
- ophyd_async-0.8.0a3.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
110
- ophyd_async-0.8.0a3.dist-info/entry_points.txt,sha256=O0YNJTEufO0w9BozXi-JurTy2U1_o0ypeCgJLQ727Jk,58
111
- ophyd_async-0.8.0a3.dist-info/top_level.txt,sha256=-hjorMsv5Rmjo3qrgqhjpal1N6kW5vMxZO3lD4iEaXs,12
112
- ophyd_async-0.8.0a3.dist-info/RECORD,,
107
+ ophyd_async-0.8.0a4.dist-info/LICENSE,sha256=pU5shZcsvWgz701EbT7yjFZ8rMvZcWgRH54CRt8ld_c,1517
108
+ ophyd_async-0.8.0a4.dist-info/METADATA,sha256=W1JjO8G4VcMybLwHqOkiySyWLZ_f_TCwTLA54Hf64Sg,6708
109
+ ophyd_async-0.8.0a4.dist-info/WHEEL,sha256=a7TGlA-5DaHMRrarXjVbQagU3Man_dCnGIWMJr5kRWo,91
110
+ ophyd_async-0.8.0a4.dist-info/entry_points.txt,sha256=O0YNJTEufO0w9BozXi-JurTy2U1_o0ypeCgJLQ727Jk,58
111
+ ophyd_async-0.8.0a4.dist-info/top_level.txt,sha256=-hjorMsv5Rmjo3qrgqhjpal1N6kW5vMxZO3lD4iEaXs,12
112
+ ophyd_async-0.8.0a4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.4.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5