ophyd-async 0.3a2__py3-none-any.whl → 0.3a4__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 (53) hide show
  1. ophyd_async/_version.py +1 -1
  2. ophyd_async/core/__init__.py +35 -11
  3. ophyd_async/core/async_status.py +2 -0
  4. ophyd_async/core/detector.py +8 -9
  5. ophyd_async/core/device.py +22 -9
  6. ophyd_async/core/flyer.py +2 -2
  7. ophyd_async/core/mock_signal_backend.py +86 -0
  8. ophyd_async/core/mock_signal_utils.py +149 -0
  9. ophyd_async/core/signal.py +140 -49
  10. ophyd_async/core/signal_backend.py +2 -2
  11. ophyd_async/core/{sim_signal_backend.py → soft_signal_backend.py} +29 -39
  12. ophyd_async/core/standard_readable.py +211 -24
  13. ophyd_async/epics/_backend/_aioca.py +17 -13
  14. ophyd_async/epics/_backend/_p4p.py +28 -18
  15. ophyd_async/epics/_backend/common.py +17 -17
  16. ophyd_async/epics/areadetector/__init__.py +4 -4
  17. ophyd_async/epics/areadetector/aravis.py +7 -9
  18. ophyd_async/epics/areadetector/controllers/__init__.py +2 -1
  19. ophyd_async/epics/areadetector/controllers/kinetix_controller.py +49 -0
  20. ophyd_async/epics/areadetector/controllers/vimba_controller.py +66 -0
  21. ophyd_async/epics/areadetector/drivers/__init__.py +6 -0
  22. ophyd_async/epics/areadetector/drivers/ad_base.py +12 -10
  23. ophyd_async/epics/areadetector/drivers/aravis_driver.py +7 -5
  24. ophyd_async/epics/areadetector/drivers/kinetix_driver.py +27 -0
  25. ophyd_async/epics/areadetector/drivers/pilatus_driver.py +5 -2
  26. ophyd_async/epics/areadetector/drivers/vimba_driver.py +63 -0
  27. ophyd_async/epics/areadetector/kinetix.py +46 -0
  28. ophyd_async/epics/areadetector/pilatus.py +7 -12
  29. ophyd_async/epics/areadetector/single_trigger_det.py +14 -6
  30. ophyd_async/epics/areadetector/utils.py +2 -12
  31. ophyd_async/epics/areadetector/vimba.py +43 -0
  32. ophyd_async/epics/areadetector/writers/hdf_writer.py +6 -3
  33. ophyd_async/epics/areadetector/writers/nd_file_hdf.py +21 -18
  34. ophyd_async/epics/areadetector/writers/nd_plugin.py +6 -7
  35. ophyd_async/epics/demo/__init__.py +19 -22
  36. ophyd_async/epics/motion/motor.py +16 -13
  37. ophyd_async/epics/pvi/pvi.py +11 -11
  38. ophyd_async/epics/signal/signal.py +1 -1
  39. ophyd_async/log.py +130 -0
  40. ophyd_async/panda/_hdf_panda.py +3 -3
  41. ophyd_async/panda/writers/_hdf_writer.py +3 -3
  42. ophyd_async/protocols.py +26 -3
  43. ophyd_async/sim/demo/sim_motor.py +14 -12
  44. ophyd_async/sim/pattern_generator.py +9 -9
  45. ophyd_async/sim/sim_pattern_detector_writer.py +2 -2
  46. ophyd_async/sim/sim_pattern_generator.py +2 -2
  47. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a4.dist-info}/METADATA +20 -3
  48. ophyd_async-0.3a4.dist-info/RECORD +85 -0
  49. ophyd_async-0.3a2.dist-info/RECORD +0 -76
  50. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a4.dist-info}/LICENSE +0 -0
  51. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a4.dist-info}/WHEEL +0 -0
  52. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a4.dist-info}/entry_points.txt +0 -0
  53. {ophyd_async-0.3a2.dist-info → ophyd_async-0.3a4.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.3a2'
15
+ __version__ = version = '0.3a4'
16
16
  __version_tuple__ = version_tuple = (0, 3)
@@ -24,24 +24,37 @@ from .device_save_loader import (
24
24
  walk_rw_signals,
25
25
  )
26
26
  from .flyer import HardwareTriggeredFlyable, TriggerLogic
27
+ from .mock_signal_backend import (
28
+ MockSignalBackend,
29
+ )
30
+ from .mock_signal_utils import (
31
+ assert_mock_put_called_with,
32
+ callback_on_mock_put,
33
+ mock_puts_blocked,
34
+ reset_mock_put_calls,
35
+ set_mock_put_proceeds,
36
+ set_mock_value,
37
+ set_mock_values,
38
+ )
27
39
  from .signal import (
28
40
  Signal,
29
41
  SignalR,
30
42
  SignalRW,
31
43
  SignalW,
32
44
  SignalX,
45
+ assert_configuration,
46
+ assert_emitted,
47
+ assert_reading,
48
+ assert_value,
33
49
  observe_value,
34
50
  set_and_wait_for_value,
35
- set_sim_callback,
36
- set_sim_put_proceeds,
37
- set_sim_value,
38
- soft_signal_r_and_backend,
51
+ soft_signal_r_and_setter,
39
52
  soft_signal_rw,
40
53
  wait_for_value,
41
54
  )
42
55
  from .signal_backend import SignalBackend
43
- from .sim_signal_backend import SimSignalBackend
44
- from .standard_readable import StandardReadable
56
+ from .soft_signal_backend import SoftSignalBackend
57
+ from .standard_readable import ConfigSignal, HintedSignal, StandardReadable
45
58
  from .utils import (
46
59
  DEFAULT_TIMEOUT,
47
60
  Callback,
@@ -55,9 +68,15 @@ from .utils import (
55
68
  )
56
69
 
57
70
  __all__ = [
71
+ "assert_mock_put_called_with",
72
+ "callback_on_mock_put",
73
+ "mock_puts_blocked",
74
+ "set_mock_values",
75
+ "reset_mock_put_calls",
58
76
  "SignalBackend",
59
- "SimSignalBackend",
77
+ "SoftSignalBackend",
60
78
  "DetectorControl",
79
+ "MockSignalBackend",
61
80
  "DetectorTrigger",
62
81
  "DetectorWriter",
63
82
  "StandardDetector",
@@ -69,13 +88,12 @@ __all__ = [
69
88
  "SignalW",
70
89
  "SignalRW",
71
90
  "SignalX",
72
- "soft_signal_r_and_backend",
91
+ "soft_signal_r_and_setter",
73
92
  "soft_signal_rw",
74
93
  "observe_value",
75
94
  "set_and_wait_for_value",
76
- "set_sim_callback",
77
- "set_sim_put_proceeds",
78
- "set_sim_value",
95
+ "set_mock_put_proceeds",
96
+ "set_mock_value",
79
97
  "wait_for_value",
80
98
  "AsyncStatus",
81
99
  "DirectoryInfo",
@@ -84,6 +102,8 @@ __all__ = [
84
102
  "ShapeProvider",
85
103
  "StaticDirectoryProvider",
86
104
  "StandardReadable",
105
+ "ConfigSignal",
106
+ "HintedSignal",
87
107
  "TriggerInfo",
88
108
  "TriggerLogic",
89
109
  "HardwareTriggeredFlyable",
@@ -103,4 +123,8 @@ __all__ = [
103
123
  "walk_rw_signals",
104
124
  "load_device",
105
125
  "save_device",
126
+ "assert_reading",
127
+ "assert_value",
128
+ "assert_configuration",
129
+ "assert_emitted",
106
130
  ]
@@ -21,7 +21,9 @@ class AsyncStatus(Status):
21
21
  self.task = awaitable
22
22
  else:
23
23
  self.task = asyncio.create_task(awaitable) # type: ignore
24
+
24
25
  self.task.add_done_callback(self._run_callbacks)
26
+
25
27
  self._callbacks = cast(List[Callback[Status]], [])
26
28
  self._watchers = watchers
27
29
 
@@ -19,7 +19,7 @@ from typing import (
19
19
 
20
20
  from bluesky.protocols import (
21
21
  Collectable,
22
- Descriptor,
22
+ DataKey,
23
23
  Flyable,
24
24
  Preparable,
25
25
  Reading,
@@ -33,7 +33,6 @@ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
33
33
 
34
34
  from .async_status import AsyncStatus
35
35
  from .device import Device
36
- from .signal import SignalR
37
36
  from .utils import DEFAULT_TIMEOUT, merge_gathered_dicts
38
37
 
39
38
  T = TypeVar("T")
@@ -110,7 +109,7 @@ class DetectorWriter(ABC):
110
109
  (e.g. an HDF5 file)"""
111
110
 
112
111
  @abstractmethod
113
- async def open(self, multiplier: int = 1) -> Dict[str, Descriptor]:
112
+ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
114
113
  """Open writer and wait for it to be ready for data.
115
114
 
116
115
  Args:
@@ -161,7 +160,7 @@ class StandardDetector(
161
160
  self,
162
161
  controller: DetectorControl,
163
162
  writer: DetectorWriter,
164
- config_sigs: Sequence[SignalR] = (),
163
+ config_sigs: Sequence[AsyncReadable] = (),
165
164
  name: str = "",
166
165
  writer_timeout: float = DEFAULT_TIMEOUT,
167
166
  ) -> None:
@@ -181,7 +180,7 @@ class StandardDetector(
181
180
  """
182
181
  self._controller = controller
183
182
  self._writer = writer
184
- self._describe: Dict[str, Descriptor] = {}
183
+ self._describe: Dict[str, DataKey] = {}
185
184
  self._config_sigs = list(config_sigs)
186
185
  self._frame_writing_timeout = writer_timeout
187
186
  # For prepare
@@ -214,7 +213,7 @@ class StandardDetector(
214
213
  async def _check_config_sigs(self):
215
214
  """Checks configuration signals are named and connected."""
216
215
  for signal in self._config_sigs:
217
- if signal._name == "":
216
+ if signal.name == "":
218
217
  raise Exception(
219
218
  "config signal must be named before it is passed to the detector"
220
219
  )
@@ -234,14 +233,14 @@ class StandardDetector(
234
233
  async def read_configuration(self) -> Dict[str, Reading]:
235
234
  return await merge_gathered_dicts(sig.read() for sig in self._config_sigs)
236
235
 
237
- async def describe_configuration(self) -> Dict[str, Descriptor]:
236
+ async def describe_configuration(self) -> Dict[str, DataKey]:
238
237
  return await merge_gathered_dicts(sig.describe() for sig in self._config_sigs)
239
238
 
240
239
  async def read(self) -> Dict[str, Reading]:
241
240
  # All data is in StreamResources, not Events, so nothing to output here
242
241
  return {}
243
242
 
244
- async def describe(self) -> Dict[str, Descriptor]:
243
+ async def describe(self) -> Dict[str, DataKey]:
245
244
  return self._describe
246
245
 
247
246
  @AsyncStatus.wrap
@@ -330,7 +329,7 @@ class StandardDetector(
330
329
  assert self._fly_status, "Kickoff not run"
331
330
  return await self._fly_status
332
331
 
333
- async def describe_collect(self) -> Dict[str, Descriptor]:
332
+ async def describe_collect(self) -> Dict[str, DataKey]:
334
333
  return self._describe
335
334
 
336
335
  async def collect_asset_docs(
@@ -3,6 +3,8 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import sys
6
+ from functools import cached_property
7
+ from logging import LoggerAdapter, getLogger
6
8
  from typing import (
7
9
  Any,
8
10
  Coroutine,
@@ -39,6 +41,12 @@ class Device(HasName):
39
41
  """Return the name of the Device"""
40
42
  return self._name
41
43
 
44
+ @cached_property
45
+ def log(self):
46
+ return LoggerAdapter(
47
+ getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
48
+ )
49
+
42
50
  def children(self) -> Iterator[Tuple[str, Device]]:
43
51
  for attr_name, attr in self.__dict__.items():
44
52
  if attr_name != "parent" and isinstance(attr, Device):
@@ -52,26 +60,31 @@ class Device(HasName):
52
60
  name:
53
61
  New name to set
54
62
  """
63
+
64
+ # Ensure self.log is recreated after a name change
65
+ if hasattr(self, "log"):
66
+ del self.log
67
+
55
68
  self._name = name
56
69
  for attr_name, child in self.children():
57
70
  child_name = f"{name}-{attr_name.rstrip('_')}" if name else ""
58
71
  child.set_name(child_name)
59
72
  child.parent = self
60
73
 
61
- async def connect(self, sim: bool = False, timeout: float = DEFAULT_TIMEOUT):
74
+ async def connect(self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT):
62
75
  """Connect self and all child Devices.
63
76
 
64
77
  Contains a timeout that gets propagated to child.connect methods.
65
78
 
66
79
  Parameters
67
80
  ----------
68
- sim:
69
- If True then connect in simulation mode.
81
+ mock:
82
+ If True then use ``MockSignalBackend`` for all Signals
70
83
  timeout:
71
84
  Time to wait before failing with a TimeoutError.
72
85
  """
73
86
  coros = {
74
- name: child_device.connect(sim, timeout=timeout)
87
+ name: child_device.connect(mock=mock, timeout=timeout)
75
88
  for name, child_device in self.children()
76
89
  }
77
90
  if coros:
@@ -105,9 +118,9 @@ class DeviceCollector:
105
118
  If True, call ``device.set_name(variable_name)`` on all collected
106
119
  Devices
107
120
  connect:
108
- If True, call ``device.connect(sim)`` in parallel on all
121
+ If True, call ``device.connect(mock)`` in parallel on all
109
122
  collected Devices
110
- sim:
123
+ mock:
111
124
  If True, connect Signals in simulation mode
112
125
  timeout:
113
126
  How long to wait for connect before logging an exception
@@ -129,12 +142,12 @@ class DeviceCollector:
129
142
  self,
130
143
  set_name=True,
131
144
  connect=True,
132
- sim=False,
145
+ mock=False,
133
146
  timeout: float = 10.0,
134
147
  ):
135
148
  self._set_name = set_name
136
149
  self._connect = connect
137
- self._sim = sim
150
+ self._mock = mock
138
151
  self._timeout = timeout
139
152
  self._names_on_enter: Set[str] = set()
140
153
  self._objects_on_exit: Dict[str, Any] = {}
@@ -168,7 +181,7 @@ class DeviceCollector:
168
181
  obj.set_name(name)
169
182
  if self._connect:
170
183
  connect_coroutines[name] = obj.connect(
171
- self._sim, timeout=self._timeout
184
+ self._mock, timeout=self._timeout
172
185
  )
173
186
 
174
187
  # Connect to all the devices
ophyd_async/core/flyer.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Dict, Generic, Sequence, TypeVar
3
3
 
4
- from bluesky.protocols import Descriptor, Flyable, Preparable, Reading, Stageable
4
+ from bluesky.protocols import DataKey, Flyable, Preparable, Reading, Stageable
5
5
 
6
6
  from .async_status import AsyncStatus
7
7
  from .device import Device
@@ -74,7 +74,7 @@ class HardwareTriggeredFlyable(
74
74
  async def complete(self) -> None:
75
75
  await self._trigger_logic.complete()
76
76
 
77
- async def describe_configuration(self) -> Dict[str, Descriptor]:
77
+ async def describe_configuration(self) -> Dict[str, DataKey]:
78
78
  return await merge_gathered_dicts(
79
79
  [sig.describe() for sig in self._configuration_signals]
80
80
  )
@@ -0,0 +1,86 @@
1
+ import asyncio
2
+ from typing import Optional, Type
3
+ from unittest.mock import MagicMock
4
+
5
+ from bluesky.protocols import Descriptor, Reading
6
+
7
+ from ophyd_async.core.signal_backend import SignalBackend
8
+ from ophyd_async.core.soft_signal_backend import SoftSignalBackend
9
+ from ophyd_async.core.utils import DEFAULT_TIMEOUT, ReadingValueCallback, T
10
+
11
+
12
+ class MockSignalBackend(SignalBackend):
13
+ def __init__(
14
+ self,
15
+ datatype: Optional[Type[T]] = None,
16
+ initial_backend: Optional[SignalBackend[T]] = None,
17
+ ) -> None:
18
+ if isinstance(initial_backend, MockSignalBackend):
19
+ raise ValueError("Cannot make a MockSignalBackend for a MockSignalBackends")
20
+
21
+ self.initial_backend = initial_backend
22
+
23
+ if datatype is None:
24
+ assert (
25
+ self.initial_backend
26
+ ), "Must supply either initial_backend or datatype"
27
+ datatype = self.initial_backend.datatype
28
+
29
+ self.datatype = datatype
30
+
31
+ if not isinstance(self.initial_backend, SoftSignalBackend):
32
+ # If the backend is a hard signal backend, or not provided,
33
+ # then we create a soft signal to mimick it
34
+
35
+ self.soft_backend = SoftSignalBackend(datatype=datatype)
36
+ else:
37
+ self.soft_backend = initial_backend
38
+
39
+ self.mock = MagicMock()
40
+
41
+ self.put_proceeds = asyncio.Event()
42
+ self.put_proceeds.set()
43
+
44
+ def source(self, name: str) -> str:
45
+ self.mock.source(name)
46
+ if self.initial_backend:
47
+ return f"mock+{self.initial_backend.source(name)}"
48
+ return f"mock+{name}"
49
+
50
+ async def connect(self, timeout: float = DEFAULT_TIMEOUT) -> None:
51
+ self.mock.connect(timeout=timeout)
52
+
53
+ async def put(self, value: Optional[T], wait=True, timeout=None):
54
+ self.mock.put(value, wait=wait, timeout=timeout)
55
+ await self.soft_backend.put(value, wait=wait, timeout=timeout)
56
+
57
+ if wait:
58
+ await asyncio.wait_for(self.put_proceeds.wait(), timeout=timeout)
59
+
60
+ def set_value(self, value: T):
61
+ self.mock.set_value(value)
62
+ self.soft_backend.set_value(value)
63
+
64
+ async def get_descriptor(self, source: str) -> Descriptor:
65
+ self.mock.get_descriptor(source)
66
+ return await self.soft_backend.get_descriptor(source)
67
+
68
+ async def get_reading(self) -> Reading:
69
+ self.mock.get_reading()
70
+ return await self.soft_backend.get_reading()
71
+
72
+ async def get_value(self) -> T:
73
+ self.mock.get_value()
74
+ return await self.soft_backend.get_value()
75
+
76
+ async def get_setpoint(self) -> T:
77
+ """For a soft signal, the setpoint and readback values are the same."""
78
+ self.mock.get_setpoint()
79
+ return await self.soft_backend.get_setpoint()
80
+
81
+ async def get_datakey(self, source: str) -> Descriptor:
82
+ return await self.soft_backend.get_datakey(source)
83
+
84
+ def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
85
+ self.mock.set_callback(callback)
86
+ self.soft_backend.set_callback(callback)
@@ -0,0 +1,149 @@
1
+ from contextlib import asynccontextmanager, contextmanager
2
+ from typing import Any, Callable, Generator, Iterable, Iterator, List
3
+ from unittest.mock import ANY
4
+
5
+ from ophyd_async.core.signal import Signal
6
+ from ophyd_async.core.utils import T
7
+
8
+ from .mock_signal_backend import MockSignalBackend
9
+
10
+
11
+ def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend:
12
+ assert isinstance(signal._backend, MockSignalBackend), (
13
+ "Expected to receive a `MockSignalBackend`, instead "
14
+ f" received {type(signal._backend)}. "
15
+ )
16
+ return signal._backend
17
+
18
+
19
+ def set_mock_value(signal: Signal[T], value: T):
20
+ """Set the value of a signal that is in mock mode."""
21
+ backend = _get_mock_signal_backend(signal)
22
+ backend.set_value(value)
23
+
24
+
25
+ def set_mock_put_proceeds(signal: Signal[T], proceeds: bool):
26
+ """Allow or block a put with wait=True from proceeding"""
27
+ backend = _get_mock_signal_backend(signal)
28
+
29
+ if proceeds:
30
+ backend.put_proceeds.set()
31
+ else:
32
+ backend.put_proceeds.clear()
33
+
34
+
35
+ @asynccontextmanager
36
+ async def mock_puts_blocked(*signals: List[Signal]):
37
+ for signal in signals:
38
+ set_mock_put_proceeds(signal, False)
39
+ yield
40
+ for signal in signals:
41
+ set_mock_put_proceeds(signal, True)
42
+
43
+
44
+ def assert_mock_put_called_with(signal: Signal, value: Any, wait=ANY, timeout=ANY):
45
+ backend = _get_mock_signal_backend(signal)
46
+ backend.mock.put.assert_called_with(value, wait=wait, timeout=timeout)
47
+
48
+
49
+ def reset_mock_put_calls(signal: Signal):
50
+ backend = _get_mock_signal_backend(signal)
51
+ backend.mock.put.reset_mock()
52
+
53
+
54
+ class _SetValuesIterator:
55
+ # Garbage collected by the time __del__ is called unless we put it as a
56
+ # global attrbute here.
57
+ require_all_consumed: bool = False
58
+
59
+ def __init__(
60
+ self,
61
+ signal: Signal,
62
+ values: Iterable[Any],
63
+ require_all_consumed: bool = False,
64
+ ):
65
+ self.signal = signal
66
+ self.values = values
67
+ self.require_all_consumed = require_all_consumed
68
+ self.index = 0
69
+
70
+ self.iterator = enumerate(values, start=1)
71
+
72
+ def __iter__(self):
73
+ return self
74
+
75
+ def __next__(self):
76
+ # Will propogate StopIteration
77
+ self.index, next_value = next(self.iterator)
78
+ set_mock_value(self.signal, next_value)
79
+ return next_value
80
+
81
+ def __del__(self):
82
+ if self.require_all_consumed and self.index != len(self.values):
83
+ raise AssertionError("Not all values have been consumed.")
84
+
85
+
86
+ def set_mock_values(
87
+ signal: Signal,
88
+ values: Iterable[Any],
89
+ require_all_consumed: bool = False,
90
+ ) -> Iterator[Any]:
91
+ """Iterator to set a signal to a sequence of values, optionally repeating the
92
+ sequence.
93
+
94
+ Parameters
95
+ ----------
96
+ signal:
97
+ A signal with a `MockSignalBackend` backend.
98
+ values:
99
+ An iterable of the values to set the signal to, on each iteration
100
+ the value will be set.
101
+ require_all_consumed:
102
+ If True, an AssertionError will be raised if the iterator is deleted before
103
+ all values have been consumed.
104
+
105
+ Notes
106
+ -----
107
+ Example usage::
108
+
109
+ for value_set in set_mock_values(signal, [1, 2, 3]):
110
+ # do something
111
+
112
+ cm = set_mock_values(signal, 1, 2, 3, require_all_consumed=True):
113
+ next(cm)
114
+ # do something
115
+ """
116
+
117
+ return _SetValuesIterator(
118
+ signal,
119
+ values,
120
+ require_all_consumed=require_all_consumed,
121
+ )
122
+
123
+
124
+ @contextmanager
125
+ def _unset_side_effect_cm(mock):
126
+ yield
127
+ mock.put.side_effect = None
128
+
129
+
130
+ # linting isn't smart enought to realize @contextmanager will give use a
131
+ # ContextManager[None]
132
+ def callback_on_mock_put(
133
+ signal: Signal, callback: Callable[[T], None]
134
+ ) -> Generator[None, None, None]:
135
+ """For setting a callback when a backend is put to.
136
+
137
+ Can either be used in a context, with the callback being
138
+ unset on exit, or as an ordinary function.
139
+
140
+ Parameters
141
+ ----------
142
+ signal:
143
+ A signal with a `MockSignalBackend` backend.
144
+ callback:
145
+ The callback to call when the backend is put to during the context.
146
+ """
147
+ backend = _get_mock_signal_backend(signal)
148
+ backend.mock.put.side_effect = callback
149
+ return _unset_side_effect_cm(backend.mock)