diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 695d35ea5e..9c85ba4054 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -20,6 +20,7 @@ "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, + "remote.autoForwardPorts": false, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff" } @@ -38,7 +39,7 @@ }, "features": { // add in eternal history and other bash features - "ghcr.io/diamondlightsource/devcontainer-features/bash-config:1.0.0": {} + "ghcr.io/diamondlightsource/devcontainer-features/bash-config:1": {} }, // Create the config folder for the bash-config feature "initializeCommand": "mkdir -p ${localEnv:HOME}/.config/bash-config", diff --git a/docs/_templates/custom-module-template.rst b/docs/_templates/custom-module-template.rst index 726bf49435..9aeca54015 100644 --- a/docs/_templates/custom-module-template.rst +++ b/docs/_templates/custom-module-template.rst @@ -1,8 +1,3 @@ -.. note:: - - Ophyd async is considered experimental until the v1.0 release and - may change API on minor release numbers before then - {{ ('``' + fullname + '``') | underline }} {%- set filtered_members = [] %} diff --git a/docs/conf.py b/docs/conf.py index 7a7d1db7bc..324a4496be 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -98,7 +98,7 @@ autodoc_inherit_docstrings = False # Add some more modules to the top level autosummary -ophyd_async.__all__ += ["sim", "epics", "tango", "fastcs", "plan_stubs"] +ophyd_async.__all__ += ["sim", "epics", "tango", "fastcs", "plan_stubs", "testing"] # Document only what is in __all__ autosummary_ignore_module_all = False diff --git a/docs/examples/epics_demo.py b/docs/examples/epics_demo.py deleted file mode 100644 index 8e446ba2e5..0000000000 --- a/docs/examples/epics_demo.py +++ /dev/null @@ -1,37 +0,0 @@ -# Import bluesky and ophyd -import matplotlib.pyplot as plt -from bluesky import RunEngine -from bluesky.callbacks.best_effort import BestEffortCallback -from bluesky.plan_stubs import mov, movr, rd # noqa -from bluesky.plans import grid_scan # noqa -from bluesky.utils import ProgressBarManager, register_transform -from ophyd import Component, Device, EpicsSignal, EpicsSignalRO - -from ophyd_async.core import init_devices -from ophyd_async.epics import demo - -# Create a run engine, with plotting, progressbar and transform -RE = RunEngine({}, call_returns_result=True) -bec = BestEffortCallback() -RE.subscribe(bec) -RE.waiting_hook = ProgressBarManager() -plt.ion() -register_transform("RE", prefix="<") - -# Start IOC with demo pvs in subprocess -pv_prefix = demo.start_ioc_subprocess() - - -# Create ophyd devices -class OldSensor(Device): - mode = Component(EpicsSignal, "Mode", kind="config") - value = Component(EpicsSignalRO, "Value", kind="hinted") - - -det_old = OldSensor(pv_prefix, name="det_old") - -# Create ophyd-async devices -with init_devices(): - det = demo.Sensor(pv_prefix) - det_group = demo.SensorGroup(pv_prefix) - samp = demo.SampleStage(pv_prefix) diff --git a/docs/explanations/declarative-vs-procedural.md b/docs/explanations/declarative-vs-procedural.md new file mode 100644 index 0000000000..f6612d5798 --- /dev/null +++ b/docs/explanations/declarative-vs-procedural.md @@ -0,0 +1,38 @@ +# Declarative vs Procedural Devices + +Ophyd async has two styles of creating Devices, Declarative and Procedural. This article describes why there are two mechanisms for building Devices, and looks at the pros and cons of each style. + +## Procedural style + +The procedural style mirrors how you would create a traditional python class, you define an `__init__` method, add some class members, then call the superclass `__init__` method. In the case of ophyd async those class members are likely to be Signals and other Devices. For example, in the `ophyd_async.sim.SimMotor` we create its soft signal children in an `__init__` method: +```{literalinclude} ../../src/ophyd_async/sim/_sim_motor.py +:start-after: class SimMotor +:end-before: def set_name +``` +It is explicit and obvious, but verbose. It also allows you to embed arbitrary python logic in the creation of signals, so is required for making soft signals and DeviceVectors with contents based on an argument passed to `__init__`. It also allows you to use the [](#StandardReadable.add_readable_children) context manager which can save some typing. + +## Declarative style + +The declarative style mirrors how you would create a pydantic `BaseModel`. You create type hints to tell the base class what type of object you create, add annotations to tell it some parameters on how to create it, then the base class `__init__` will introspect and create them. For example, in the `ophyd_async.fastcs.panda.PulseBlock` we define the members we expect, and the baseclass will introspect the selected FastCS transport (EPICS IOC or Tango Device Server) and connect them, adding any extras that are published: +```{literalinclude} ../../src/ophyd_async/fastcs/panda/_panda.py +:start-after: for docs: start PulseBlock +:end-before: for docs: end PulseBlock +``` +For a traditional EPICS IOC there is no such introspection mechanism, so we require a PV Suffix to be supplied via an annotation. For example, in `ophyd_async.epics.sim.Counter` we describe the PV Suffix and whether the signal appears in `read()` or `read_configuration()` using [](#typing.Annotated): +```{literalinclude} ../../src/ophyd_async/epics/sim/_counter_.py +:start-after: class Counter +:end-before: class MultiChannelCounter +``` +It is compact and has the minimum amount of boilerplate, but is limited in its scope to what sorts of Signals and Devices the base class can create. It also requires the usage of a [](#StandardReadableFormat) for each Signal if using [](#StandardReadable) which may be more verbose than the procedural approach. It is best suited for introspectable FastCS and Tango devices, and repetitive EPICS Devices that are wrapped into larger Devices like areaDetectors. + +## Grey area + +There is quite a large segment of Devices that could be written both ways, for instance `ophyd_async.epics.sim.Mover`. This could be written in either style with roughly the same legibility, so is a matter of taste: +```{literalinclude} ../../src/ophyd_async/epics/sim/_mover.py +:start-after: class Mover +:end-before: baa +``` + +## Conclusion + +Ophyd async supports both the declarative and procedural style, and is not prescriptive about which is used. In the end the decision is likely to come down to personal taste, and the style of the surrounding code. diff --git a/docs/tutorials/implementing-devices.md b/docs/tutorials/implementing-devices.md new file mode 100644 index 0000000000..e68e63bcd8 --- /dev/null +++ b/docs/tutorials/implementing-devices.md @@ -0,0 +1 @@ +# Implementing Devices diff --git a/docs/tutorials/installation.md b/docs/tutorials/installation.md index a55b96b935..1d8654be90 100644 --- a/docs/tutorials/installation.md +++ b/docs/tutorials/installation.md @@ -27,6 +27,19 @@ You can now use `pip` to install the library and its dependencies: $ python3 -m pip install ophyd-async ``` +If you need to talk to a given control system, you will need to install +the specific extra: +- `ca` for EPICS Channel Access +- `pva` for EPICS PVAccess +- `tango` for Tango +- `demo` for tutorial requirements like h5py and ipython +- `testing` for testing requirements like pytest + +E.g.: +``` +$ python3 -m pip install ophyd-async[ca,demo] +``` + If you require a feature that is not currently released you can also install from github: @@ -38,5 +51,5 @@ The library should now be installed and the commandline interface on your path. You can check the version that has been installed by typing: ``` -$ ophyd-async --version +$ python -m ophyd_async --version ``` diff --git a/docs/tutorials/using-existing-devices.md b/docs/tutorials/using-existing-devices.md new file mode 100644 index 0000000000..2c9c453cc2 --- /dev/null +++ b/docs/tutorials/using-existing-devices.md @@ -0,0 +1,136 @@ +# Using Devices + +In this tutorial we will create a bluesky RunEngine, instantiate some existing ophyd-async Devices, and use them in some bluesky plans. It assumes you have already run through the Bluesky tutorial on [](inv:bluesky#tutorial_run_engine_setup). + +## Run the demo + +Ophyd-async ships with some simulated devices and a demo script that will create them along with a RunEngine. Let's take a look at it now: +```{literalinclude} ../../src/ophyd_async/sim/demo/__main__.py +:language: python +``` + +We will explain the contents in more detail later on, but for now let's run it in an interactive [ipython](https://ipython.org) shell: +``` +$ ipython -i -m ophyd_async.sim +Python 3.11.11 (main, Dec 4 2024, 20:38:25) [GCC 12.2.0] +Type 'copyright', 'credits' or 'license' for more information +IPython 8.30.0 -- An enhanced Interactive Python. Type '?' for help. + +In [1]: +``` + +This has launched an ipython shell, told it to import and run the demo script packaged inside `ophyd_async.sim`, then return to an interactive prompt. + +## Investigate the Devices + +We will look at the `stage.x` and `y` motors first. If we examine them we can see that they have a name: +```python +In [1]: stage.x.name +Out[1]: 'stage-x' +``` + +But if we try to call any of the other methods like `read()` we will see that it doesn't return the value, but a [coroutines](inv:python:std:label#coroutine): + +```python +In [2]: stage.x.read() +Out[2]: +``` + +This is because ophyd-async devices implement async versions of the bluesky [verbs](inv:bluesky#hardware). To get the value we can `await` it: + ```python +In [3]: await stage.x.read() +Out[3]: +{'x-user_readback': {'value': 0.0, + 'timestamp': 367727.615860209, + 'alarm_severity': 0}} +``` + +## Run some plans + +Although it is useful to run the verbs using the `await` syntax for debugging, most of the time we will run them via plans executed by the [](#bluesky.run_engine.RunEngine). For instance we can read it using the [`bps.rd`](#bluesky.plan_stubs.rd) plan stub: + ```python +In [4]: RE(bps.rd(stage.x)) +Out[4]: RunEngineResult(run_start_uids=(), plan_result=0.0, exit_status='success', interrupted=False, reason='', exception=None) +``` + +and move it using the [`bps.mv`](#bluesky.plan_stubs.mv) plan sub: + ```python +In [5]: RE(bps.mv(stage.x, 1.5)) +Out[5]: RunEngineResult(run_start_uids=(), plan_result=(, done>,), exit_status='success', interrupted=False, reason='', exception=None) + +In [6]: RE(bps.rd(stage.x)) +Out[6]: RunEngineResult(run_start_uids=(), plan_result=1.5, exit_status='success', interrupted=False, reason='', exception=None) +``` + +There is also a point detector that changes its 3 channels of output based on the positions of the `stage.x` and `stage.y` motors, so we can use it in a [`bp.grid_scan`](#bluesky.plans.grid_scan): + +```{eval-rst} +.. ipython:: python + :suppress: + + from ophyd_async.sim.__main__ import * + +.. ipython:: python + + @savefig sim_grid_scan.png width=4in + RE(bp.grid_scan([pdet], stage.x, 1, 2, 3, stage.y, 2, 3, 3)) +``` + +This detector produces a single point of information for each channel at each motor value. This means that the [](inv:bluesky#best-effort-callback) is able to print a tabular form of the scan. + +There is also a blob detector that produces a gaussian blob with intensity based on the positions of the `stage.x` and `stage.y` motors, writing the data to an HDF file. You can also use this in a grid scan, but there will be no data displayed as the `BestEffortCallback` doesn't know how to read data from file: + +```{eval-rst} +.. ipython:: python + :okwarning: + + @savefig sim_grid_scan2.png width=4in + RE(bp.grid_scan([bdet], stage.x, 1, 2, 3, stage.y, 2, 3, 3)) +``` + +:::{seealso} +A more interactive scanning tutorial including live plotting of the data from file is in the process of being written in [the bluesky cookbook](https://github.com/bluesky/bluesky-cookbook/pull/22) +::: + +## Examine the script + +We will now walk through the script section by section and examine what each part does. First of all we import the bluesky and ophyd libraries: +```{literalinclude} ../../src/ophyd_async/sim/demo/__main__.py +:language: python +:start-after: Import bluesky and ophyd +:end-before: Create a run engine +``` + +After this we create a RunEngine: +```{literalinclude} ../../src/ophyd_async/sim/demo/__main__.py +:language: python +:start-after: Create a run engine +:end-before: Define where test data should be written +``` +We pass `call_returns_result=True` to the RunEngine so that we can see the result of `bps.rd` above. We call `autoawait_in_bluesky_event_loop()` so that when we `await bps.rd(x)` it will happen in the same event loop that the RunEngine uses rather than an IPython specific one. This avoids some surprising behaviour that occurs when devices are accessed from multiple event loops. + +Next up is the path provider: +```{literalinclude} ../../src/ophyd_async/sim/demo/__main__.py +:language: python +:start-after: Define where test data should be written +:end-before: All Devices created within this block +``` +This is how we specify in which location file-writing detectors store their data. In this example we choose to write to a static directory `/tmp` using the [](#StaticPathProvider), and to name each file within it with a unique UUID using the [](#UUIDFilenameProvider). [Other PathProviders](#PathProvider) allow this to be customized. + +Finally we create and connect the Devices: +```{literalinclude} ../../src/ophyd_async/sim/demo/__main__.py +:language: python +:start-after: connected and named at the end of the with block +``` +The first thing to note is the `with` statement. This uses a [](#init_devices) as a context manager to collect up the top level `Device` instances created in the context, and run the following: + +- If `set_name=True` (the default), then call [](#Device.set_name) passing the name of the variable within the context. For example, here we call + ``det.set_name("det")`` +- If ``connect=True`` (the default), then call [](#Device.connect) in parallel for all top level Devices, waiting for up to ``timeout`` seconds. For example, here we will connect `x`, `y` and `det` at the same time. This parallel connect speeds up connection to the underlying control system. +- If ``mock=True`` is passed, then don't connect to the control system, but set Devices into mock mode for testing. + +Within it the device creation happens, in this case the `x` and `y` motors and a `det` detector that gives different data depending on the position of the motors. + +## Conclusion + +In this tutorial we have instantiated some existing ophyd-async devices, seen how they can be connected and named, and used them in some basic plans. Read on to see how to implement support for devices via a control system like EPICS or Tango. diff --git a/docs/tutorials/using-existing-devices.rst b/docs/tutorials/using-existing-devices.rst deleted file mode 100644 index 77a031b39b..0000000000 --- a/docs/tutorials/using-existing-devices.rst +++ /dev/null @@ -1,184 +0,0 @@ -.. note:: - - Ophyd async is included on a provisional basis until the v1.0 release and - may change API on minor release numbers before then - -Using existing Devices -====================== - -To use an Ophyd Device that has already been written, you need to make a -RunEngine, then instantiate the Device in that process. This tutorial will take -you through this process. It assumes you have already run through the Bluesky -tutorial on `tutorial_run_engine_setup`. - -Create Startup file -------------------- - -For this tutorial we will use IPython. We will instantiate the RunEngine and -Devices in a startup file. This is just a regular Python file that IPython -will execute before giving us a prompt to execute scans. Copy the text -below and place it in an ``epics_demo.py`` file: - -.. literalinclude:: ../examples/epics_demo.py - :language: python - -The top section of the file is explained in the Bluesky tutorial, but the bottom -section is Ophyd specific. - -First of all we start up a specific EPICS IOC for the demo devices. This is only -used in this tutorial: - -.. literalinclude:: ../examples/epics_demo.py - :language: python - :start-after: # Start IOC - :end-before: # Create ophyd devices - -Next we create an example Ophyd device for comparison purposes. It is here to show -that you can mix Ophyd and Ophyd Async devices in the same RunEngine: - -.. literalinclude:: ../examples/epics_demo.py - :language: python - :start-after: # Create ophyd devices - :end-before: # Create ophyd-async devices - -Finally we create the Ophyd Async devices imported from the `epics.sim` module: - -.. literalinclude:: ../examples/epics_demo.py - :language: python - :start-after: # Create ophyd-async devices - -The first thing to note is `with`. This uses `init_devices` as a context -manager to collect up the top level `Device` instances created in the context, -and run the following: - -- If ``set_name=True`` (the default), then call `Device.set_name` passing the - name of the variable within the context. For example, here we call - ``det.set_name("det")`` -- If ``connect=True`` (the default), then call `Device.connect` in parallel for - all top level Devices, waiting for up to ``timeout`` seconds. For example, - here we call ``asyncio.wait([det.connect(), samp.connect()])`` -- If ``mock=True`` is passed, then don't connect to PVs, but set Devices into - simulation mode - -The Devices we create in this example are a "sample stage" with a couple of -"movers" called ``x`` and ``y`` and a "sensor" called ``det`` that gives a -different reading depending on the position of the "movers". - -.. note:: - - There are very few devices implemented using ophyd async, see ophyd_async.epics.devices - and ophyd-tango-devices for some common ones associated with each control - system - -Run IPython ------------ - -You can now run ipython with this startup file:: - - $ ipython -i epics_demo.py - IPython 8.5.0 -- An enhanced Interactive Python. Type '?' for help. - - In [1]: - -.. ipython:: python - :suppress: - :okexcept: - - import sys - from pathlib import Path - sys.path.append(str(Path(".").absolute()/"docs/examples")) - from epics_demo import * - # Turn off progressbar and table - RE.waiting_hook = None - bec.disable_table() - -This is like a regular python console with the contents of that file executed. -IPython adds some extra features like tab completion and magics (shortcut -commands). - -Run some plans --------------- - -Ophyd Devices give an interface to the `bluesky.run_engine.RunEngine` so they -can be used in plans. We can move the ``samp.x`` mover to 100mm using -`bluesky.plan_stubs.mv`: - -.. ipython:: - :okexcept: - - In [1]: RE(mov(samp.x, 100)) - -If this is too verbose to write, we registered a shorthand with -``bluesky.utils.register_transform``: `` str async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None: - """Assert a signal's value and compare it an expected signal. + """Assert that a Signal has the given value. Parameters ---------- signal: - signal with get_value. + Signal with get_value. value: The expected value from the signal. - - Notes - ----- - Example usage:: - await assert_value(signal, value) - """ actual_value = await signal.get_value() assert actual_value == value, _generate_assert_error_msg( @@ -53,37 +47,32 @@ async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None: def _approx_readable_value(reading: Mapping[str, Reading]) -> Mapping[str, Reading]: - """Change Reading value to pytest.approx(value)""" for i in reading: + # np_array1 == np_array2 gives an array of booleans rather than a single bool + # Use pytest.approx(np_array1) so that we get a bool instead reading[i]["value"] = pytest.approx(reading[i]["value"]) return reading async def assert_reading( - readable: AsyncReadable, expected_reading: Mapping[str, Reading] + readable: AsyncReadable, + reading: Mapping[str, Reading], ) -> None: - """Assert readings from readable. + """Assert that a readable Device has the given reading. Parameters ---------- readable: - Callable with readable.read function that generate readings. - + Device with an async ``read()`` method to get the reading from. reading: - The expected readings from the readable. - - Notes - ----- - Example usage:: - await assert_reading(readable, reading) - + The expected reading from the readable. """ actual_reading = await readable.read() assert ( - _approx_readable_value(expected_reading) == actual_reading + _approx_readable_value(reading) == actual_reading ), _generate_assert_error_msg( name=readable.name, - expected_result=expected_reading, + expected_result=reading, actual_result=actual_reading, ) @@ -92,21 +81,15 @@ async def assert_configuration( configurable: AsyncConfigurable, configuration: Mapping[str, Reading], ) -> None: - """Assert readings from Configurable. + """Assert that a configurable Device has the given configuration. Parameters ---------- configurable: - Configurable with Configurable.read function that generate readings. - + Device with an async ``read_configuration()`` method to get the configuration + from. configuration: - The expected readings from configurable. - - Notes - ----- - Example usage:: - await assert_configuration(configurable configuration) - + The expected configuration from the configurable. """ actual_configurable = await configurable.read_configuration() assert ( @@ -131,15 +114,15 @@ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int): Parameters ---------- - Doc: - A dictionary - + docs: + A mapping of document type -> list of documents that have been emitted. numbers: - expected emission in kwarg from + The number of each document type expected. + + Examples + -------- + .. code:: - Notes - ----- - Example usage:: docs = defaultdict(list) RE.subscribe(lambda name, doc: docs[name].append(doc)) RE(my_plan()) diff --git a/src/ophyd_async/testing/_mock_signal_utils.py b/src/ophyd_async/testing/_mock_signal_utils.py index 683666e6b4..e71fce7858 100644 --- a/src/ophyd_async/testing/_mock_signal_utils.py +++ b/src/ophyd_async/testing/_mock_signal_utils.py @@ -1,4 +1,4 @@ -from collections.abc import Awaitable, Callable, Iterable +from collections.abc import Awaitable, Callable, Iterable, Iterator from contextlib import contextmanager from unittest.mock import AsyncMock, Mock @@ -14,6 +14,10 @@ def get_mock(device: Device | Signal) -> Mock: + """Return the mock (which may have child mocks attached) for a Device. + + The device must have been connected in mock mode. + """ mock = device._mock # noqa: SLF001 assert isinstance(mock, LazyMock), f"Device {device} not connected in mock mode" return mock() @@ -34,36 +38,7 @@ def set_mock_value(signal: Signal[SignalDatatypeT], value: SignalDatatypeT): backend.set_value(value) -def set_mock_put_proceeds(signal: Signal, proceeds: bool): - """Allow or block a put with wait=True from proceeding""" - backend = _get_mock_signal_backend(signal) - - if proceeds: - backend.put_proceeds.set() - else: - backend.put_proceeds.clear() - - -@contextmanager -def mock_puts_blocked(*signals: Signal): - for signal in signals: - set_mock_put_proceeds(signal, False) - yield - for signal in signals: - set_mock_put_proceeds(signal, True) - - -def get_mock_put(signal: Signal) -> AsyncMock: - """Get the mock associated with the put call on the signal.""" - return _get_mock_signal_backend(signal).put_mock - - -def reset_mock_put_calls(signal: Signal): - backend = _get_mock_signal_backend(signal) - backend.put_mock.reset_mock() - - -class _SetValuesIterator: +class _SetValuesIterator(Iterator[SignalDatatypeT]): # Garbage collected by the time __del__ is called unless we put it as a # global attrbute here. require_all_consumed: bool = False @@ -78,13 +53,9 @@ def __init__( self.values = values self.require_all_consumed = require_all_consumed self.index = 0 - self.iterator = enumerate(values, start=1) - def __iter__(self): - return self - - def __next__(self): + def __next__(self) -> SignalDatatypeT: # Will propogate StopIteration self.index, next_value = next(self.iterator) set_mock_value(self.signal, next_value) @@ -113,33 +84,32 @@ def set_mock_values( signal: SignalR[SignalDatatypeT], values: Iterable[SignalDatatypeT], require_all_consumed: bool = False, -) -> _SetValuesIterator: +) -> Iterator[SignalDatatypeT]: """Iterator to set a signal to a sequence of values, optionally repeating the sequence. Parameters ---------- signal: - A signal with a `MockSignalBackend` backend. + A signal connected in mock mode. values: An iterable of the values to set the signal to, on each iteration - the value will be set. + the next value will be set. require_all_consumed: If True, an AssertionError will be raised if the iterator is deleted before all values have been consumed. - Notes - ----- - Example usage:: - - for value_set in set_mock_values(signal, [1, 2, 3]): - # do something + Examples + -------- + .. code:: - cm = set_mock_values(signal, 1, 2, 3, require_all_consumed=True): - next(cm) + for value_set in set_mock_values(signal, range(3)): # do something - """ + cm = set_mock_values(signal, [1, 3, 8], require_all_consumed=True): + next(cm) + # do something + """ return _SetValuesIterator( signal, values, @@ -173,3 +143,32 @@ def callback_on_mock_put( backend = _get_mock_signal_backend(signal) backend.put_mock.side_effect = callback return _unset_side_effect_cm(backend.put_mock) + + +def set_mock_put_proceeds(signal: Signal, proceeds: bool): + """Allow or block a put with wait=True from proceeding""" + backend = _get_mock_signal_backend(signal) + + if proceeds: + backend.put_proceeds.set() + else: + backend.put_proceeds.clear() + + +@contextmanager +def mock_puts_blocked(*signals: Signal): + for signal in signals: + set_mock_put_proceeds(signal, False) + yield + for signal in signals: + set_mock_put_proceeds(signal, True) + + +def get_mock_put(signal: Signal) -> AsyncMock: + """Get the mock associated with the put call on the signal.""" + return _get_mock_signal_backend(signal).put_mock + + +def reset_mock_put_calls(signal: Signal): + backend = _get_mock_signal_backend(signal) + backend.put_mock.reset_mock() diff --git a/tests/test_tutorials.py b/tests/test_tutorials.py index ca6eaf6e3b..bf62bb0d68 100644 --- a/tests/test_tutorials.py +++ b/tests/test_tutorials.py @@ -8,7 +8,7 @@ EXPECTED = """ +-----------+------------+-----------------------+-----------------------+----------------------+----------------------+----------------------+ -| seq_num | time | stage-x-user_readback | stage-y-user_readback | det1-channel-1-value | det1-channel-2-value | det1-channel-3-value | +| seq_num | time | stage-x-user_readback | stage-y-user_readback | pdet-channel-1-value | pdet-channel-2-value | pdet-channel-3-value | +-----------+------------+-----------------------+-----------------------+----------------------+----------------------+----------------------+ | 1 | 10:41:18.8 | 1.000 | 1.000 | 711 | 678 | 650 | | 2 | 10:41:18.9 | 1.000 | 1.500 | 831 | 797 | 769 | @@ -47,7 +47,7 @@ def test_implementing_devices(module, capsys, expected_scan_output): for motor in [main.stage.x, main.stage.y]: RE(main.bps.mv(motor.velocity, 1000)) start = time.monotonic() - RE(main.bp.grid_scan([main.det1], main.stage.x, 1, 2, 3, main.stage.y, 1, 2, 3)) + RE(main.bp.grid_scan([main.pdet], main.stage.x, 1, 2, 3, main.stage.y, 1, 2, 3)) assert time.monotonic() - start == pytest.approx(2.0, abs=1.0) captured = capsys.readouterr() assert captured.err == ""