ophyd-async 0.8.0a3__py3-none-any.whl → 0.8.0a5__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.0a5'
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,29 @@ 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
+ exceptions: dict[str, Exception] = {}
41
+ for name, child_device in device.children():
42
+ try:
43
+ await child_device.connect(mock=mock.child(name))
44
+ except Exception as e:
45
+ exceptions[name] = e
46
+ if exceptions:
47
+ raise NotConnected.with_other_exceptions_logged(exceptions)
48
+
49
+ async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
47
50
  """Used during ``Device.connect``.
48
51
 
49
52
  This is called when a previous connect has not been done, or has been
50
53
  done in a different mock more. It should connect the Device and all its
51
54
  children.
52
55
  """
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
- )
56
+ # Connect in parallel, gathering up NotConnected errors
57
+ coros = {
58
+ name: child_device.connect(timeout=timeout, force_reconnect=force_reconnect)
59
+ for name, child_device in device.children()
60
+ }
59
61
  await wait_for_connection(**coros)
60
62
 
61
63
 
@@ -67,9 +69,8 @@ class Device(HasName, Connectable):
67
69
  parent: Device | None = None
68
70
  # None if connect hasn't started, a Task if it has
69
71
  _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
72
+ # The mock if we have connected in mock mode
73
+ _mock: LazyMock | None = None
73
74
 
74
75
  def __init__(
75
76
  self, name: str = "", connector: DeviceConnector | None = None
@@ -83,10 +84,18 @@ class Device(HasName, Connectable):
83
84
  """Return the name of the Device"""
84
85
  return self._name
85
86
 
87
+ @cached_property
88
+ def _child_devices(self) -> dict[str, Device]:
89
+ return {}
90
+
86
91
  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
92
+ yield from self._child_devices.items()
93
+
94
+ @cached_property
95
+ def log(self) -> LoggerAdapter:
96
+ return LoggerAdapter(
97
+ getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
98
+ )
90
99
 
91
100
  def set_name(self, name: str):
92
101
  """Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
@@ -97,28 +106,33 @@ class Device(HasName, Connectable):
97
106
  New name to set
98
107
  """
99
108
  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
- )
109
+ # Ensure logger is recreated after a name change
110
+ if "log" in self.__dict__:
111
+ del self.log
104
112
  for child_name, child in self.children():
105
113
  child_name = f"{self.name}-{child_name.strip('_')}" if self.name else ""
106
114
  child.set_name(child_name)
107
115
 
108
116
  def __setattr__(self, name: str, value: Any) -> None:
117
+ # Bear in mind that this function is called *a lot*, so
118
+ # we need to make sure nothing expensive happens in it...
109
119
  if name == "parent":
110
120
  if self.parent not in (value, None):
111
121
  raise TypeError(
112
122
  f"Cannot set the parent of {self} to be {value}: "
113
123
  f"it is already a child of {self.parent}"
114
124
  )
115
- elif isinstance(value, Device):
125
+ # ...hence not doing an isinstance check for attributes we
126
+ # know not to be Devices
127
+ elif name not in _not_device_attrs and isinstance(value, Device):
116
128
  value.parent = self
117
- return super().__setattr__(name, value)
129
+ self._child_devices[name] = value
130
+ # ...and avoiding the super call as we know it resolves to `object`
131
+ return object.__setattr__(self, name, value)
118
132
 
119
133
  async def connect(
120
134
  self,
121
- mock: bool | Mock = False,
135
+ mock: bool | LazyMock = False,
122
136
  timeout: float = DEFAULT_TIMEOUT,
123
137
  force_reconnect: bool = False,
124
138
  ) -> None:
@@ -133,26 +147,39 @@ class Device(HasName, Connectable):
133
147
  timeout:
134
148
  Time to wait before failing with a TimeoutError.
135
149
  """
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
150
+ if mock:
151
+ # Always connect in mock mode serially
152
+ if isinstance(mock, LazyMock):
153
+ # Use the provided mock
154
+ self._mock = mock
155
+ elif not self._mock:
156
+ # Make one
157
+ self._mock = LazyMock()
158
+ await self._connector.connect_mock(self, self._mock)
159
+ else:
160
+ # Try to cache the connect in real mode
161
+ can_use_previous_connect = (
162
+ self._mock is None
163
+ and self._connect_task
164
+ and not (self._connect_task.done() and self._connect_task.exception())
150
165
  )
151
- self._connect_task = asyncio.create_task(coro)
166
+ if force_reconnect or not can_use_previous_connect:
167
+ self._mock = None
168
+ coro = self._connector.connect_real(self, timeout, force_reconnect)
169
+ self._connect_task = asyncio.create_task(coro)
170
+ assert self._connect_task, "Connect task not created, this shouldn't happen"
171
+ # Wait for it to complete
172
+ await self._connect_task
173
+
152
174
 
153
- assert self._connect_task, "Connect task not created, this shouldn't happen"
154
- # Wait for it to complete
155
- await self._connect_task
175
+ _not_device_attrs = {
176
+ "_name",
177
+ "_children",
178
+ "_connector",
179
+ "_timeout",
180
+ "_mock",
181
+ "_connect_task",
182
+ }
156
183
 
157
184
 
158
185
  DeviceT = TypeVar("DeviceT", bound=Device)
@@ -172,7 +199,8 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
172
199
  children: Mapping[int, DeviceT],
173
200
  name: str = "",
174
201
  ) -> None:
175
- self._children = dict(children)
202
+ self._children: dict[int, DeviceT] = {}
203
+ self.update(children)
176
204
  super().__init__(name=name)
177
205
 
178
206
  def __setattr__(self, name: str, child: Any) -> None:
@@ -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):
@@ -451,12 +449,6 @@ async def observe_value(
451
449
  """
452
450
 
453
451
  q: asyncio.Queue[SignalDatatypeT | Status] = asyncio.Queue()
454
- if timeout is None:
455
- get_value = q.get
456
- else:
457
-
458
- async def get_value():
459
- return await asyncio.wait_for(q.get(), timeout)
460
452
 
461
453
  if done_status is not None:
462
454
  done_status.add_callback(q.put_nowait)
@@ -464,7 +456,10 @@ async def observe_value(
464
456
  signal.subscribe_value(q.put_nowait)
465
457
  try:
466
458
  while True:
467
- item = await get_value()
459
+ # yield here in case something else is filling the queue
460
+ # like in test_observe_value_times_out_with_no_external_task()
461
+ await asyncio.sleep(0)
462
+ item = await asyncio.wait_for(q.get(), timeout)
468
463
  if done_status and item is done_status:
469
464
  if exc := done_status.exception():
470
465
  raise exc
@@ -4,7 +4,8 @@ import time
4
4
  from abc import abstractmethod
5
5
  from collections.abc import Sequence
6
6
  from dataclasses import dataclass
7
- from typing import Any, Generic, get_origin
7
+ from functools import lru_cache
8
+ from typing import Any, Generic, get_args, get_origin
8
9
 
9
10
  import numpy as np
10
11
  from bluesky.protocols import Reading
@@ -57,7 +58,7 @@ class SequenceEnumSoftConverter(SoftConverter[Sequence[EnumT]]):
57
58
 
58
59
  @dataclass
59
60
  class NDArraySoftConverter(SoftConverter[Array1D]):
60
- datatype: np.dtype
61
+ datatype: np.dtype | None = None
61
62
 
62
63
  def write_value(self, value: Any) -> Array1D:
63
64
  return np.array(() if value is None else value, dtype=self.datatype)
@@ -90,13 +91,18 @@ 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]:
96
98
  return SequenceStrSoftConverter()
97
99
  elif get_origin(datatype) == Sequence and enum_cls:
98
100
  return SequenceEnumSoftConverter(enum_cls)
101
+ elif datatype is np.ndarray:
102
+ return NDArraySoftConverter()
99
103
  elif get_origin(datatype) == np.ndarray:
104
+ if datatype not in get_args(SignalDatatype):
105
+ raise TypeError(f"Expected Array1D[dtype], got {datatype}")
100
106
  return NDArraySoftConverter(get_dtype(datatype))
101
107
  elif enum_cls:
102
108
  return EnumSoftConverter(enum_cls)
@@ -2,18 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- from collections.abc import Awaitable, Callable, Iterable, Sequence
5
+ from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
6
6
  from dataclasses import dataclass
7
7
  from enum import Enum, EnumMeta
8
- from typing import (
9
- Any,
10
- Generic,
11
- Literal,
12
- ParamSpec,
13
- TypeVar,
14
- get_args,
15
- get_origin,
16
- )
8
+ from typing import Any, Generic, Literal, ParamSpec, TypeVar, get_args, get_origin
9
+ from unittest.mock import Mock
17
10
 
18
11
  import numpy as np
19
12
 
@@ -21,7 +14,7 @@ T = TypeVar("T")
21
14
  P = ParamSpec("P")
22
15
  Callback = Callable[[T], None]
23
16
  DEFAULT_TIMEOUT = 10.0
24
- ErrorText = str | dict[str, Exception]
17
+ ErrorText = str | Mapping[str, Exception]
25
18
 
26
19
 
27
20
  class StrictEnum(str, Enum):
@@ -69,6 +62,13 @@ class NotConnected(Exception):
69
62
 
70
63
  self._errors = errors
71
64
 
65
+ @property
66
+ def sub_errors(self) -> Mapping[str, Exception]:
67
+ if isinstance(self._errors, dict):
68
+ return self._errors.copy()
69
+ else:
70
+ return {}
71
+
72
72
  def _format_sub_errors(self, name: str, error: Exception, indent="") -> str:
73
73
  if isinstance(error, NotConnected):
74
74
  error_txt = ":" + error.format_error_string(indent + self._indent_width)
@@ -99,6 +99,19 @@ class NotConnected(Exception):
99
99
  def __str__(self) -> str:
100
100
  return self.format_error_string(indent="")
101
101
 
102
+ @classmethod
103
+ def with_other_exceptions_logged(
104
+ cls, exceptions: Mapping[str, Exception]
105
+ ) -> NotConnected:
106
+ for name, exception in exceptions.items():
107
+ if not isinstance(exception, NotConnected):
108
+ logging.exception(
109
+ f"device `{name}` raised unexpected exception "
110
+ f"{type(exception).__name__}",
111
+ exc_info=exception,
112
+ )
113
+ return NotConnected(exceptions)
114
+
102
115
 
103
116
  @dataclass(frozen=True)
104
117
  class WatcherUpdate(Generic[T]):
@@ -120,21 +133,23 @@ async def wait_for_connection(**coros: Awaitable[None]):
120
133
 
121
134
  Expected kwargs should be a mapping of names to coroutine tasks to execute.
122
135
  """
123
- results = await asyncio.gather(*coros.values(), return_exceptions=True)
124
- exceptions = {}
125
-
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):
130
- logging.exception(
131
- f"device `{name}` raised unexpected exception "
132
- f"{type(result).__name__}",
133
- exc_info=result,
134
- )
136
+ exceptions: dict[str, Exception] = {}
137
+ if len(coros) == 1:
138
+ # Single device optimization
139
+ name, coro = coros.popitem()
140
+ try:
141
+ await coro
142
+ except Exception as e:
143
+ exceptions[name] = e
144
+ else:
145
+ # Use gather to connect in parallel
146
+ results = await asyncio.gather(*coros.values(), return_exceptions=True)
147
+ for name, result in zip(coros, results, strict=False):
148
+ if isinstance(result, Exception):
149
+ exceptions[name] = result
135
150
 
136
151
  if exceptions:
137
- raise NotConnected(exceptions)
152
+ raise NotConnected.with_other_exceptions_logged(exceptions)
138
153
 
139
154
 
140
155
  def get_dtype(datatype: type) -> np.dtype:
@@ -252,3 +267,38 @@ class Reference(Generic[T]):
252
267
 
253
268
  def __call__(self) -> T:
254
269
  return self._obj
270
+
271
+
272
+ class LazyMock:
273
+ """A lazily created Mock to be used when connecting in mock mode.
274
+
275
+ Creating Mocks is reasonably expensive when each Device (and Signal)
276
+ requires its own, and the tree is only used when ``Signal.set()`` is
277
+ called. This class allows a tree of lazily connected Mocks to be
278
+ constructed so that when the leaf is created, so are its parents.
279
+ Any calls to the child are then accessible from the parent mock.
280
+
281
+ >>> parent = LazyMock()
282
+ >>> child = parent.child("child")
283
+ >>> child_mock = child()
284
+ >>> child_mock() # doctest: +ELLIPSIS
285
+ <Mock name='mock.child()' id='...'>
286
+ >>> parent_mock = parent()
287
+ >>> parent_mock.mock_calls
288
+ [call.child()]
289
+ """
290
+
291
+ def __init__(self, name: str = "", parent: LazyMock | None = None) -> None:
292
+ self.parent = parent
293
+ self.name = name
294
+ self._mock: Mock | None = None
295
+
296
+ def child(self, name: str) -> LazyMock:
297
+ return LazyMock(name, self)
298
+
299
+ def __call__(self) -> Mock:
300
+ if self._mock is None:
301
+ self._mock = Mock(spec=object)
302
+ if self.parent is not None:
303
+ self.parent().attach_mock(self._mock, self.name)
304
+ 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],
@@ -115,7 +115,7 @@ class CaLongStrConverter(CaConverter[str]):
115
115
  def __init__(self):
116
116
  super().__init__(str, dbr.DBR_CHAR_STR, dbr.DBR_CHAR_STR)
117
117
 
118
- def write_value_and_dbr(self, value: Any) -> Any:
118
+ def write_value(self, value: Any) -> Any:
119
119
  # Add a null in here as this is what the commandline caput does
120
120
  # TODO: this should be in the server so check if it can be pushed to asyn
121
121
  return value + "\0"
@@ -188,6 +188,7 @@ _datatype_converter_from_typeid: dict[
188
188
  ("epics:nt/NTScalarArray:1.0", "as"): (Sequence[str], PvaConverter),
189
189
  ("epics:nt/NTTable:1.0", "S"): (Table, PvaTableConverter),
190
190
  ("epics:nt/NTNDArray:1.0", "v"): (np.ndarray, PvaNDArrayConverter),
191
+ ("epics:nt/NTNDArray:1.0", "U"): (np.ndarray, PvaNDArrayConverter),
191
192
  }
192
193
 
193
194
 
@@ -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)
@@ -13,7 +13,7 @@ class PandaHdf5DatasetType(StrictEnum):
13
13
 
14
14
  class DatasetTable(Table):
15
15
  name: Sequence[str]
16
- hdf5_type: Sequence[PandaHdf5DatasetType]
16
+ dtype: Sequence[PandaHdf5DatasetType]
17
17
 
18
18
 
19
19
  class SeqTrigger(StrictEnum):
@@ -1,28 +1,33 @@
1
- from unittest.mock import Mock
1
+ from collections.abc import Awaitable
2
2
 
3
3
  import bluesky.plan_stubs as bps
4
4
 
5
- from ophyd_async.core import DEFAULT_TIMEOUT, Device, wait_for_connection
5
+ from ophyd_async.core import DEFAULT_TIMEOUT, Device, LazyMock, wait_for_connection
6
6
 
7
7
 
8
8
  def ensure_connected(
9
9
  *devices: Device,
10
- mock: bool | Mock = False,
10
+ mock: bool | LazyMock = False,
11
11
  timeout: float = DEFAULT_TIMEOUT,
12
12
  force_reconnect=False,
13
13
  ):
14
- (connect_task,) = yield from bps.wait_for(
15
- [
16
- lambda: wait_for_connection(
17
- **{
18
- device.name: device.connect(
19
- mock=mock, timeout=timeout, force_reconnect=force_reconnect
20
- )
21
- for device in devices
22
- }
14
+ device_names = [device.name for device in devices]
15
+ non_unique = {
16
+ device: device.name for device in devices if device_names.count(device.name) > 1
17
+ }
18
+ if non_unique:
19
+ raise ValueError(f"Devices do not have unique names {non_unique}")
20
+
21
+ def connect_devices() -> Awaitable[None]:
22
+ coros = {
23
+ device.name: device.connect(
24
+ mock=mock, timeout=timeout, force_reconnect=force_reconnect
23
25
  )
24
- ]
25
- )
26
+ for device in devices
27
+ }
28
+ return wait_for_connection(**coros)
29
+
30
+ (connect_task,) = yield from bps.wait_for([connect_devices])
26
31
 
27
32
  if connect_task and connect_task.exception() is not None:
28
33
  raise connect_task.exception()
@@ -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.0a5
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=SQdFsQ53whTNOERBL9cmDWrar7r4kVAXQthVKIJB7mU,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=ygXxDKiTO43qDnLeLzrKKyABwlrfGvSVii7PHyCIjHg,12074
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=IpOyYu1IqpX8sFs9_CLUd1LuFWgAqEaFZgA12BG5tfM,19897
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=w9zzD4eoD9SsJpORXNSaFOLJrD6biYBbCSVAybLa_7k,5926
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=wzzGL7yPAMuPueGOG1cpTgh0vho5YxI86m8SSX7Z9hw,9494
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
@@ -50,11 +50,11 @@ ophyd_async/epics/advimba/_vimba.py,sha256=E_RJ0uJQt-RWAY7uFTNkHaOdGYS5sa7ZbRgAe
50
50
  ophyd_async/epics/advimba/_vimba_controller.py,sha256=Ej7irxTab9cfjmqz4G4Zxv3CjhJw_eRmIb3E62YWh6g,2226
51
51
  ophyd_async/epics/advimba/_vimba_io.py,sha256=F3KUzMN-GMe637za-iRVGGTt8v9F1a79fRcl3MCkfXA,1864
52
52
  ophyd_async/epics/core/__init__.py,sha256=8NoQxEEc2Ny_L9nrD2fnGSf_2gJr1wCR1LwUeLNcIJo,588
53
- ophyd_async/epics/core/_aioca.py,sha256=318lw_dGWnckgyQ1f4K8QeX_KVOD4idzWX8sx2jbnyk,11625
53
+ ophyd_async/epics/core/_aioca.py,sha256=iQWiHWYbMJLa7qeBrCz4_e16Y8A-NYYi6oYNi8oOFVY,11617
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
- 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
56
+ ophyd_async/epics/core/_p4p.py,sha256=S6zXXApRF0454aOcxUI_cd7Y7tXiOnss_ODhjjk0PMo,14691
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
@@ -74,12 +74,12 @@ ophyd_async/fastcs/panda/__init__.py,sha256=_o7n7ckoTM6hTRHpLphpL7r_9sADE59MRNM0
74
74
  ophyd_async/fastcs/panda/_block.py,sha256=STQo6NJAqIVfxyMf-2pxINPyr9_nKtXSdicp92a25xo,1709
75
75
  ophyd_async/fastcs/panda/_control.py,sha256=61vcJMjYQiUGAM5J0SfkfthFs7U28m9Pe9mgmGGf0-w,1021
76
76
  ophyd_async/fastcs/panda/_hdf_panda.py,sha256=WdgWgdrU2yT4keH70VG-ZBVOmT-IpKVyukEuKk7QnJs,1049
77
- ophyd_async/fastcs/panda/_table.py,sha256=8XkfpdZETflzoZE4qB5Y5ySi8JFxxZulDszVccgY_w4,2293
77
+ ophyd_async/fastcs/panda/_table.py,sha256=5YyAfsl3H7kxH3bDjUKHuH9DyrWQmAn9dv-v0NYzFNo,2289
78
78
  ophyd_async/fastcs/panda/_trigger.py,sha256=forImtdnDnaZ0KKhqSxCqwHWXq13SJ4mn9wdM4yqNLY,3056
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=uoqfAzghjifdfD_JM860TvMvj9T2Y12nKPvtI5l6zZc,1021
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.0a5.dist-info/LICENSE,sha256=pU5shZcsvWgz701EbT7yjFZ8rMvZcWgRH54CRt8ld_c,1517
108
+ ophyd_async-0.8.0a5.dist-info/METADATA,sha256=0uu4GmEt1a33pHw4Oa5BhF22ns92xsqC_0wNmg3nkM0,6708
109
+ ophyd_async-0.8.0a5.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
110
+ ophyd_async-0.8.0a5.dist-info/entry_points.txt,sha256=O0YNJTEufO0w9BozXi-JurTy2U1_o0ypeCgJLQ727Jk,58
111
+ ophyd_async-0.8.0a5.dist-info/top_level.txt,sha256=-hjorMsv5Rmjo3qrgqhjpal1N6kW5vMxZO3lD4iEaXs,12
112
+ ophyd_async-0.8.0a5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5