ophyd-async 0.7.0__py3-none-any.whl → 0.8.0__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 (92) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +34 -9
  3. ophyd_async/core/_detector.py +5 -10
  4. ophyd_async/core/_device.py +170 -68
  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 +35 -40
  8. ophyd_async/core/_mock_signal_utils.py +25 -16
  9. ophyd_async/core/_protocol.py +28 -8
  10. ophyd_async/core/_readable.py +133 -134
  11. ophyd_async/core/_signal.py +219 -163
  12. ophyd_async/core/_signal_backend.py +131 -64
  13. ophyd_async/core/_soft_signal_backend.py +131 -194
  14. ophyd_async/core/_status.py +22 -6
  15. ophyd_async/core/_table.py +102 -100
  16. ophyd_async/core/_utils.py +143 -32
  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/_core_logic.py +3 -1
  21. ophyd_async/epics/adcore/_hdf_writer.py +2 -2
  22. ophyd_async/epics/adcore/_single_trigger.py +6 -10
  23. ophyd_async/epics/adcore/_utils.py +15 -10
  24. ophyd_async/epics/adkinetix/__init__.py +2 -1
  25. ophyd_async/epics/adkinetix/_kinetix_controller.py +6 -3
  26. ophyd_async/epics/adkinetix/_kinetix_io.py +4 -5
  27. ophyd_async/epics/adpilatus/_pilatus_controller.py +2 -2
  28. ophyd_async/epics/adpilatus/_pilatus_io.py +3 -4
  29. ophyd_async/epics/adsimdetector/_sim_controller.py +2 -2
  30. ophyd_async/epics/advimba/__init__.py +4 -1
  31. ophyd_async/epics/advimba/_vimba_controller.py +6 -3
  32. ophyd_async/epics/advimba/_vimba_io.py +8 -9
  33. ophyd_async/epics/core/__init__.py +26 -0
  34. ophyd_async/epics/core/_aioca.py +323 -0
  35. ophyd_async/epics/core/_epics_connector.py +53 -0
  36. ophyd_async/epics/core/_epics_device.py +13 -0
  37. ophyd_async/epics/core/_p4p.py +383 -0
  38. ophyd_async/epics/core/_pvi_connector.py +91 -0
  39. ophyd_async/epics/core/_signal.py +171 -0
  40. ophyd_async/epics/core/_util.py +61 -0
  41. ophyd_async/epics/demo/_mover.py +4 -5
  42. ophyd_async/epics/demo/_sensor.py +14 -13
  43. ophyd_async/epics/eiger/_eiger.py +1 -2
  44. ophyd_async/epics/eiger/_eiger_controller.py +7 -2
  45. ophyd_async/epics/eiger/_eiger_io.py +3 -5
  46. ophyd_async/epics/eiger/_odin_io.py +5 -5
  47. ophyd_async/epics/motor.py +4 -5
  48. ophyd_async/epics/signal.py +11 -0
  49. ophyd_async/epics/testing/__init__.py +24 -0
  50. ophyd_async/epics/testing/_example_ioc.py +105 -0
  51. ophyd_async/epics/testing/_utils.py +78 -0
  52. ophyd_async/epics/testing/test_records.db +152 -0
  53. ophyd_async/epics/testing/test_records_pva.db +177 -0
  54. ophyd_async/fastcs/core.py +9 -0
  55. ophyd_async/fastcs/panda/__init__.py +4 -4
  56. ophyd_async/fastcs/panda/_block.py +18 -13
  57. ophyd_async/fastcs/panda/_control.py +3 -5
  58. ophyd_async/fastcs/panda/_hdf_panda.py +5 -19
  59. ophyd_async/fastcs/panda/_table.py +30 -52
  60. ophyd_async/fastcs/panda/_trigger.py +8 -8
  61. ophyd_async/fastcs/panda/_writer.py +2 -5
  62. ophyd_async/plan_stubs/_ensure_connected.py +20 -13
  63. ophyd_async/plan_stubs/_fly.py +2 -2
  64. ophyd_async/plan_stubs/_nd_attributes.py +5 -4
  65. ophyd_async/py.typed +0 -0
  66. ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +1 -2
  67. ophyd_async/sim/demo/_sim_motor.py +3 -4
  68. ophyd_async/tango/__init__.py +0 -45
  69. ophyd_async/tango/{signal → core}/__init__.py +9 -6
  70. ophyd_async/tango/core/_base_device.py +132 -0
  71. ophyd_async/tango/{signal → core}/_signal.py +42 -53
  72. ophyd_async/tango/{base_devices → core}/_tango_readable.py +3 -4
  73. ophyd_async/tango/{signal → core}/_tango_transport.py +38 -40
  74. ophyd_async/tango/demo/_counter.py +12 -23
  75. ophyd_async/tango/demo/_mover.py +13 -13
  76. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/METADATA +52 -55
  77. ophyd_async-0.8.0.dist-info/RECORD +116 -0
  78. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/WHEEL +1 -1
  79. ophyd_async/epics/pvi/__init__.py +0 -3
  80. ophyd_async/epics/pvi/_pvi.py +0 -338
  81. ophyd_async/epics/signal/__init__.py +0 -21
  82. ophyd_async/epics/signal/_aioca.py +0 -378
  83. ophyd_async/epics/signal/_common.py +0 -57
  84. ophyd_async/epics/signal/_epics_transport.py +0 -34
  85. ophyd_async/epics/signal/_p4p.py +0 -518
  86. ophyd_async/epics/signal/_signal.py +0 -114
  87. ophyd_async/tango/base_devices/__init__.py +0 -4
  88. ophyd_async/tango/base_devices/_base_device.py +0 -225
  89. ophyd_async-0.7.0.dist-info/RECORD +0 -108
  90. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/LICENSE +0 -0
  91. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.dist-info}/entry_points.txt +0 -0
  92. {ophyd_async-0.7.0.dist-info → ophyd_async-0.8.0.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.0'
16
- __version_tuple__ = version_tuple = (0, 7, 0)
15
+ __version__ = version = '0.8.0'
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,
@@ -54,6 +61,7 @@ from ._signal import (
54
61
  assert_emitted,
55
62
  assert_reading,
56
63
  assert_value,
64
+ observe_signals_value,
57
65
  observe_value,
58
66
  set_and_wait_for_other_value,
59
67
  set_and_wait_for_value,
@@ -62,9 +70,11 @@ from ._signal import (
62
70
  wait_for_value,
63
71
  )
64
72
  from ._signal_backend import (
65
- RuntimeSubsetEnum,
73
+ Array1D,
66
74
  SignalBackend,
67
- SubsetEnum,
75
+ SignalDatatype,
76
+ SignalDatatypeT,
77
+ make_datakey,
68
78
  )
69
79
  from ._soft_signal_backend import SignalMetadata, SoftSignalBackend
70
80
  from ._status import AsyncStatus, WatchableAsyncStatus, completed_status
@@ -73,14 +83,18 @@ from ._utils import (
73
83
  CALCULATE_TIMEOUT,
74
84
  DEFAULT_TIMEOUT,
75
85
  CalculatableTimeout,
86
+ Callback,
87
+ LazyMock,
76
88
  NotConnected,
77
- ReadingValueCallback,
89
+ Reference,
90
+ StrictEnum,
91
+ SubsetEnum,
78
92
  T,
79
93
  WatcherUpdate,
80
94
  get_dtype,
95
+ get_enum_cls,
81
96
  get_unique,
82
97
  in_micros,
83
- is_pydantic_model,
84
98
  wait_for_connection,
85
99
  )
86
100
 
@@ -91,8 +105,10 @@ __all__ = [
91
105
  "StandardDetector",
92
106
  "TriggerInfo",
93
107
  "Device",
108
+ "DeviceConnector",
94
109
  "DeviceCollector",
95
110
  "DeviceVector",
111
+ "DeviceFiller",
96
112
  "all_at_once",
97
113
  "get_signal_values",
98
114
  "load_device",
@@ -108,6 +124,7 @@ __all__ = [
108
124
  "config_ophyd_async_logging",
109
125
  "MockSignalBackend",
110
126
  "callback_on_mock_put",
127
+ "get_mock",
111
128
  "get_mock_put",
112
129
  "mock_puts_blocked",
113
130
  "reset_mock_put_calls",
@@ -131,6 +148,7 @@ __all__ = [
131
148
  "ConfigSignal",
132
149
  "HintedSignal",
133
150
  "StandardReadable",
151
+ "StandardReadableFormat",
134
152
  "Signal",
135
153
  "SignalR",
136
154
  "SignalRW",
@@ -141,30 +159,37 @@ __all__ = [
141
159
  "assert_reading",
142
160
  "assert_value",
143
161
  "observe_value",
162
+ "observe_signals_value",
144
163
  "set_and_wait_for_value",
145
164
  "set_and_wait_for_other_value",
146
165
  "soft_signal_r_and_setter",
147
166
  "soft_signal_rw",
148
167
  "wait_for_value",
149
- "RuntimeSubsetEnum",
168
+ "Array1D",
150
169
  "SignalBackend",
170
+ "make_datakey",
171
+ "StrictEnum",
151
172
  "SubsetEnum",
173
+ "SignalDatatype",
174
+ "SignalDatatypeT",
152
175
  "SignalMetadata",
153
176
  "SoftSignalBackend",
154
177
  "AsyncStatus",
155
178
  "WatchableAsyncStatus",
156
179
  "DEFAULT_TIMEOUT",
157
180
  "CalculatableTimeout",
181
+ "Callback",
182
+ "LazyMock",
158
183
  "CALCULATE_TIMEOUT",
159
184
  "NotConnected",
160
- "ReadingValueCallback",
185
+ "Reference",
161
186
  "Table",
162
187
  "T",
163
188
  "WatcherUpdate",
164
189
  "get_dtype",
190
+ "get_enum_cls",
165
191
  "get_unique",
166
192
  "in_micros",
167
- "is_pydantic_model",
168
193
  "wait_for_connection",
169
194
  "completed_status",
170
195
  ]
@@ -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,82 @@
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
5
+ from collections.abc import Coroutine, Iterator, Mapping, MutableMapping
6
6
  from functools import cached_property
7
7
  from logging import LoggerAdapter, getLogger
8
- from typing import (
9
- Any,
10
- Optional,
11
- TypeVar,
12
- )
8
+ from typing import Any, TypeVar
13
9
 
14
10
  from bluesky.protocols import HasName
15
11
  from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
16
12
 
17
- from ._utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
13
+ from ._protocol import Connectable
14
+ from ._utils import DEFAULT_TIMEOUT, LazyMock, NotConnected, wait_for_connection
18
15
 
19
16
 
20
- class Device(HasName):
21
- """Common base class for all Ophyd Async Devices.
17
+ class DeviceConnector:
18
+ """Defines how a `Device` should be connected and type hints processed."""
22
19
 
23
- By default, names and connects all Device children.
24
- """
20
+ def create_children_from_annotations(self, device: Device):
21
+ """Used when children can be created from introspecting the hardware.
22
+
23
+ Some control systems allow introspection of a device to determine what
24
+ children it has. To allow this to work nicely with typing we add these
25
+ hints to the Device like so::
26
+
27
+ my_signal: SignalRW[int]
28
+ my_device: MyDevice
29
+
30
+ This method will be run during ``Device.__init__``, and is responsible
31
+ for turning all of those type hints into real Signal and Device instances.
32
+
33
+ Subsequent runs of this function should do nothing, to allow it to be
34
+ called early in Devices that need to pass references to their children
35
+ during ``__init__``.
36
+ """
37
+
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):
50
+ """Used during ``Device.connect``.
51
+
52
+ This is called when a previous connect has not been done, or has been
53
+ done in a different mock more. It should connect the Device and all its
54
+ children.
55
+ """
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
+ }
61
+ await wait_for_connection(**coros)
62
+
63
+
64
+ class Device(HasName, Connectable):
65
+ """Common base class for all Ophyd Async Devices."""
25
66
 
26
67
  _name: str = ""
27
68
  #: The parent Device if it exists
28
- parent: Optional["Device"] = None
69
+ parent: Device | None = None
29
70
  # None if connect hasn't started, a Task if it has
30
71
  _connect_task: asyncio.Task | None = None
72
+ # The mock if we have connected in mock mode
73
+ _mock: LazyMock | None = None
31
74
 
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:
75
+ def __init__(
76
+ self, name: str = "", connector: DeviceConnector | None = None
77
+ ) -> None:
78
+ self._connector = connector or DeviceConnector()
79
+ self._connector.create_children_from_annotations(self)
37
80
  self.set_name(name)
38
81
 
39
82
  @property
@@ -42,16 +85,18 @@ class Device(HasName):
42
85
  return self._name
43
86
 
44
87
  @cached_property
45
- def log(self):
88
+ def _child_devices(self) -> dict[str, Device]:
89
+ return {}
90
+
91
+ def children(self) -> Iterator[tuple[str, Device]]:
92
+ yield from self._child_devices.items()
93
+
94
+ @cached_property
95
+ def log(self) -> LoggerAdapter:
46
96
  return LoggerAdapter(
47
97
  getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
48
98
  )
49
99
 
50
- def children(self) -> Iterator[tuple[str, "Device"]]:
51
- for attr_name, attr in self.__dict__.items():
52
- if attr_name != "parent" and isinstance(attr, Device):
53
- yield attr_name, attr
54
-
55
100
  def set_name(self, name: str):
56
101
  """Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
57
102
 
@@ -60,23 +105,37 @@ class Device(HasName):
60
105
  name:
61
106
  New name to set
62
107
  """
63
-
64
- # Ensure self.log is recreated after a name change
65
- if hasattr(self, "log"):
66
- del self.log
67
-
68
108
  self._name = name
69
- for attr_name, child in self.children():
70
- child_name = f"{name}-{attr_name.rstrip('_')}" if name else ""
109
+ # Ensure logger is recreated after a name change
110
+ if "log" in self.__dict__:
111
+ del self.log
112
+ for child_name, child in self.children():
113
+ child_name = f"{self.name}-{child_name.strip('_')}" if self.name else ""
71
114
  child.set_name(child_name)
72
- child.parent = self
115
+
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...
119
+ if name == "parent":
120
+ if self.parent not in (value, None):
121
+ raise TypeError(
122
+ f"Cannot set the parent of {self} to be {value}: "
123
+ f"it is already a child of {self.parent}"
124
+ )
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):
128
+ value.parent = self
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)
73
132
 
74
133
  async def connect(
75
134
  self,
76
- mock: bool = False,
135
+ mock: bool | LazyMock = False,
77
136
  timeout: float = DEFAULT_TIMEOUT,
78
137
  force_reconnect: bool = False,
79
- ):
138
+ ) -> None:
80
139
  """Connect self and all child Devices.
81
140
 
82
141
  Contains a timeout that gets propagated to child.connect methods.
@@ -88,41 +147,45 @@ class Device(HasName):
88
147
  timeout:
89
148
  Time to wait before failing with a TimeoutError.
90
149
  """
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."
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())
100
165
  )
101
- self._previous_connect_was_mock = mock
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
102
173
 
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()
106
- )
107
- 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))
116
174
 
117
- assert self._connect_task, "Connect task not created, this shouldn't happen"
118
- # Wait for it to complete
119
- await self._connect_task
175
+ _not_device_attrs = {
176
+ "_name",
177
+ "_children",
178
+ "_connector",
179
+ "_timeout",
180
+ "_mock",
181
+ "_connect_task",
182
+ }
120
183
 
121
184
 
122
- VT = TypeVar("VT", bound=Device)
185
+ DeviceT = TypeVar("DeviceT", bound=Device)
123
186
 
124
187
 
125
- class DeviceVector(dict[int, VT], Device):
188
+ class DeviceVector(MutableMapping[int, DeviceT], Device):
126
189
  """
127
190
  Defines device components with indices.
128
191
 
@@ -131,10 +194,49 @@ class DeviceVector(dict[int, VT], Device):
131
194
  :class:`~ophyd_async.epics.demo.DynamicSensorGroup`
132
195
  """
133
196
 
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
197
+ def __init__(
198
+ self,
199
+ children: Mapping[int, DeviceT],
200
+ name: str = "",
201
+ ) -> None:
202
+ self._children: dict[int, DeviceT] = {}
203
+ self.update(children)
204
+ super().__init__(name=name)
205
+
206
+ def __setattr__(self, name: str, child: Any) -> None:
207
+ if name != "parent" and isinstance(child, Device):
208
+ raise AttributeError(
209
+ "DeviceVector can only have integer named children, "
210
+ "set via device_vector[i] = child"
211
+ )
212
+ super().__setattr__(name, child)
213
+
214
+ def __getitem__(self, key: int) -> DeviceT:
215
+ return self._children[key]
216
+
217
+ def __setitem__(self, key: int, value: DeviceT) -> None:
218
+ # Check the types on entry to dict to make sure we can't accidentally
219
+ # make a non-integer named child
220
+ assert isinstance(key, int), f"Expected int, got {key}"
221
+ assert isinstance(value, Device), f"Expected Device, got {value}"
222
+ self._children[key] = value
223
+ value.parent = self
224
+
225
+ def __delitem__(self, key: int) -> None:
226
+ del self._children[key]
227
+
228
+ def __iter__(self) -> Iterator[int]:
229
+ yield from self._children
230
+
231
+ def __len__(self) -> int:
232
+ return len(self._children)
233
+
234
+ def children(self) -> Iterator[tuple[str, Device]]:
235
+ for key, child in self._children.items():
236
+ yield str(key), child
237
+
238
+ def __hash__(self): # to allow DeviceVector to be used as dict keys and in sets
239
+ return hash(id(self))
138
240
 
139
241
 
140
242
  class DeviceCollector:
@@ -195,12 +297,12 @@ class DeviceCollector:
195
297
  ), "No previous frame to the one with self in it, this shouldn't happen"
196
298
  return caller_frame.f_locals
197
299
 
198
- def __enter__(self) -> "DeviceCollector":
300
+ def __enter__(self) -> DeviceCollector:
199
301
  # Stash the names that were defined before we were called
200
302
  self._names_on_enter = set(self._caller_locals())
201
303
  return self
202
304
 
203
- async def __aenter__(self) -> "DeviceCollector":
305
+ async def __aenter__(self) -> DeviceCollector:
204
306
  return self.__enter__()
205
307
 
206
308
  async def _on_exit(self) -> None: