ophyd-async 0.7.0a1__py3-none-any.whl → 0.8.0a3__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.
Files changed (83) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +30 -9
  3. ophyd_async/core/_detector.py +5 -10
  4. ophyd_async/core/_device.py +146 -67
  5. ophyd_async/core/_device_filler.py +269 -0
  6. ophyd_async/core/_device_save_loader.py +6 -7
  7. ophyd_async/core/_mock_signal_backend.py +32 -40
  8. ophyd_async/core/_mock_signal_utils.py +22 -16
  9. ophyd_async/core/_protocol.py +28 -8
  10. ophyd_async/core/_readable.py +133 -134
  11. ophyd_async/core/_signal.py +140 -152
  12. ophyd_async/core/_signal_backend.py +131 -64
  13. ophyd_async/core/_soft_signal_backend.py +125 -194
  14. ophyd_async/core/_status.py +22 -6
  15. ophyd_async/core/_table.py +97 -100
  16. ophyd_async/core/_utils.py +79 -18
  17. ophyd_async/epics/adaravis/_aravis_controller.py +2 -2
  18. ophyd_async/epics/adaravis/_aravis_io.py +8 -6
  19. ophyd_async/epics/adcore/_core_io.py +5 -7
  20. ophyd_async/epics/adcore/_hdf_writer.py +2 -2
  21. ophyd_async/epics/adcore/_single_trigger.py +4 -9
  22. ophyd_async/epics/adcore/_utils.py +15 -10
  23. ophyd_async/epics/adkinetix/__init__.py +2 -1
  24. ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
  25. ophyd_async/epics/adkinetix/_kinetix_io.py +4 -5
  26. ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
  27. ophyd_async/epics/adpilatus/_pilatus_io.py +3 -4
  28. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  29. ophyd_async/epics/advimba/__init__.py +4 -1
  30. ophyd_async/epics/advimba/_vimba_controller.py +6 -3
  31. ophyd_async/epics/advimba/_vimba_io.py +8 -9
  32. ophyd_async/epics/core/__init__.py +26 -0
  33. ophyd_async/epics/core/_aioca.py +323 -0
  34. ophyd_async/epics/core/_epics_connector.py +53 -0
  35. ophyd_async/epics/core/_epics_device.py +13 -0
  36. ophyd_async/epics/core/_p4p.py +382 -0
  37. ophyd_async/epics/core/_pvi_connector.py +92 -0
  38. ophyd_async/epics/core/_signal.py +171 -0
  39. ophyd_async/epics/core/_util.py +61 -0
  40. ophyd_async/epics/demo/_mover.py +4 -5
  41. ophyd_async/epics/demo/_sensor.py +14 -13
  42. ophyd_async/epics/eiger/_eiger.py +1 -2
  43. ophyd_async/epics/eiger/_eiger_controller.py +1 -1
  44. ophyd_async/epics/eiger/_eiger_io.py +3 -5
  45. ophyd_async/epics/eiger/_odin_io.py +5 -5
  46. ophyd_async/epics/motor.py +4 -5
  47. ophyd_async/epics/signal.py +11 -0
  48. ophyd_async/fastcs/core.py +9 -0
  49. ophyd_async/fastcs/panda/__init__.py +4 -4
  50. ophyd_async/fastcs/panda/_block.py +23 -11
  51. ophyd_async/fastcs/panda/_control.py +3 -5
  52. ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
  53. ophyd_async/fastcs/panda/_table.py +29 -51
  54. ophyd_async/fastcs/panda/_trigger.py +8 -8
  55. ophyd_async/fastcs/panda/_writer.py +4 -7
  56. ophyd_async/plan_stubs/_ensure_connected.py +3 -1
  57. ophyd_async/plan_stubs/_fly.py +2 -2
  58. ophyd_async/plan_stubs/_nd_attributes.py +5 -4
  59. ophyd_async/py.typed +0 -0
  60. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
  61. ophyd_async/sim/demo/_sim_motor.py +3 -4
  62. ophyd_async/tango/__init__.py +2 -4
  63. ophyd_async/tango/base_devices/_base_device.py +76 -144
  64. ophyd_async/tango/demo/_counter.py +8 -18
  65. ophyd_async/tango/demo/_mover.py +5 -6
  66. ophyd_async/tango/signal/__init__.py +2 -4
  67. ophyd_async/tango/signal/_signal.py +29 -50
  68. ophyd_async/tango/signal/_tango_transport.py +38 -40
  69. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/METADATA +8 -12
  70. ophyd_async-0.8.0a3.dist-info/RECORD +112 -0
  71. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/WHEEL +1 -1
  72. ophyd_async/epics/pvi/__init__.py +0 -3
  73. ophyd_async/epics/pvi/_pvi.py +0 -338
  74. ophyd_async/epics/signal/__init__.py +0 -21
  75. ophyd_async/epics/signal/_aioca.py +0 -378
  76. ophyd_async/epics/signal/_common.py +0 -57
  77. ophyd_async/epics/signal/_epics_transport.py +0 -34
  78. ophyd_async/epics/signal/_p4p.py +0 -518
  79. ophyd_async/epics/signal/_signal.py +0 -114
  80. ophyd_async-0.7.0a1.dist-info/RECORD +0 -108
  81. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/LICENSE +0 -0
  82. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/entry_points.txt +0 -0
  83. {ophyd_async-0.7.0a1.dist-info → ophyd_async-0.8.0a3.dist-info}/top_level.txt +0 -0
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.7.0a1'
16
- __version_tuple__ = version_tuple = (0, 7, 0)
15
+ __version__ = version = '0.8.0a3'
16
+ __version_tuple__ = version_tuple = (0, 8, 0)
@@ -5,7 +5,8 @@ from ._detector import (
5
5
  StandardDetector,
6
6
  TriggerInfo,
7
7
  )
8
- from ._device import Device, DeviceCollector, DeviceVector
8
+ from ._device import Device, DeviceCollector, DeviceConnector, DeviceVector
9
+ from ._device_filler import DeviceFiller
9
10
  from ._device_save_loader import (
10
11
  all_at_once,
11
12
  get_signal_values,
@@ -22,6 +23,7 @@ from ._log import config_ophyd_async_logging
22
23
  from ._mock_signal_backend import MockSignalBackend
23
24
  from ._mock_signal_utils import (
24
25
  callback_on_mock_put,
26
+ get_mock,
25
27
  get_mock_put,
26
28
  mock_puts_blocked,
27
29
  reset_mock_put_calls,
@@ -43,7 +45,12 @@ from ._providers import (
43
45
  UUIDFilenameProvider,
44
46
  YMDPathProvider,
45
47
  )
46
- from ._readable import ConfigSignal, HintedSignal, StandardReadable
48
+ from ._readable import (
49
+ ConfigSignal,
50
+ HintedSignal,
51
+ StandardReadable,
52
+ StandardReadableFormat,
53
+ )
47
54
  from ._signal import (
48
55
  Signal,
49
56
  SignalR,
@@ -62,9 +69,11 @@ from ._signal import (
62
69
  wait_for_value,
63
70
  )
64
71
  from ._signal_backend import (
65
- RuntimeSubsetEnum,
72
+ Array1D,
66
73
  SignalBackend,
67
- SubsetEnum,
74
+ SignalDatatype,
75
+ SignalDatatypeT,
76
+ make_datakey,
68
77
  )
69
78
  from ._soft_signal_backend import SignalMetadata, SoftSignalBackend
70
79
  from ._status import AsyncStatus, WatchableAsyncStatus, completed_status
@@ -73,14 +82,17 @@ from ._utils import (
73
82
  CALCULATE_TIMEOUT,
74
83
  DEFAULT_TIMEOUT,
75
84
  CalculatableTimeout,
85
+ Callback,
76
86
  NotConnected,
77
- ReadingValueCallback,
87
+ Reference,
88
+ StrictEnum,
89
+ SubsetEnum,
78
90
  T,
79
91
  WatcherUpdate,
80
92
  get_dtype,
93
+ get_enum_cls,
81
94
  get_unique,
82
95
  in_micros,
83
- is_pydantic_model,
84
96
  wait_for_connection,
85
97
  )
86
98
 
@@ -91,8 +103,10 @@ __all__ = [
91
103
  "StandardDetector",
92
104
  "TriggerInfo",
93
105
  "Device",
106
+ "DeviceConnector",
94
107
  "DeviceCollector",
95
108
  "DeviceVector",
109
+ "DeviceFiller",
96
110
  "all_at_once",
97
111
  "get_signal_values",
98
112
  "load_device",
@@ -108,6 +122,7 @@ __all__ = [
108
122
  "config_ophyd_async_logging",
109
123
  "MockSignalBackend",
110
124
  "callback_on_mock_put",
125
+ "get_mock",
111
126
  "get_mock_put",
112
127
  "mock_puts_blocked",
113
128
  "reset_mock_put_calls",
@@ -131,6 +146,7 @@ __all__ = [
131
146
  "ConfigSignal",
132
147
  "HintedSignal",
133
148
  "StandardReadable",
149
+ "StandardReadableFormat",
134
150
  "Signal",
135
151
  "SignalR",
136
152
  "SignalRW",
@@ -146,25 +162,30 @@ __all__ = [
146
162
  "soft_signal_r_and_setter",
147
163
  "soft_signal_rw",
148
164
  "wait_for_value",
149
- "RuntimeSubsetEnum",
165
+ "Array1D",
150
166
  "SignalBackend",
167
+ "make_datakey",
168
+ "StrictEnum",
151
169
  "SubsetEnum",
170
+ "SignalDatatype",
171
+ "SignalDatatypeT",
152
172
  "SignalMetadata",
153
173
  "SoftSignalBackend",
154
174
  "AsyncStatus",
155
175
  "WatchableAsyncStatus",
156
176
  "DEFAULT_TIMEOUT",
157
177
  "CalculatableTimeout",
178
+ "Callback",
158
179
  "CALCULATE_TIMEOUT",
159
180
  "NotConnected",
160
- "ReadingValueCallback",
181
+ "Reference",
161
182
  "Table",
162
183
  "T",
163
184
  "WatcherUpdate",
164
185
  "get_dtype",
186
+ "get_enum_cls",
165
187
  "get_unique",
166
188
  "in_micros",
167
- "is_pydantic_model",
168
189
  "wait_for_connection",
169
190
  "completed_status",
170
191
  ]
@@ -4,11 +4,7 @@ import asyncio
4
4
  import time
5
5
  from abc import ABC, abstractmethod
6
6
  from collections.abc import AsyncGenerator, AsyncIterator, Callable, Iterator, Sequence
7
- from enum import Enum
8
7
  from functools import cached_property
9
- from typing import (
10
- Generic,
11
- )
12
8
 
13
9
  from bluesky.protocols import (
14
10
  Collectable,
@@ -23,14 +19,14 @@ from bluesky.protocols import (
23
19
  from event_model import DataKey
24
20
  from pydantic import BaseModel, Field, NonNegativeInt, computed_field
25
21
 
26
- from ._device import Device
22
+ from ._device import Device, DeviceConnector
27
23
  from ._protocol import AsyncConfigurable, AsyncReadable
28
24
  from ._signal import SignalR
29
25
  from ._status import AsyncStatus, WatchableAsyncStatus
30
- from ._utils import DEFAULT_TIMEOUT, T, WatcherUpdate, merge_gathered_dicts
26
+ from ._utils import DEFAULT_TIMEOUT, StrictEnum, WatcherUpdate, merge_gathered_dicts
31
27
 
32
28
 
33
- class DetectorTrigger(str, Enum):
29
+ class DetectorTrigger(StrictEnum):
34
30
  """Type of mechanism for triggering a detector to take frames"""
35
31
 
36
32
  #: Detector generates internal trigger for given rate
@@ -172,7 +168,6 @@ class StandardDetector(
172
168
  Flyable,
173
169
  Collectable,
174
170
  WritesStreamAssets,
175
- Generic[T],
176
171
  ):
177
172
  """
178
173
  Useful detector base class for step and fly scanning detectors.
@@ -185,6 +180,7 @@ class StandardDetector(
185
180
  writer: DetectorWriter,
186
181
  config_sigs: Sequence[SignalR] = (),
187
182
  name: str = "",
183
+ connector: DeviceConnector | None = None,
188
184
  ) -> None:
189
185
  """
190
186
  Constructor
@@ -213,8 +209,7 @@ class StandardDetector(
213
209
  self._completable_frames: int = 0
214
210
  self._number_of_triggers_iter: Iterator[int] | None = None
215
211
  self._initial_frame: int = 0
216
-
217
- super().__init__(name)
212
+ super().__init__(name, connector=connector)
218
213
 
219
214
  @property
220
215
  def controller(self) -> DetectorController:
@@ -1,39 +1,81 @@
1
- """Base device"""
1
+ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import sys
5
- from collections.abc import Coroutine, Generator, Iterator
6
- from functools import cached_property
5
+ from collections.abc import Coroutine, Iterator, Mapping, MutableMapping
7
6
  from logging import LoggerAdapter, getLogger
8
- from typing import (
9
- Any,
10
- Optional,
11
- TypeVar,
12
- )
7
+ from typing import Any, TypeVar
8
+ from unittest.mock import Mock
13
9
 
14
10
  from bluesky.protocols import HasName
15
- from bluesky.run_engine import call_in_bluesky_event_loop
11
+ from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
16
12
 
13
+ from ._protocol import Connectable
17
14
  from ._utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
18
15
 
16
+ _device_mocks: dict[Device, Mock] = {}
19
17
 
20
- class Device(HasName):
21
- """Common base class for all Ophyd Async Devices.
22
18
 
23
- By default, names and connects all Device children.
24
- """
19
+ class DeviceConnector:
20
+ """Defines how a `Device` should be connected and type hints processed."""
21
+
22
+ def create_children_from_annotations(self, device: Device):
23
+ """Used when children can be created from introspecting the hardware.
24
+
25
+ Some control systems allow introspection of a device to determine what
26
+ children it has. To allow this to work nicely with typing we add these
27
+ hints to the Device like so::
28
+
29
+ my_signal: SignalRW[int]
30
+ my_device: MyDevice
31
+
32
+ This method will be run during ``Device.__init__``, and is responsible
33
+ for turning all of those type hints into real Signal and Device instances.
34
+
35
+ Subsequent runs of this function should do nothing, to allow it to be
36
+ called early in Devices that need to pass references to their children
37
+ during ``__init__``.
38
+ """
39
+
40
+ async def connect(
41
+ self,
42
+ device: Device,
43
+ mock: bool | Mock,
44
+ timeout: float,
45
+ force_reconnect: bool,
46
+ ):
47
+ """Used during ``Device.connect``.
48
+
49
+ This is called when a previous connect has not been done, or has been
50
+ done in a different mock more. It should connect the Device and all its
51
+ children.
52
+ """
53
+ coros = {}
54
+ for name, child_device in device.children():
55
+ child_mock = getattr(mock, name) if mock else mock # Mock() or False
56
+ coros[name] = child_device.connect(
57
+ mock=child_mock, timeout=timeout, force_reconnect=force_reconnect
58
+ )
59
+ await wait_for_connection(**coros)
60
+
61
+
62
+ class Device(HasName, Connectable):
63
+ """Common base class for all Ophyd Async Devices."""
25
64
 
26
65
  _name: str = ""
27
66
  #: The parent Device if it exists
28
- parent: Optional["Device"] = None
67
+ parent: Device | None = None
29
68
  # None if connect hasn't started, a Task if it has
30
69
  _connect_task: asyncio.Task | None = None
70
+ # If not None, then this is the mock arg of the previous connect
71
+ # to let us know if we can reuse an existing connection
72
+ _connect_mock_arg: bool | None = None
31
73
 
32
- # Used to check if the previous connect was mocked,
33
- # if the next mock value differs then we fail
34
- _previous_connect_was_mock = None
35
-
36
- def __init__(self, name: str = "") -> None:
74
+ def __init__(
75
+ self, name: str = "", connector: DeviceConnector | None = None
76
+ ) -> None:
77
+ self._connector = connector or DeviceConnector()
78
+ self._connector.create_children_from_annotations(self)
37
79
  self.set_name(name)
38
80
 
39
81
  @property
@@ -41,13 +83,7 @@ class Device(HasName):
41
83
  """Return the name of the Device"""
42
84
  return self._name
43
85
 
44
- @cached_property
45
- def log(self):
46
- return LoggerAdapter(
47
- getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
48
- )
49
-
50
- def children(self) -> Iterator[tuple[str, "Device"]]:
86
+ def children(self) -> Iterator[tuple[str, Device]]:
51
87
  for attr_name, attr in self.__dict__.items():
52
88
  if attr_name != "parent" and isinstance(attr, Device):
53
89
  yield attr_name, attr
@@ -60,23 +96,32 @@ class Device(HasName):
60
96
  name:
61
97
  New name to set
62
98
  """
63
-
64
- # Ensure self.log is recreated after a name change
65
- if hasattr(self, "log"):
66
- del self.log
67
-
68
99
  self._name = name
69
- for attr_name, child in self.children():
70
- child_name = f"{name}-{attr_name.rstrip('_')}" if name else ""
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
+ )
104
+ for child_name, child in self.children():
105
+ child_name = f"{self.name}-{child_name.strip('_')}" if self.name else ""
71
106
  child.set_name(child_name)
72
- child.parent = self
107
+
108
+ def __setattr__(self, name: str, value: Any) -> None:
109
+ if name == "parent":
110
+ if self.parent not in (value, None):
111
+ raise TypeError(
112
+ f"Cannot set the parent of {self} to be {value}: "
113
+ f"it is already a child of {self.parent}"
114
+ )
115
+ elif isinstance(value, Device):
116
+ value.parent = self
117
+ return super().__setattr__(name, value)
73
118
 
74
119
  async def connect(
75
120
  self,
76
- mock: bool = False,
121
+ mock: bool | Mock = False,
77
122
  timeout: float = DEFAULT_TIMEOUT,
78
123
  force_reconnect: bool = False,
79
- ):
124
+ ) -> None:
80
125
  """Connect self and all child Devices.
81
126
 
82
127
  Contains a timeout that gets propagated to child.connect methods.
@@ -88,41 +133,32 @@ class Device(HasName):
88
133
  timeout:
89
134
  Time to wait before failing with a TimeoutError.
90
135
  """
91
-
92
- if (
93
- self._previous_connect_was_mock is not None
94
- and self._previous_connect_was_mock != mock
95
- ):
96
- raise RuntimeError(
97
- f"`connect(mock={mock})` called on a `Device` where the previous "
98
- f"connect was `mock={self._previous_connect_was_mock}`. Changing mock "
99
- "value between connects is not permitted."
100
- )
101
- self._previous_connect_was_mock = mock
102
-
103
- # If previous connect with same args has started and not errored, can use it
104
- can_use_previous_connect = self._connect_task and not (
105
- self._connect_task.done() and self._connect_task.exception()
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())
106
141
  )
142
+ if mock is True:
143
+ mock = Mock() # create a new Mock if one not provided
107
144
  if force_reconnect or not can_use_previous_connect:
108
- # Kick off a connection
109
- coros = {
110
- name: child_device.connect(
111
- mock, timeout=timeout, force_reconnect=force_reconnect
112
- )
113
- for name, child_device in self.children()
114
- }
115
- self._connect_task = asyncio.create_task(wait_for_connection(**coros))
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
+ )
151
+ self._connect_task = asyncio.create_task(coro)
116
152
 
117
153
  assert self._connect_task, "Connect task not created, this shouldn't happen"
118
154
  # Wait for it to complete
119
155
  await self._connect_task
120
156
 
121
157
 
122
- VT = TypeVar("VT", bound=Device)
158
+ DeviceT = TypeVar("DeviceT", bound=Device)
123
159
 
124
160
 
125
- class DeviceVector(dict[int, VT], Device):
161
+ class DeviceVector(MutableMapping[int, DeviceT], Device):
126
162
  """
127
163
  Defines device components with indices.
128
164
 
@@ -131,10 +167,48 @@ class DeviceVector(dict[int, VT], Device):
131
167
  :class:`~ophyd_async.epics.demo.DynamicSensorGroup`
132
168
  """
133
169
 
134
- def children(self) -> Generator[tuple[str, Device], None, None]:
135
- for attr_name, attr in self.items():
136
- if isinstance(attr, Device):
137
- yield str(attr_name), attr
170
+ def __init__(
171
+ self,
172
+ children: Mapping[int, DeviceT],
173
+ name: str = "",
174
+ ) -> None:
175
+ self._children = dict(children)
176
+ super().__init__(name=name)
177
+
178
+ def __setattr__(self, name: str, child: Any) -> None:
179
+ if name != "parent" and isinstance(child, Device):
180
+ raise AttributeError(
181
+ "DeviceVector can only have integer named children, "
182
+ "set via device_vector[i] = child"
183
+ )
184
+ super().__setattr__(name, child)
185
+
186
+ def __getitem__(self, key: int) -> DeviceT:
187
+ return self._children[key]
188
+
189
+ def __setitem__(self, key: int, value: DeviceT) -> None:
190
+ # Check the types on entry to dict to make sure we can't accidentally
191
+ # make a non-integer named child
192
+ assert isinstance(key, int), f"Expected int, got {key}"
193
+ assert isinstance(value, Device), f"Expected Device, got {value}"
194
+ self._children[key] = value
195
+ value.parent = self
196
+
197
+ def __delitem__(self, key: int) -> None:
198
+ del self._children[key]
199
+
200
+ def __iter__(self) -> Iterator[int]:
201
+ yield from self._children
202
+
203
+ def __len__(self) -> int:
204
+ return len(self._children)
205
+
206
+ def children(self) -> Iterator[tuple[str, Device]]:
207
+ for key, child in self._children.items():
208
+ yield str(key), child
209
+
210
+ def __hash__(self): # to allow DeviceVector to be used as dict keys and in sets
211
+ return hash(id(self))
138
212
 
139
213
 
140
214
  class DeviceCollector:
@@ -195,12 +269,12 @@ class DeviceCollector:
195
269
  ), "No previous frame to the one with self in it, this shouldn't happen"
196
270
  return caller_frame.f_locals
197
271
 
198
- def __enter__(self) -> "DeviceCollector":
272
+ def __enter__(self) -> DeviceCollector:
199
273
  # Stash the names that were defined before we were called
200
274
  self._names_on_enter = set(self._caller_locals())
201
275
  return self
202
276
 
203
- async def __aenter__(self) -> "DeviceCollector":
277
+ async def __aenter__(self) -> DeviceCollector:
204
278
  return self.__enter__()
205
279
 
206
280
  async def _on_exit(self) -> None:
@@ -224,6 +298,11 @@ class DeviceCollector:
224
298
  await self._on_exit()
225
299
 
226
300
  def __exit__(self, type_, value, traceback):
301
+ if in_bluesky_event_loop():
302
+ raise RuntimeError(
303
+ "Cannot use DeviceConnector inside a plan, instead use "
304
+ "`yield from ophyd_async.plan_stubs.ensure_connected(device)`"
305
+ )
227
306
  self._objects_on_exit = self._caller_locals()
228
307
  try:
229
308
  fut = call_in_bluesky_event_loop(self._on_exit())