From e4d16b7b61751d3ea7983425df297db2f19ba31b Mon Sep 17 00:00:00 2001 From: Rose Yemelyanova <90774497+RAYemelyanova@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:42:09 +0100 Subject: [PATCH] Restructure code base and ensure tests still pass (#5) * Restructure code base and ensure tests still pass * Move epicsdemo into ophyd_async.epics * Prefix some modules with _ and clearly define public API in __init__ files * Temporarily remove support for python 3.8 due to cython issue in p4p * Fix docs CI job * Remove python 3.8 check in init.py * Add extra tests to pass patch * Fix linting * Increasing tolerance for patch changes due to codecov flakiness r.e. moving files * Rename to ophyd-async --- .codecov.yml | 2 +- .devcontainer/Dockerfile | 4 +- .github/workflows/code.yml | 4 +- .github/workflows/docs.yml | 2 +- .gitignore | 2 +- docs/conf.py | 14 +- .../0002-switched-to-pip-skeleton.rst | 6 +- .../decisions/0004-repository-structure.rst | 55 +++++-- docs/developer/how-to/pin-requirements.rst | 2 +- docs/developer/tutorials/dev-install.rst | 2 +- docs/examples/ad_demo.py | 6 +- docs/index.rst | 4 +- docs/user/examples/epics_demo.py | 10 +- docs/user/how-to/make-a-simple-device.rst | 10 +- docs/user/reference/api.rst | 14 +- docs/user/tutorials/installation.rst | 6 +- .../user/tutorials/using-existing-devices.rst | 6 +- other_licenses/pyepics | 84 ----------- pyproject.toml | 7 +- src/ophyd_async/__init__.py | 9 +- src/ophyd_async/core/__init__.py | 46 ++---- .../{devices => core/_device}/__init__.py | 0 .../core/_device/_backend/__init__.py | 0 .../_backend}/signal_backend.py | 2 +- .../_backend/sim_signal_backend.py} | 2 +- .../core/_device/_signal/__init__.py | 0 .../{signals => _device/_signal}/signal.py | 10 +- .../core/{devices => _device}/device.py | 2 + .../core/{ => _device}/device_collector.py | 5 +- .../{devices => _device}/device_vector.py | 2 + .../{devices => _device}/standard_readable.py | 8 +- src/ophyd_async/core/async_status.py | 2 + src/ophyd_async/core/backends/__init__.py | 4 - src/ophyd_async/core/devices/__init__.py | 12 -- src/ophyd_async/core/signals/__init__.py | 39 ----- src/ophyd_async/epics/__init__.py | 0 src/ophyd_async/epics/_backend/__init__.py | 0 .../backends => epics/_backend}/_aioca.py | 4 +- .../{core/backends => epics/_backend}/_p4p.py | 4 +- .../epics/areadetector/__init__.py | 22 +++ .../epics/areadetector/ad_driver.py | 18 +++ .../epics/areadetector/directory_provider.py | 18 +++ .../areadetector/hdf_streamer_det.py} | 142 ++---------------- .../epics/areadetector/nd_file_hdf.py | 22 +++ .../epics/areadetector/nd_plugin.py | 13 ++ .../epics/areadetector/single_trigger_det.py | 41 +++++ src/ophyd_async/epics/areadetector/utils.py | 26 ++++ .../epicsdemo => epics/demo}/__init__.py | 24 ++- .../{core/epicsdemo => epics/demo}/mover.db | 0 .../{core/epicsdemo => epics/demo}/sensor.db | 0 src/ophyd_async/epics/motion/__init__.py | 3 + .../{devices => epics/motion}/motor.py | 10 +- src/ophyd_async/epics/signal/__init__.py | 10 ++ .../epics/signal/_epics_transport.py | 31 ++++ src/ophyd_async/epics/signal/pvi_get.py | 22 +++ .../epics.py => epics/signal/signal.py} | 41 ++--- src/ophyd_async/panda/__init__.py | 21 +++ src/ophyd_async/{devices => panda}/panda.py | 51 ++++--- tests/conftest.py | 10 +- .../_backend}/test_sim.py | 4 +- .../_signal}/test_signal.py | 4 +- .../core/{devices => _device}/test_device.py | 10 +- tests/core/_device/test_device_collector.py | 14 ++ tests/core/test_async_status.py | 9 +- tests/epics/areadetector/__init__.py | 0 .../areadetector/test_hdf_streamer_det.py} | 70 ++------- .../areadetector/test_single_trigger_det.py | 66 ++++++++ tests/epics/motion/__init__.py | 0 tests/{devices => epics/motion}/test_motor.py | 5 +- .../test_epicsdemo.py => epics/test_demo.py} | 34 ++--- tests/{core/signals => epics}/test_records.db | 0 .../test_epics.py => epics/test_signals.py} | 3 +- tests/{devices => panda}/db/panda.db | 0 tests/{devices => panda}/test_panda.py | 6 +- 74 files changed, 588 insertions(+), 553 deletions(-) delete mode 100644 other_licenses/pyepics rename src/ophyd_async/{devices => core/_device}/__init__.py (100%) create mode 100644 src/ophyd_async/core/_device/_backend/__init__.py rename src/ophyd_async/core/{backends => _device/_backend}/signal_backend.py (96%) rename src/ophyd_async/core/{backends/sim.py => _device/_backend/sim_signal_backend.py} (98%) create mode 100644 src/ophyd_async/core/_device/_signal/__init__.py rename src/ophyd_async/core/{signals => _device/_signal}/signal.py (97%) rename src/ophyd_async/core/{devices => _device}/device.py (99%) rename src/ophyd_async/core/{ => _device}/device_collector.py (97%) rename src/ophyd_async/core/{devices => _device}/device_vector.py (87%) rename src/ophyd_async/core/{devices => _device}/standard_readable.py (90%) delete mode 100644 src/ophyd_async/core/backends/__init__.py delete mode 100644 src/ophyd_async/core/devices/__init__.py delete mode 100644 src/ophyd_async/core/signals/__init__.py create mode 100644 src/ophyd_async/epics/__init__.py create mode 100644 src/ophyd_async/epics/_backend/__init__.py rename src/ophyd_async/{core/backends => epics/_backend}/_aioca.py (99%) rename src/ophyd_async/{core/backends => epics/_backend}/_p4p.py (99%) create mode 100644 src/ophyd_async/epics/areadetector/__init__.py create mode 100644 src/ophyd_async/epics/areadetector/ad_driver.py create mode 100644 src/ophyd_async/epics/areadetector/directory_provider.py rename src/ophyd_async/{devices/areadetector.py => epics/areadetector/hdf_streamer_det.py} (59%) create mode 100644 src/ophyd_async/epics/areadetector/nd_file_hdf.py create mode 100644 src/ophyd_async/epics/areadetector/nd_plugin.py create mode 100644 src/ophyd_async/epics/areadetector/single_trigger_det.py create mode 100644 src/ophyd_async/epics/areadetector/utils.py rename src/ophyd_async/{core/epicsdemo => epics/demo}/__init__.py (94%) rename src/ophyd_async/{core/epicsdemo => epics/demo}/mover.db (100%) rename src/ophyd_async/{core/epicsdemo => epics/demo}/sensor.db (100%) create mode 100644 src/ophyd_async/epics/motion/__init__.py rename src/ophyd_async/{devices => epics/motion}/motor.py (93%) create mode 100644 src/ophyd_async/epics/signal/__init__.py create mode 100644 src/ophyd_async/epics/signal/_epics_transport.py create mode 100644 src/ophyd_async/epics/signal/pvi_get.py rename src/ophyd_async/{core/signals/epics.py => epics/signal/signal.py} (72%) create mode 100644 src/ophyd_async/panda/__init__.py rename src/ophyd_async/{devices => panda}/panda.py (88%) rename tests/core/{backends => _device/_backend}/test_sim.py (96%) rename tests/core/{signals => _device/_signal}/test_signal.py (97%) rename tests/core/{devices => _device}/test_device.py (93%) create mode 100644 tests/core/_device/test_device_collector.py create mode 100644 tests/epics/areadetector/__init__.py rename tests/{devices/test_area_detector.py => epics/areadetector/test_hdf_streamer_det.py} (72%) create mode 100644 tests/epics/areadetector/test_single_trigger_det.py create mode 100644 tests/epics/motion/__init__.py rename tests/{devices => epics/motion}/test_motor.py (95%) rename tests/{core/test_epicsdemo.py => epics/test_demo.py} (85%) rename tests/{core/signals => epics}/test_records.db (100%) rename tests/{core/signals/test_epics.py => epics/test_signals.py} (98%) rename tests/{devices => panda}/db/panda.db (100%) rename tests/{devices => panda}/test_panda.py (94%) diff --git a/.codecov.yml b/.codecov.yml index 9a442b1b8c..68eb55579e 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -9,6 +9,6 @@ coverage: patch: default: target: auto - threshold: 0.1% + threshold: 1% github_checks: annotations: false diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 7b70d5bdc9..349a45cc6f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,7 +3,7 @@ # The devcontainer should use the build target and run as root with podman # or docker with user namespaces. # -FROM python:3.8 as build +FROM python:3.9 as build ARG PIP_OPTIONS @@ -24,7 +24,7 @@ WORKDIR /context # install python package into /venv RUN pip install ${PIP_OPTIONS} -FROM python:3.8-slim as runtime +FROM python:3.9-slim as runtime # Add apt-get system dependecies for runtime here if needed diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 33dd22da8c..77dd8f9132 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -29,12 +29,12 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest"] # can add windows-latest, macos-latest - python: ["3.9", "3.10", "3.11"] + python: ["3.10", "3.11"] install: ["-e .[dev]"] # Make one version be non-editable to test both paths of version code include: - os: "ubuntu-latest" - python: "3.8" + python: "3.9" install: ".[dev]" runs-on: ${{ matrix.os }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c510d577a9..a501f7d030 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -46,7 +46,7 @@ jobs: if: github.event_name == 'push' && github.actor != 'dependabot[bot]' # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions - uses: peaceiris/actions-gh-pages@64b46b4226a4a12da2239ba3ea5aa73e3163c75b # v3.9.1 + uses: peaceiris/actions-gh-pages@373f7f263a76c20808c831209c920827a82a2847 # v3.9.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .github/pages diff --git a/.gitignore b/.gitignore index 409a0717a5..1ce3f7a583 100644 --- a/.gitignore +++ b/.gitignore @@ -84,4 +84,4 @@ docs/*/generated docs/savefig # generated version number -ophyd/_version.py +ophyd_async/_version.py diff --git a/docs/conf.py b/docs/conf.py index 57f5366ead..7327ea2f72 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,6 +4,7 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import os import sys from pathlib import Path from subprocess import check_output @@ -13,9 +14,11 @@ import ophyd_async # -- General configuration ------------------------------------------------ +# Source code dir relative to this file +sys.path.insert(0, os.path.abspath("../../src")) # General information about the project. -project = "ophyd_async" +project = "ophyd-async" copyright = "2014, Brookhaven National Lab" # The full version, including alpha/beta/rc tags. @@ -208,14 +211,17 @@ # every member listed in __all__ and no others. Default is True autosummary_ignore_module_all = False +# Turn on sphinx.ext.autosummary +autosummary_generate = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + # Look for signatures in the first line of the docstring (used for C functions) autodoc_docstring_signature = True # numpydoc config numpydoc_show_class_members = False -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - # Where to put Ipython savefigs ipython_savefig_dir = "../build/savefig" diff --git a/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst b/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst index a00833145a..d0cd738402 100644 --- a/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst +++ b/docs/developer/explanations/decisions/0002-switched-to-pip-skeleton.rst @@ -1,4 +1,4 @@ -2. Adopt ophyd_async for project structure +2. Adopt ophyd-async for project structure ========================================== Date: 2022-02-18 @@ -11,7 +11,7 @@ Accepted Context ------- -We should use the following `pip-skeleton `_. +We should use the following `pip-skeleton `_. The skeleton will ensure consistency in developer environments and package management. @@ -23,7 +23,7 @@ We have switched to using the skeleton. Consequences ------------ -This module will use a fixed set of tools as developed in ophyd_async +This module will use a fixed set of tools as developed in ophyd-async and can pull from this skeleton to update the packaging to the latest techniques. As such, the developer environment may have changed, the following could be diff --git a/docs/developer/explanations/decisions/0004-repository-structure.rst b/docs/developer/explanations/decisions/0004-repository-structure.rst index 31d5c572d0..411cf858d3 100644 --- a/docs/developer/explanations/decisions/0004-repository-structure.rst +++ b/docs/developer/explanations/decisions/0004-repository-structure.rst @@ -1,7 +1,13 @@ +.. role:: bash(code) + :language: bash + +.. role:: python(code) + :language: python + 4. Repository Structure ======================= -Date: 2023-08-30 +Date: 2023-09-07 Status ------ @@ -53,29 +59,48 @@ During this process, the folder structure should incrementally be changed to │ └── ophyd_async │ ├── core │ │ ├── __init__.py - │ │ ├── backends + │ │ ├── _device │ │ │ ├── __init__.py - │ │ │ ├── _aioca.py - │ │ │ └── _p4p.py - │ │ ├── devices - │ │ ├── signals - │ │ ├── epicsdemo + │ │ │ ├── _backend + │ │ │ │ ├── __init__.py + │ │ │ │ ├── signal_backend.py + │ │ │ │ └── sim.py + │ │ │ ├── _signal + │ │ │ │ ├── __init__.py + │ │ │ │ └── signal.py + │ │ │ ├── device_collector.py + │ │ │ ├── device_vector.py + │ │ │ └── ... │ │ ├── async_status.py - │ │ ├── device_collector.py │ │ └── utils.py - │ └── devices - │ ├── epics - │ └── tango + │ ├── epics + │ │ ├── _backend + │ │ │ ├── __init__.py + │ │ │ ├── _p4p.py + │ │ │ └── _aioca.py + │ │ ├── areadetector + │ │ │ ├── __init__.py + │ │ │ ├── ad_driver.py + │ │ │ └── ... + │ │ ├── signal + │ │ │ └── ... + │ │ ├── motion + │ │ │ ├── __init__.py + │ │ │ └── motor.py + │ │ └── demo + │ │ └── ... + │ └── panda + │ └── ... ├── tests │ ├── core │ │ └── ... - │ └── devices + │ └── epics └── ... -The `__init__.py` files of each submodule (core, devices.epics and devices.tango) will +The :bash:`__init__.py` files of each submodule (core, epics, panda and eventually tango) will be modified such that end users experience little disruption to how they use Ophyd Async. -For such users, `from ophyd.v2.core import ...` can be replaced with -`from ophyd_async.core import ...`. +For such users, :python:`from ophyd.v2.core import ...` can be replaced with +:python:`from ophyd_async.core import ...`. Consequences diff --git a/docs/developer/how-to/pin-requirements.rst b/docs/developer/how-to/pin-requirements.rst index 97784060c4..e42226fbcf 100644 --- a/docs/developer/how-to/pin-requirements.rst +++ b/docs/developer/how-to/pin-requirements.rst @@ -46,7 +46,7 @@ of the dependencies and sub-dependencies with pinned versions. You can download any of these files by clicking on them. It is best to use the one that ran with the lowest Python version as this is more likely to be compatible with all the versions of Python in the test matrix. -i.e. ``requirements-test-ubuntu-latest-3.8.txt`` in this example. +i.e. ``requirements-test-ubuntu-latest-3.9.txt`` in this example. Applying the lock file ---------------------- diff --git a/docs/developer/tutorials/dev-install.rst b/docs/developer/tutorials/dev-install.rst index 13e029d5f4..59f16a919b 100644 --- a/docs/developer/tutorials/dev-install.rst +++ b/docs/developer/tutorials/dev-install.rst @@ -16,7 +16,7 @@ Install dependencies -------------------- You can choose to either develop on the host machine using a `venv` (which -requires python 3.8 or later) or to run in a container under `VSCode +requires python 3.9 or later) or to run in a container under `VSCode `_ .. tab-set:: diff --git a/docs/examples/ad_demo.py b/docs/examples/ad_demo.py index f11790c495..c4f0a709dc 100644 --- a/docs/examples/ad_demo.py +++ b/docs/examples/ad_demo.py @@ -9,7 +9,7 @@ from bluesky.utils import ProgressBarManager, register_transform from ophyd.v2.core import DeviceCollector -from ophyd_async.devices import areadetector +from ophyd_async.epics import areadetector # Create a run engine, with plotting, progressbar and transform RE = RunEngine({}, call_returns_result=True) @@ -25,9 +25,7 @@ # Create v2 devices with DeviceCollector(): - det1 = areadetector.MySingleTriggerSim(pv_prefix) - det2 = areadetector.MyHDFWritingSim(pv_prefix) - det3 = areadetector.MyHDFFlyerSim(pv_prefix) + det3 = areadetector.HDFStreamerDet(pv_prefix) # And a plan diff --git a/docs/index.rst b/docs/index.rst index 72b1c07974..054db2b377 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,13 +14,13 @@ The documentation is split into 2 sections: :link: user/index :link-type: doc - The User Guide contains documentation on how to install and use ophyd_async. + The User Guide contains documentation on how to install and use ophyd-async. .. grid-item-card:: :material-regular:`code;4em` :link: developer/index :link-type: doc - The Developer Guide contains documentation on how to develop and contribute changes back to ophyd_async. + The Developer Guide contains documentation on how to develop and contribute changes back to ophyd-async. .. toctree:: :hidden: diff --git a/docs/user/examples/epics_demo.py b/docs/user/examples/epics_demo.py index ebc1b4b4ee..862377e73d 100644 --- a/docs/user/examples/epics_demo.py +++ b/docs/user/examples/epics_demo.py @@ -7,8 +7,8 @@ from bluesky.utils import ProgressBarManager, register_transform from ophyd import Component, Device, EpicsSignal, EpicsSignalRO -from ophyd_async.core import epicsdemo -from ophyd_async.core.device_collector import DeviceCollector +from ophyd_async.core import DeviceCollector +from ophyd_async.epics import demo # Create a run engine, with plotting, progressbar and transform RE = RunEngine({}, call_returns_result=True) @@ -19,7 +19,7 @@ register_transform("RE", prefix="<") # Start IOC with demo pvs in subprocess -pv_prefix = epicsdemo.start_ioc_subprocess() +pv_prefix = demo.start_ioc_subprocess() # Create ophyd devices @@ -32,5 +32,5 @@ class OldSensor(Device): # Create ophyd-async devices with DeviceCollector(): - det = epicsdemo.Sensor(pv_prefix) - samp = epicsdemo.SampleStage(pv_prefix) + det = demo.Sensor(pv_prefix) + samp = demo.SampleStage(pv_prefix) diff --git a/docs/user/how-to/make-a-simple-device.rst b/docs/user/how-to/make-a-simple-device.rst index 4541eb6c75..86b112d307 100644 --- a/docs/user/how-to/make-a-simple-device.rst +++ b/docs/user/how-to/make-a-simple-device.rst @@ -6,14 +6,14 @@ Make a Simple Device ==================== -.. currentmodule:: ophyd_async.core.core +.. currentmodule:: ophyd_async.core To make a simple device, you need to subclass from the `StandardReadable` class, create some `Signal` instances, and optionally implement other suitable Bluesky `Protocols ` like :class:`~bluesky.protocols.Movable`. -The rest of this guide will show examples from ``src/ophyd_async/core/epicsdemo/__init__.py`` +The rest of this guide will show examples from ``src/ophyd_async/epics/demo/__init__.py`` Readable -------- @@ -22,7 +22,7 @@ For a simple :class:`~bluesky.protocols.Readable` object like a `Sensor`, you ne define some signals, then tell the superclass which signals should contribute to ``read()`` and ``read_configuration()``: -.. literalinclude:: ../../../src/ophyd_async/core/epicsdemo/__init__.py +.. literalinclude:: ../../../src/ophyd_async/epics/demo/__init__.py :pyobject: Sensor First some Signals are constructed and stored on the Device. Each one is passed @@ -53,7 +53,7 @@ Movable For a more complicated device like a `Mover`, you can still use `StandardReadable` and implement some addition protocols: -.. literalinclude:: ../../../src/ophyd_async/core/epicsdemo/__init__.py +.. literalinclude:: ../../../src/ophyd_async/epics/demo/__init__.py :pyobject: Mover The ``set()`` method implements :class:`~bluesky.protocols.Movable`. This @@ -70,7 +70,7 @@ Assembly Compound assemblies can be used to group Devices into larger logical Devices: -.. literalinclude:: ../../../src/ophyd_async/core/epicsdemo/__init__.py +.. literalinclude:: ../../../src/ophyd_async/epics/demo/__init__.py :pyobject: SampleStage This applies prefixes on construction: diff --git a/docs/user/reference/api.rst b/docs/user/reference/api.rst index 60a3dfaf4a..362154c8d1 100644 --- a/docs/user/reference/api.rst +++ b/docs/user/reference/api.rst @@ -6,9 +6,9 @@ API === -.. automodule:: ophyd_async.core +.. automodule:: ophyd_async - ``ophyd_async.core`` + ``ophyd_async`` ----------------------------------- This is the internal API reference for ophyd_async @@ -24,10 +24,6 @@ This is the internal API reference for ophyd_async :template: custom-module-template.rst :recursive: - ophyd_async.core.backends - ophyd_async.core.devices - ophyd_async.core.epicsdemo - ophyd_async.core.signals - ophyd_async.core.async_status - ophyd_async.core.device_collector - ophyd_async.core.utils + core + epics + panda diff --git a/docs/user/tutorials/installation.rst b/docs/user/tutorials/installation.rst index beb9aa2677..9e310adbce 100644 --- a/docs/user/tutorials/installation.rst +++ b/docs/user/tutorials/installation.rst @@ -9,7 +9,7 @@ Installation Check your version of python ---------------------------- -You will need python 3.8 or later. You can check your version of python by +You will need python 3.9 or later. You can check your version of python by typing into a terminal:: $ python3 --version @@ -30,7 +30,7 @@ Installing the library You can now use ``pip`` to install the library and its dependencies:: - $ python3 -m pip install ophyd_async + $ python3 -m pip install ophyd-async If you require a feature that is not currently released you can also install from github:: @@ -40,4 +40,4 @@ from github:: 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 + $ ophyd-async --version diff --git a/docs/user/tutorials/using-existing-devices.rst b/docs/user/tutorials/using-existing-devices.rst index db76e3d577..067d01ca99 100644 --- a/docs/user/tutorials/using-existing-devices.rst +++ b/docs/user/tutorials/using-existing-devices.rst @@ -41,7 +41,7 @@ that you can mix Ophyd and Ophyd Async devices in the same RunEngine: :start-after: # Create ophyd devices :end-before: # Create ophyd-async devices -Finally we create the Ophyd Async devices imported from the `epicsdemo` module: +Finally we create the Ophyd Async devices imported from the `epics.demo` module: .. literalinclude:: ../examples/epics_demo.py :language: python @@ -154,7 +154,7 @@ There is also an "energy mode" that can be changed to modify the ``det`` output. In [1]: - CARS, University of Chicago - -There have been several contributions from many others, notably Angus -Gratton . See the Acknowledgements section of -the documentation for a list of more contributors. - -Except where explicitly noted, all files in this distribution are licensed -under the Epics Open License.: - ------------------------------------------------- - -Copyright 2010 Matthew Newville, The University of Chicago. All rights reserved. - -The epics python module is distributed subject to the following license conditions: -SOFTWARE LICENSE AGREEMENT -Software: epics python module - - 1. The "Software", below, refers to the epics python module (in either - source code, or binary form and accompanying documentation). Each - licensee is addressed as "you" or "Licensee." - - 2. The copyright holders shown above and their third-party licensors - hereby grant Licensee a royalty-free nonexclusive license, subject to - the limitations stated herein and U.S. Government license rights. - - 3. You may modify and make a copy or copies of the Software for use - within your organization, if you meet the following conditions: - - 1. Copies in source code must include the copyright notice and this - Software License Agreement. - - 2. Copies in binary form must include the copyright notice and this - Software License Agreement in the documentation and/or other - materials provided with the copy. - - 4. You may modify a copy or copies of the Software or any portion of - it, thus forming a work based on the Software, and distribute copies of - such work outside your organization, if you meet all of the following - conditions: - - 1. Copies in source code must include the copyright notice and this - Software License Agreement; - - 2. Copies in binary form must include the copyright notice and this - Software License Agreement in the documentation and/or other - materials provided with the copy; - - 3. Modified copies and works based on the Software must carry - prominent notices stating that you changed specified portions of - the Software. - - 5. Portions of the Software resulted from work developed under a - U.S. Government contract and are subject to the following license: the - Government is granted for itself and others acting on its behalf a - paid-up, nonexclusive, irrevocable worldwide license in this computer - software to reproduce, prepare derivative works, and perform publicly - and display publicly. - - 6. WARRANTY DISCLAIMER. THE SOFTWARE IS SUPPLIED "AS IS" WITHOUT - WARRANTY OF ANY KIND. THE COPYRIGHT HOLDERS, THEIR THIRD PARTY - LICENSORS, THE UNITED STATES, THE UNITED STATES DEPARTMENT OF ENERGY, - AND THEIR EMPLOYEES: (1) DISCLAIM ANY WARRANTIES, EXPRESS OR IMPLIED, - INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE, TITLE OR NON-INFRINGEMENT, (2) DO NOT - ASSUME ANY LEGAL LIABILITY OR RESPONSIBILITY FOR THE ACCURACY, - COMPLETENESS, OR USEFULNESS OF THE SOFTWARE, (3) DO NOT REPRESENT THAT - USE OF THE SOFTWARE WOULD NOT INFRINGE PRIVATELY OWNED RIGHTS, (4) DO - NOT WARRANT THAT THE SOFTWARE WILL FUNCTION UNINTERRUPTED, THAT IT IS - ERROR-FREE OR THAT ANY ERRORS WILL BE CORRECTED. - - 7. LIMITATION OF LIABILITY. IN NO EVENT WILL THE COPYRIGHT HOLDERS, - THEIR THIRD PARTY LICENSORS, THE UNITED STATES, THE UNITED STATES - DEPARTMENT OF ENERGY, OR THEIR EMPLOYEES: BE LIABLE FOR ANY INDIRECT, - INCIDENTAL, CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES OF ANY KIND OR - NATURE, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS OR LOSS OF DATA, - FOR ANY REASON WHATSOEVER, WHETHER SUCH LIABILITY IS ASSERTED ON THE - BASIS OF CONTRACT, TORT (INCLUDING NEGLIGENCE OR STRICT LIABILITY), OR - OTHERWISE, EVEN IF ANY OF SAID PARTIES HAS BEEN WARNED OF THE - POSSIBILITY OF SUCH LOSS OR DAMAGES. - ------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index d921d02291..d65a1f2a15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,11 +3,10 @@ requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2", "wheel"] build-backend = "setuptools.build_meta" [project] -name = "ophyd_async" +name = "ophyd-async" classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: BSD License", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -27,7 +26,7 @@ dependencies = [ dynamic = ["version"] license.file = "LICENSE" readme = "README.rst" -requires-python = ">=3.8" +requires-python = ">=3.9" [project.optional-dependencies] ca = ["aioca>=1.6"] @@ -74,7 +73,7 @@ dev = [ ] [project.scripts] -ophyd_async = "ophyd_async.__main__:main" +ophyd-async = "ophyd_async.__main__:main" [project.urls] GitHub = "https://github.com/bluesky/ophyd-async" diff --git a/src/ophyd_async/__init__.py b/src/ophyd_async/__init__.py index 5171b1dcda..4100754a36 100644 --- a/src/ophyd_async/__init__.py +++ b/src/ophyd_async/__init__.py @@ -1,11 +1,6 @@ -import sys +from importlib.metadata import version # noqa -if sys.version_info < (3, 8): - from importlib_metadata import version # noqa -else: - from importlib.metadata import version # noqa - -__version__ = version("ophyd_async") +__version__ = version("ophyd-async") del version __all__ = ["__version__"] diff --git a/src/ophyd_async/core/__init__.py b/src/ophyd_async/core/__init__.py index a6b6c95615..f74eba7692 100644 --- a/src/ophyd_async/core/__init__.py +++ b/src/ophyd_async/core/__init__.py @@ -1,25 +1,11 @@ -from .async_status import AsyncStatus -from .backends import SignalBackend, SimSignalBackend -from .device_collector import DeviceCollector -from .devices import ( - Device, - DeviceVector, - StandardReadable, - connect_children, - get_device_children, - name_children, -) -from .signals import ( - EpicsTransport, +from ._device._backend.signal_backend import SignalBackend +from ._device._backend.sim_signal_backend import SimSignalBackend +from ._device._signal.signal import ( Signal, SignalR, SignalRW, SignalW, SignalX, - epics_signal_r, - epics_signal_rw, - epics_signal_w, - epics_signal_x, observe_value, set_and_wait_for_value, set_sim_callback, @@ -27,6 +13,11 @@ set_sim_value, wait_for_value, ) +from ._device.device import Device, connect_children, get_device_children, name_children +from ._device.device_collector import DeviceCollector +from ._device.device_vector import DeviceVector +from ._device.standard_readable import StandardReadable +from .async_status import AsyncStatus from .utils import ( DEFAULT_TIMEOUT, Callback, @@ -40,32 +31,27 @@ ) __all__ = [ - "AsyncStatus", "SignalBackend", "SimSignalBackend", - "DeviceCollector", - "Device", - "DeviceVector", - "StandardReadable", - "connect_children", - "get_device_children", - "name_children", - "EpicsTransport", "Signal", "SignalR", "SignalW", "SignalRW", "SignalX", - "epics_signal_r", - "epics_signal_w", - "epics_signal_rw", - "epics_signal_x", "observe_value", "set_and_wait_for_value", "set_sim_callback", "set_sim_put_proceeds", "set_sim_value", "wait_for_value", + "Device", + "connect_children", + "get_device_children", + "name_children", + "DeviceCollector", + "DeviceVector", + "StandardReadable", + "AsyncStatus", "DEFAULT_TIMEOUT", "Callback", "NotConnected", diff --git a/src/ophyd_async/devices/__init__.py b/src/ophyd_async/core/_device/__init__.py similarity index 100% rename from src/ophyd_async/devices/__init__.py rename to src/ophyd_async/core/_device/__init__.py diff --git a/src/ophyd_async/core/_device/_backend/__init__.py b/src/ophyd_async/core/_device/_backend/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ophyd_async/core/backends/signal_backend.py b/src/ophyd_async/core/_device/_backend/signal_backend.py similarity index 96% rename from src/ophyd_async/core/backends/signal_backend.py rename to src/ophyd_async/core/_device/_backend/signal_backend.py index 4413d2debb..e0722ced77 100644 --- a/src/ophyd_async/core/backends/signal_backend.py +++ b/src/ophyd_async/core/_device/_backend/signal_backend.py @@ -3,7 +3,7 @@ from bluesky.protocols import Descriptor, Reading -from ..utils import ReadingValueCallback, T +from ...utils import ReadingValueCallback, T class SignalBackend(Generic[T]): diff --git a/src/ophyd_async/core/backends/sim.py b/src/ophyd_async/core/_device/_backend/sim_signal_backend.py similarity index 98% rename from src/ophyd_async/core/backends/sim.py rename to src/ophyd_async/core/_device/_backend/sim_signal_backend.py index 2454497b93..2d29363c29 100644 --- a/src/ophyd_async/core/backends/sim.py +++ b/src/ophyd_async/core/_device/_backend/sim_signal_backend.py @@ -11,7 +11,7 @@ from bluesky.protocols import Descriptor, Dtype, Reading -from ..utils import ReadingValueCallback, T, get_dtype +from ...utils import ReadingValueCallback, T, get_dtype from .signal_backend import SignalBackend primitive_dtypes: Dict[type, Dtype] = { diff --git a/src/ophyd_async/core/_device/_signal/__init__.py b/src/ophyd_async/core/_device/_signal/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ophyd_async/core/signals/signal.py b/src/ophyd_async/core/_device/_signal/signal.py similarity index 97% rename from src/ophyd_async/core/signals/signal.py rename to src/ophyd_async/core/_device/_signal/signal.py index d97f871047..1b5d4b03d3 100644 --- a/src/ophyd_async/core/signals/signal.py +++ b/src/ophyd_async/core/_device/_signal/signal.py @@ -13,11 +13,11 @@ Subscribable, ) -from ..async_status import AsyncStatus -from ..backends.signal_backend import SignalBackend -from ..backends.sim import SimSignalBackend -from ..devices import Device -from ..utils import DEFAULT_TIMEOUT, Callback, ReadingValueCallback, T +from ...async_status import AsyncStatus +from ...utils import DEFAULT_TIMEOUT, Callback, ReadingValueCallback, T +from .._backend.signal_backend import SignalBackend +from .._backend.sim_signal_backend import SimSignalBackend +from ..device import Device _sim_backends: Dict[Signal, SimSignalBackend] = {} diff --git a/src/ophyd_async/core/devices/device.py b/src/ophyd_async/core/_device/device.py similarity index 99% rename from src/ophyd_async/core/devices/device.py rename to src/ophyd_async/core/_device/device.py index e47f4c9bbc..0595a2e673 100644 --- a/src/ophyd_async/core/devices/device.py +++ b/src/ophyd_async/core/_device/device.py @@ -1,3 +1,5 @@ +"""Base device""" + from typing import Generator, Optional, Tuple from bluesky.protocols import HasName diff --git a/src/ophyd_async/core/device_collector.py b/src/ophyd_async/core/_device/device_collector.py similarity index 97% rename from src/ophyd_async/core/device_collector.py rename to src/ophyd_async/core/_device/device_collector.py index 2cf1a319aa..736a21635e 100644 --- a/src/ophyd_async/core/device_collector.py +++ b/src/ophyd_async/core/_device/device_collector.py @@ -1,3 +1,4 @@ +"""Interface for connecting and naming multiple devices""" import asyncio import logging import sys @@ -6,8 +7,8 @@ from bluesky.run_engine import call_in_bluesky_event_loop -from .devices import Device -from .utils import NotConnected +from ..utils import NotConnected +from .device import Device class DeviceCollector: diff --git a/src/ophyd_async/core/devices/device_vector.py b/src/ophyd_async/core/_device/device_vector.py similarity index 87% rename from src/ophyd_async/core/devices/device_vector.py rename to src/ophyd_async/core/_device/device_vector.py index 027280a886..077e51d20f 100644 --- a/src/ophyd_async/core/devices/device_vector.py +++ b/src/ophyd_async/core/_device/device_vector.py @@ -1,3 +1,5 @@ +"""Dictionary which can contain mappings between integers and devices.""" + from typing import Dict, TypeVar from ..utils import wait_for_connection diff --git a/src/ophyd_async/core/devices/standard_readable.py b/src/ophyd_async/core/_device/standard_readable.py similarity index 90% rename from src/ophyd_async/core/devices/standard_readable.py rename to src/ophyd_async/core/_device/standard_readable.py index 1b60172d03..fe6a7a3fd6 100644 --- a/src/ophyd_async/core/devices/standard_readable.py +++ b/src/ophyd_async/core/_device/standard_readable.py @@ -3,8 +3,8 @@ from bluesky.protocols import Configurable, Descriptor, Readable, Reading, Stageable from ..async_status import AsyncStatus -from ..signals import SignalR from ..utils import merge_gathered_dicts +from ._signal.signal import SignalR from .device import Device @@ -30,11 +30,11 @@ def set_readable_signals( Parameters ---------- read: - Signals to make up `read()` + Signals to make up :meth:`~StandardReadable.read` conf: - Signals to make up `read_configuration()` + Signals to make up :meth:`~StandardReadable.read_configuration` read_uncached: - Signals to make up `read()` that won't be cached + Signals to make up :meth:`~StandardReadable.read` that won't be cached """ self._read_signals = tuple(read) self._configuration_signals = tuple(config) diff --git a/src/ophyd_async/core/async_status.py b/src/ophyd_async/core/async_status.py index 0905845cf9..96f9f717a9 100644 --- a/src/ophyd_async/core/async_status.py +++ b/src/ophyd_async/core/async_status.py @@ -1,3 +1,5 @@ +"""Equivalent of bluesky.protols.Status for asynchronous tasks.""" + import asyncio import functools from typing import Awaitable, Callable, Coroutine, List, Optional, cast diff --git a/src/ophyd_async/core/backends/__init__.py b/src/ophyd_async/core/backends/__init__.py deleted file mode 100644 index f88d17e64a..0000000000 --- a/src/ophyd_async/core/backends/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .signal_backend import SignalBackend -from .sim import SimSignalBackend - -__all__ = ["SignalBackend", "SimSignalBackend"] diff --git a/src/ophyd_async/core/devices/__init__.py b/src/ophyd_async/core/devices/__init__.py deleted file mode 100644 index 43fbc4802d..0000000000 --- a/src/ophyd_async/core/devices/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .device import Device, connect_children, get_device_children, name_children -from .device_vector import DeviceVector -from .standard_readable import StandardReadable - -__all__ = [ - "Device", - "connect_children", - "get_device_children", - "name_children", - "DeviceVector", - "StandardReadable", -] diff --git a/src/ophyd_async/core/signals/__init__.py b/src/ophyd_async/core/signals/__init__.py deleted file mode 100644 index 33f7549ada..0000000000 --- a/src/ophyd_async/core/signals/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from .epics import ( - EpicsTransport, - epics_signal_r, - epics_signal_rw, - epics_signal_w, - epics_signal_x, -) -from .signal import ( - Signal, - SignalR, - SignalRW, - SignalW, - SignalX, - observe_value, - set_and_wait_for_value, - set_sim_callback, - set_sim_put_proceeds, - set_sim_value, - wait_for_value, -) - -__all__ = [ - "EpicsTransport", - "epics_signal_r", - "epics_signal_rw", - "epics_signal_w", - "epics_signal_x", - "Signal", - "SignalR", - "SignalRW", - "SignalW", - "SignalX", - "observe_value", - "set_and_wait_for_value", - "set_sim_callback", - "set_sim_put_proceeds", - "set_sim_value", - "wait_for_value", -] diff --git a/src/ophyd_async/epics/__init__.py b/src/ophyd_async/epics/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ophyd_async/epics/_backend/__init__.py b/src/ophyd_async/epics/_backend/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/ophyd_async/core/backends/_aioca.py b/src/ophyd_async/epics/_backend/_aioca.py similarity index 99% rename from src/ophyd_async/core/backends/_aioca.py rename to src/ophyd_async/epics/_backend/_aioca.py index 7cb60b10d1..46bf491486 100644 --- a/src/ophyd_async/core/backends/_aioca.py +++ b/src/ophyd_async/epics/_backend/_aioca.py @@ -17,10 +17,10 @@ from bluesky.protocols import Descriptor, Dtype, Reading from epicscorelibs.ca import dbr -from ..signals.signal import SignalBackend -from ..utils import ( +from ophyd_async.core import ( NotConnected, ReadingValueCallback, + SignalBackend, T, get_dtype, get_unique, diff --git a/src/ophyd_async/core/backends/_p4p.py b/src/ophyd_async/epics/_backend/_p4p.py similarity index 99% rename from src/ophyd_async/core/backends/_p4p.py rename to src/ophyd_async/epics/_backend/_p4p.py index f9b855e568..d92d36a604 100644 --- a/src/ophyd_async/core/backends/_p4p.py +++ b/src/ophyd_async/epics/_backend/_p4p.py @@ -8,10 +8,10 @@ from bluesky.protocols import Descriptor, Dtype, Reading from p4p.client.asyncio import Context, Subscription -from ..signals.signal import SignalBackend -from ..utils import ( +from ophyd_async.core import ( NotConnected, ReadingValueCallback, + SignalBackend, T, get_dtype, get_unique, diff --git a/src/ophyd_async/epics/areadetector/__init__.py b/src/ophyd_async/epics/areadetector/__init__.py new file mode 100644 index 0000000000..ef910a4170 --- /dev/null +++ b/src/ophyd_async/epics/areadetector/__init__.py @@ -0,0 +1,22 @@ +from .ad_driver import ADDriver +from .directory_provider import DirectoryProvider, TmpDirectoryProvider +from .hdf_streamer_det import HDFStreamerDet +from .nd_file_hdf import NDFileHDF +from .nd_plugin import NDPlugin, NDPluginStats +from .single_trigger_det import SingleTriggerDet +from .utils import FileWriteMode, ImageMode, ad_r, ad_rw + +__all__ = [ + "ADDriver", + "DirectoryProvider", + "TmpDirectoryProvider", + "HDFStreamerDet", + "NDFileHDF", + "NDPlugin", + "NDPluginStats", + "SingleTriggerDet", + "FileWriteMode", + "ImageMode", + "ad_r", + "ad_rw", +] diff --git a/src/ophyd_async/epics/areadetector/ad_driver.py b/src/ophyd_async/epics/areadetector/ad_driver.py new file mode 100644 index 0000000000..7b95e53d6e --- /dev/null +++ b/src/ophyd_async/epics/areadetector/ad_driver.py @@ -0,0 +1,18 @@ +from ophyd_async.core import Device + +from ..signal.signal import epics_signal_rw +from .utils import ImageMode, ad_r, ad_rw + + +class ADDriver(Device): + def __init__(self, prefix: str) -> None: + # Define some signals + self.acquire = ad_rw(bool, prefix + "Acquire") + self.acquire_time = ad_rw(float, prefix + "AcquireTime") + self.num_images = ad_rw(int, prefix + "NumImages") + self.image_mode = ad_rw(ImageMode, prefix + "ImageMode") + self.array_counter = ad_rw(int, prefix + "ArrayCounter") + self.array_size_x = ad_r(int, prefix + "ArraySizeX") + self.array_size_y = ad_r(int, prefix + "ArraySizeY") + # There is no _RBV for this one + self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins") diff --git a/src/ophyd_async/epics/areadetector/directory_provider.py b/src/ophyd_async/epics/areadetector/directory_provider.py new file mode 100644 index 0000000000..8c05d1f2ef --- /dev/null +++ b/src/ophyd_async/epics/areadetector/directory_provider.py @@ -0,0 +1,18 @@ +import tempfile +from abc import abstractmethod +from pathlib import Path +from typing import Protocol + + +class DirectoryProvider(Protocol): + @abstractmethod + async def get_directory(self) -> Path: + ... + + +class TmpDirectoryProvider(DirectoryProvider): + def __init__(self) -> None: + self._directory = Path(tempfile.mkdtemp()) + + async def get_directory(self) -> Path: + return self._directory diff --git a/src/ophyd_async/devices/areadetector.py b/src/ophyd_async/epics/areadetector/hdf_streamer_det.py similarity index 59% rename from src/ophyd_async/devices/areadetector.py rename to src/ophyd_async/epics/areadetector/hdf_streamer_det.py index 84a2348a93..65870d7c91 100644 --- a/src/ophyd_async/devices/areadetector.py +++ b/src/ophyd_async/epics/areadetector/hdf_streamer_det.py @@ -1,126 +1,35 @@ import asyncio import collections -import tempfile import time -from abc import abstractmethod -from enum import Enum -from pathlib import Path -from typing import Callable, Dict, Iterator, Optional, Protocol, Sequence, Sized, Type +from typing import Callable, Dict, Iterator, Optional, Sized from bluesky.protocols import ( Asset, Descriptor, Flyable, PartialEvent, - Triggerable, WritesExternalAssets, ) from bluesky.utils import new_uid from event_model import compose_stream_resource -from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.devices import Device, StandardReadable -from ophyd_async.core.signals import ( - SignalR, - SignalRW, - epics_signal_r, - epics_signal_rw, +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + AsyncStatus, + StandardReadable, set_and_wait_for_value, ) -from ophyd_async.core.utils import DEFAULT_TIMEOUT, T +from .ad_driver import ADDriver +from .directory_provider import DirectoryProvider +from .nd_file_hdf import NDFileHDF +from .utils import FileWriteMode, ImageMode -def ad_rw(datatype: Type[T], prefix: str) -> SignalRW[T]: - return epics_signal_rw(datatype, prefix + "_RBV", prefix) - - -def ad_r(datatype: Type[T], prefix: str) -> SignalR[T]: - return epics_signal_r(datatype, prefix + "_RBV") - - -class ImageMode(Enum): - single = "Single" - multiple = "Multiple" - continuous = "Continuous" - - -class ADDriver(Device): - def __init__(self, prefix: str) -> None: - # Define some signals - self.acquire = ad_rw(bool, prefix + "Acquire") - self.acquire_time = ad_rw(float, prefix + "AcquireTime") - self.num_images = ad_rw(int, prefix + "NumImages") - self.image_mode = ad_rw(ImageMode, prefix + "ImageMode") - self.array_counter = ad_rw(int, prefix + "ArrayCounter") - self.array_size_x = ad_r(int, prefix + "ArraySizeX") - self.array_size_y = ad_r(int, prefix + "ArraySizeY") - # There is no _RBV for this one - self.wait_for_plugins = epics_signal_rw(bool, prefix + "WaitForPlugins") - - -class NDPlugin(Device): - pass - - -class NDPluginStats(NDPlugin): - def __init__(self, prefix: str) -> None: - # Define some signals - self.unique_id = ad_r(int, prefix + "UniqueId") - - -class SingleTriggerDet(StandardReadable, Triggerable): - def __init__( - self, - drv: ADDriver, - read_uncached: Sequence[SignalR] = (), - name="", - **plugins: NDPlugin, - ) -> None: - self.drv = drv - self.__dict__.update(plugins) - self.set_readable_signals( - # Can't subscribe to read signals as race between monitor coming back and - # caput callback on acquire - read_uncached=[self.drv.array_counter] + list(read_uncached), - config=[self.drv.acquire_time], - ) - super().__init__(name=name) - - @AsyncStatus.wrap - async def stage(self) -> None: - await asyncio.gather( - self.drv.image_mode.set(ImageMode.single), - self.drv.wait_for_plugins.set(True), - ) - await super().stage() - - @AsyncStatus.wrap - async def trigger(self) -> None: - await self.drv.acquire.set(1) - - -class FileWriteMode(str, Enum): - single = "Single" - capture = "Capture" - stream = "Stream" - +# How long in seconds to wait between flushes of HDF datasets +FLUSH_PERIOD = 0.5 -class NDFileHDF(Device): - def __init__(self, prefix: str) -> None: - # Define some signals - self.file_path = ad_rw(str, prefix + "FilePath") - self.file_name = ad_rw(str, prefix + "FileName") - self.file_template = ad_rw(str, prefix + "FileTemplate") - self.full_file_name = ad_r(str, prefix + "FullFileName") - self.file_write_mode = ad_rw(FileWriteMode, prefix + "FileWriteMode") - self.num_capture = ad_rw(int, prefix + "NumCapture") - self.num_captured = ad_r(int, prefix + "NumCaptured") - self.swmr_mode = ad_rw(bool, prefix + "SWMRMode") - self.lazy_open = ad_rw(bool, prefix + "LazyOpen") - self.capture = ad_rw(bool, prefix + "Capture") - self.flush_now = epics_signal_rw(bool, prefix + "FlushNow") - self.array_size0 = ad_r(int, prefix + "ArraySize0") - self.array_size1 = ad_r(int, prefix + "ArraySize1") +# How long to wait for new frames before timing out +FRAME_TIMEOUT = 120 class _HDFResource: @@ -160,33 +69,12 @@ async def flush_and_publish(self, hdf: NDFileHDF): event_count = num_captured - self._last_emitted if event_count: self._append_datum(event_count) - await hdf.flush_now.set(1) + await hdf.flush_now.set(True) self._last_flush = time.monotonic() if time.monotonic() - self._last_flush > FRAME_TIMEOUT: raise TimeoutError(f"{hdf.name}: writing stalled on frame {num_captured}") -class DirectoryProvider(Protocol): - @abstractmethod - async def get_directory(self) -> Path: - ... - - -class TmpDirectoryProvider(DirectoryProvider): - def __init__(self) -> None: - self._directory = Path(tempfile.mkdtemp()) - - async def get_directory(self) -> Path: - return self._directory - - -# How long in seconds to wait between flushes of HDF datasets -FLUSH_PERIOD = 0.5 - -# How long to wait for new frames before timing out -FRAME_TIMEOUT = 120 - - class HDFStreamerDet(StandardReadable, Flyable, WritesExternalAssets): def __init__( self, drv: ADDriver, hdf: NDFileHDF, dp: DirectoryProvider, name="" @@ -273,7 +161,7 @@ async def complete(self) -> None: @AsyncStatus.wrap async def unstage(self) -> None: # Already done a caput callback in _capture_status, so can't do one here - await self.hdf.capture.set(0, wait=False) + await self.hdf.capture.set(False, wait=False) assert self._capture_status, "Stage not run" await self._capture_status await super().unstage() diff --git a/src/ophyd_async/epics/areadetector/nd_file_hdf.py b/src/ophyd_async/epics/areadetector/nd_file_hdf.py new file mode 100644 index 0000000000..b8d6096fda --- /dev/null +++ b/src/ophyd_async/epics/areadetector/nd_file_hdf.py @@ -0,0 +1,22 @@ +from ophyd_async.core import Device + +from ..signal.signal import epics_signal_rw +from .utils import FileWriteMode, ad_r, ad_rw + + +class NDFileHDF(Device): + def __init__(self, prefix: str) -> None: + # Define some signals + self.file_path = ad_rw(str, prefix + "FilePath") + self.file_name = ad_rw(str, prefix + "FileName") + self.file_template = ad_rw(str, prefix + "FileTemplate") + self.full_file_name = ad_r(str, prefix + "FullFileName") + self.file_write_mode = ad_rw(FileWriteMode, prefix + "FileWriteMode") + self.num_capture = ad_rw(int, prefix + "NumCapture") + self.num_captured = ad_r(int, prefix + "NumCaptured") + self.swmr_mode = ad_rw(bool, prefix + "SWMRMode") + self.lazy_open = ad_rw(bool, prefix + "LazyOpen") + self.capture = ad_rw(bool, prefix + "Capture") + self.flush_now = epics_signal_rw(bool, prefix + "FlushNow") + self.array_size0 = ad_r(int, prefix + "ArraySize0") + self.array_size1 = ad_r(int, prefix + "ArraySize1") diff --git a/src/ophyd_async/epics/areadetector/nd_plugin.py b/src/ophyd_async/epics/areadetector/nd_plugin.py new file mode 100644 index 0000000000..0128cade29 --- /dev/null +++ b/src/ophyd_async/epics/areadetector/nd_plugin.py @@ -0,0 +1,13 @@ +from ophyd_async.core import Device + +from .utils import ad_r + + +class NDPlugin(Device): + pass + + +class NDPluginStats(NDPlugin): + def __init__(self, prefix: str) -> None: + # Define some signals + self.unique_id = ad_r(int, prefix + "UniqueId") diff --git a/src/ophyd_async/epics/areadetector/single_trigger_det.py b/src/ophyd_async/epics/areadetector/single_trigger_det.py new file mode 100644 index 0000000000..271890c321 --- /dev/null +++ b/src/ophyd_async/epics/areadetector/single_trigger_det.py @@ -0,0 +1,41 @@ +import asyncio +from typing import Sequence + +from bluesky.protocols import Triggerable + +from ophyd_async.core import AsyncStatus, SignalR, StandardReadable + +from .ad_driver import ADDriver +from .nd_plugin import NDPlugin +from .utils import ImageMode + + +class SingleTriggerDet(StandardReadable, Triggerable): + def __init__( + self, + drv: ADDriver, + read_uncached: Sequence[SignalR] = (), + name="", + **plugins: NDPlugin, + ) -> None: + self.drv = drv + self.__dict__.update(plugins) + self.set_readable_signals( + # Can't subscribe to read signals as race between monitor coming back and + # caput callback on acquire + read_uncached=[self.drv.array_counter] + list(read_uncached), + config=[self.drv.acquire_time], + ) + super().__init__(name=name) + + @AsyncStatus.wrap + async def stage(self) -> None: + await asyncio.gather( + self.drv.image_mode.set(ImageMode.single), + self.drv.wait_for_plugins.set(True), + ) + await super().stage() + + @AsyncStatus.wrap + async def trigger(self) -> None: + await self.drv.acquire.set(True) diff --git a/src/ophyd_async/epics/areadetector/utils.py b/src/ophyd_async/epics/areadetector/utils.py new file mode 100644 index 0000000000..25b283e017 --- /dev/null +++ b/src/ophyd_async/epics/areadetector/utils.py @@ -0,0 +1,26 @@ +from enum import Enum +from typing import Type + +from ophyd_async.core import SignalR, SignalRW, T + +from ..signal.signal import epics_signal_r, epics_signal_rw + + +def ad_rw(datatype: Type[T], prefix: str) -> SignalRW[T]: + return epics_signal_rw(datatype, prefix + "_RBV", prefix) + + +def ad_r(datatype: Type[T], prefix: str) -> SignalR[T]: + return epics_signal_r(datatype, prefix + "_RBV") + + +class FileWriteMode(str, Enum): + single = "Single" + capture = "Capture" + stream = "Stream" + + +class ImageMode(Enum): + single = "Single" + multiple = "Multiple" + continuous = "Continuous" diff --git a/src/ophyd_async/core/epicsdemo/__init__.py b/src/ophyd_async/epics/demo/__init__.py similarity index 94% rename from src/ophyd_async/core/epicsdemo/__init__.py rename to src/ophyd_async/epics/demo/__init__.py index 5d01f8ec2b..da9a0947a5 100644 --- a/src/ophyd_async/core/epicsdemo/__init__.py +++ b/src/ophyd_async/epics/demo/__init__.py @@ -1,22 +1,22 @@ """Demo EPICS Devices for the tutorial""" import asyncio +import atexit +import random +import string +import subprocess +import sys import time from enum import Enum +from pathlib import Path from typing import Callable, List, Optional import numpy as np from bluesky.protocols import Movable, Stoppable -from ophyd_async.core import ( - AsyncStatus, - Device, - StandardReadable, - epics_signal_r, - epics_signal_rw, - epics_signal_x, - observe_value, -) +from ophyd_async.core import AsyncStatus, Device, StandardReadable, observe_value + +from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x class EnergyMode(Enum): @@ -130,12 +130,6 @@ def start_ioc_subprocess() -> str: """Start an IOC subprocess with EPICS database for sample stage and sensor with the same pv prefix """ - import atexit - import random - import string - import subprocess - import sys - from pathlib import Path pv_prefix = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + ":" here = Path(__file__).absolute().parent diff --git a/src/ophyd_async/core/epicsdemo/mover.db b/src/ophyd_async/epics/demo/mover.db similarity index 100% rename from src/ophyd_async/core/epicsdemo/mover.db rename to src/ophyd_async/epics/demo/mover.db diff --git a/src/ophyd_async/core/epicsdemo/sensor.db b/src/ophyd_async/epics/demo/sensor.db similarity index 100% rename from src/ophyd_async/core/epicsdemo/sensor.db rename to src/ophyd_async/epics/demo/sensor.db diff --git a/src/ophyd_async/epics/motion/__init__.py b/src/ophyd_async/epics/motion/__init__.py new file mode 100644 index 0000000000..5effa7fb6e --- /dev/null +++ b/src/ophyd_async/epics/motion/__init__.py @@ -0,0 +1,3 @@ +from .motor import Motor + +__all__ = ["Motor"] diff --git a/src/ophyd_async/devices/motor.py b/src/ophyd_async/epics/motion/motor.py similarity index 93% rename from src/ophyd_async/devices/motor.py rename to src/ophyd_async/epics/motion/motor.py index 846d24320c..9b0813705d 100644 --- a/src/ophyd_async/devices/motor.py +++ b/src/ophyd_async/epics/motion/motor.py @@ -4,13 +4,9 @@ from bluesky.protocols import Movable, Stoppable -from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.devices import StandardReadable -from ophyd_async.core.signals.epics import ( - epics_signal_r, - epics_signal_rw, - epics_signal_x, -) +from ophyd_async.core import AsyncStatus, StandardReadable + +from ..signal.signal import epics_signal_r, epics_signal_rw, epics_signal_x class Motor(StandardReadable, Movable, Stoppable): diff --git a/src/ophyd_async/epics/signal/__init__.py b/src/ophyd_async/epics/signal/__init__.py new file mode 100644 index 0000000000..ca9cebd3fd --- /dev/null +++ b/src/ophyd_async/epics/signal/__init__.py @@ -0,0 +1,10 @@ +from .pvi_get import pvi_get +from .signal import epics_signal_r, epics_signal_rw, epics_signal_w, epics_signal_x + +__all__ = [ + "pvi_get", + "epics_signal_r", + "epics_signal_rw", + "epics_signal_w", + "epics_signal_x", +] diff --git a/src/ophyd_async/epics/signal/_epics_transport.py b/src/ophyd_async/epics/signal/_epics_transport.py new file mode 100644 index 0000000000..145af21622 --- /dev/null +++ b/src/ophyd_async/epics/signal/_epics_transport.py @@ -0,0 +1,31 @@ +"""EPICS Signals over CA or PVA""" + +from __future__ import annotations + +from enum import Enum + +try: + from .._backend._aioca import CaSignalBackend +except ImportError as ca_error: + + class CaSignalBackend: # type: ignore + def __init__(*args, ca_error=ca_error, **kwargs): + raise NotImplementedError("CA support not available") from ca_error + + +try: + from .._backend._p4p import PvaSignalBackend +except ImportError as pva_error: + + class PvaSignalBackend: # type: ignore + def __init__(*args, pva_error=pva_error, **kwargs): + raise NotImplementedError("PVA support not available") from pva_error + + +class EpicsTransport(Enum): + """The sorts of transport EPICS support""" + + #: Use Channel Access (using aioca library) + ca = CaSignalBackend + #: Use PVAccess (using p4p library) + pva = PvaSignalBackend diff --git a/src/ophyd_async/epics/signal/pvi_get.py b/src/ophyd_async/epics/signal/pvi_get.py new file mode 100644 index 0000000000..f31899448c --- /dev/null +++ b/src/ophyd_async/epics/signal/pvi_get.py @@ -0,0 +1,22 @@ +from typing import Dict, TypedDict + +from p4p.client.asyncio import Context + + +class PVIEntry(TypedDict, total=False): + d: str + r: str + rw: str + w: str + x: str + + +async def pvi_get(pv: str, ctxt: Context, timeout: float = 5.0) -> Dict[str, PVIEntry]: + pv_info = ctxt.get(pv, timeout=timeout).get("pvi").todict() + + result = {} + + for attr_name, attr_info in pv_info.items(): + result[attr_name] = PVIEntry(**attr_info) # type: ignore + + return result diff --git a/src/ophyd_async/core/signals/epics.py b/src/ophyd_async/epics/signal/signal.py similarity index 72% rename from src/ophyd_async/core/signals/epics.py rename to src/ophyd_async/epics/signal/signal.py index 0ab3123a47..c02c436d82 100644 --- a/src/ophyd_async/core/signals/epics.py +++ b/src/ophyd_async/epics/signal/signal.py @@ -2,38 +2,19 @@ from __future__ import annotations -from enum import Enum from typing import Optional, Tuple, Type -from ..utils import T, get_unique -from .signal import SignalBackend, SignalR, SignalRW, SignalW, SignalX - -try: - from ..backends._aioca import CaSignalBackend -except ImportError as ca_error: - - class CaSignalBackend: # type: ignore - def __init__(*args, ca_error=ca_error, **kwargs): - raise NotImplementedError("CA support not available") from ca_error - - -try: - from ..backends._p4p import PvaSignalBackend -except ImportError as pva_error: - - class PvaSignalBackend: # type: ignore - def __init__(*args, pva_error=pva_error, **kwargs): - raise NotImplementedError("PVA support not available") from pva_error - - -class EpicsTransport(Enum): - """The sorts of transport EPICS support""" - - #: Use Channel Access (using aioca library) - ca = CaSignalBackend - #: Use PVAccess (using p4p library) - pva = PvaSignalBackend - +from ophyd_async.core import ( + SignalBackend, + SignalR, + SignalRW, + SignalW, + SignalX, + T, + get_unique, +) + +from ._epics_transport import EpicsTransport _default_epics_transport = EpicsTransport.ca diff --git a/src/ophyd_async/panda/__init__.py b/src/ophyd_async/panda/__init__.py new file mode 100644 index 0000000000..8df8dcde1f --- /dev/null +++ b/src/ophyd_async/panda/__init__.py @@ -0,0 +1,21 @@ +from .panda import ( + PandA, + PcapBlock, + PulseBlock, + PVIEntry, + SeqBlock, + SeqTable, + SeqTrigger, + pvi, +) + +__all__ = [ + "PandA", + "PcapBlock", + "PulseBlock", + "PVIEntry", + "SeqBlock", + "SeqTable", + "SeqTrigger", + "pvi", +] diff --git a/src/ophyd_async/devices/panda.py b/src/ophyd_async/panda/panda.py similarity index 88% rename from src/ophyd_async/devices/panda.py rename to src/ophyd_async/panda/panda.py index 01bb010a20..bf130b532b 100644 --- a/src/ophyd_async/devices/panda.py +++ b/src/ophyd_async/panda/panda.py @@ -12,6 +12,7 @@ Tuple, Type, TypedDict, + cast, get_args, get_origin, get_type_hints, @@ -21,17 +22,22 @@ import numpy.typing as npt from p4p.client.thread import Context -from ophyd_async.core.backends import SimSignalBackend -from ophyd_async.core.devices import Device, DeviceVector -from ophyd_async.core.signals import ( +from ophyd_async.core import ( + Device, + DeviceVector, Signal, + SignalBackend, SignalR, SignalRW, SignalX, + SimSignalBackend, +) +from ophyd_async.epics.signal import ( epics_signal_r, epics_signal_rw, epics_signal_w, epics_signal_x, + pvi_get, ) @@ -92,7 +98,7 @@ class PVIEntry(TypedDict, total=False): x: str -def block_name_number(block_name: str) -> Tuple[str, Optional[int]]: +def _block_name_number(block_name: str) -> Tuple[str, Optional[int]]: """Maps a panda block name to a block and number. There are exceptions to this rule; some blocks like pcap do not contain numbers. @@ -110,7 +116,7 @@ def block_name_number(block_name: str) -> Tuple[str, Optional[int]]: return block_name, None -def _remove_inconsistent_blocks(pvi: Dict[str, PVIEntry]) -> None: +def _remove_inconsistent_blocks(pvi_info: Dict[str, PVIEntry]) -> None: """Remove blocks from pvi information. This is needed because some pandas have 'pcap' and 'pcap1' blocks, which are @@ -118,20 +124,15 @@ def _remove_inconsistent_blocks(pvi: Dict[str, PVIEntry]) -> None: for example. """ - pvi_keys = set(pvi.keys()) + pvi_keys = set(pvi_info.keys()) for k in pvi_keys: kn = re.sub(r"\d*$", "", k) if kn and k != kn and kn in pvi_keys: - del pvi[k] - - -async def pvi_get(pv: str, ctxt: Context, timeout: float = 5.0) -> Dict[str, PVIEntry]: - pv_info = ctxt.get(pv, timeout=timeout).get("pvi").todict() + del pvi_info[k] - result = {} - for attr_name, attr_info in pv_info.items(): - result[attr_name] = PVIEntry(**attr_info) # type: ignore +async def pvi(pv: str, ctxt: Context, timeout: float = 5.0) -> Dict[str, PVIEntry]: + result = await pvi_get(pv, ctxt, timeout=timeout) _remove_inconsistent_blocks(result) return result @@ -195,7 +196,7 @@ async def _make_block( block = self.verify_block(name, num) field_annos = get_type_hints(block, globalns=globals()) - block_pvi = await pvi_get(block_pv, self.ctxt) if not sim else None + block_pvi = await pvi(block_pv, self.ctxt) if not sim else None # finds which fields this class actually has, e.g. delay, width... for sig_name, sig_type in field_annos.items(): @@ -204,6 +205,7 @@ async def _make_block( # if not in sim mode, if block_pvi: + block_pvi = cast(Dict[str, PVIEntry], block_pvi) # try to get this block in the pvi. entry: Optional[PVIEntry] = block_pvi.get(sig_name) if entry is None: @@ -215,7 +217,9 @@ async def _make_block( signal = self._make_signal(entry, args[0] if len(args) > 0 else None) else: - backend = SimSignalBackend(args[0] if len(args) > 0 else None, block_pv) + backend: SignalBackend = SimSignalBackend( + args[0] if len(args) > 0 else None, block_pv + ) signal = SignalX(backend) if not origin else origin(backend) setattr(block, sig_name, signal) @@ -237,7 +241,7 @@ async def _make_untyped_block(self, block_pv: str): included dynamically anyway. """ block = Device() - block_pvi = await pvi_get(block_pv, self.ctxt) + block_pvi: Dict[str, PVIEntry] = await pvi(block_pv, self.ctxt) for signal_name, signal_pvi in block_pvi.items(): signal = self._make_signal(signal_pvi) @@ -284,7 +288,7 @@ async def connect(self, sim=False) -> None: If there's no pvi information, that's because we're in sim mode. In that case, makes all required blocks. """ - pvi = await pvi_get(self._init_prefix + ":PVI", self.ctxt) if not sim else None + pvi_info = await pvi(self._init_prefix + ":PVI", self.ctxt) if not sim else None hints = { attr_name: attr_type for attr_name, attr_type in get_type_hints(self, globalns=globals()).items() @@ -292,9 +296,10 @@ async def connect(self, sim=False) -> None: } # create all the blocks pvi says it should have, - if pvi: - for block_name, block_pvi in pvi.items(): - name, num = block_name_number(block_name) + if pvi_info: + pvi_info = cast(Dict[str, PVIEntry], pvi_info) + for block_name, block_pvi in pvi_info.items(): + name, num = _block_name_number(block_name) if name in hints: block = await self._make_block(name, num, block_pvi["d"]) @@ -306,13 +311,13 @@ async def connect(self, sim=False) -> None: # then check if the ones defined in this class are in the pvi info # make them if there is no pvi info, i.e. sim mode. for block_name in hints.keys(): - if pvi is not None: + if pvi_info is not None: pvi_name = block_name if get_origin(hints[block_name]) == DeviceVector: pvi_name += "1" - entry: Optional[PVIEntry] = pvi.get(pvi_name) + entry: Optional[PVIEntry] = pvi_info.get(pvi_name) assert entry, f"Expected PandA to contain {block_name} block." assert list(entry) == [ diff --git a/tests/conftest.py b/tests/conftest.py index 94e2da660b..ec496795fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,12 +8,14 @@ import pytest from bluesky.run_engine import RunEngine, TransitionError -RECORD = str(Path(__file__).parent / "devices" / "db" / "panda.db") +RECORD = str(Path(__file__).parent / "panda" / "db" / "panda.db") INCOMPLETE_BLOCK_RECORD = str( - Path(__file__).parent / "devices" / "db" / "incomplete_block_panda.db" + Path(__file__).parent / "panda" / "db" / "incomplete_block_panda.db" +) +INCOMPLETE_RECORD = str(Path(__file__).parent / "panda" / "db" / "incomplete_panda.db") +EXTRA_BLOCKS_RECORD = str( + Path(__file__).parent / "panda" / "db" / "extra_blocks_panda.db" ) -INCOMPLETE_RECORD = str(Path(__file__).parent / "devices" / "db" / "incomplete_panda.db") -EXTRA_BLOCKS_RECORD = str(Path(__file__).parent / "devices" / "db" / "extra_blocks_panda.db") @pytest.fixture(scope="function") diff --git a/tests/core/backends/test_sim.py b/tests/core/_device/_backend/test_sim.py similarity index 96% rename from tests/core/backends/test_sim.py rename to tests/core/_device/_backend/test_sim.py index 2285bdcf08..a7459b9429 100644 --- a/tests/core/backends/test_sim.py +++ b/tests/core/_device/_backend/test_sim.py @@ -8,9 +8,7 @@ import pytest from bluesky.protocols import Reading -from ophyd_async.core.backends import SignalBackend, SimSignalBackend -from ophyd_async.core.signals import Signal -from ophyd_async.core.utils import T +from ophyd_async.core import Signal, SignalBackend, SimSignalBackend, T class MyEnum(str, Enum): diff --git a/tests/core/signals/test_signal.py b/tests/core/_device/_signal/test_signal.py similarity index 97% rename from tests/core/signals/test_signal.py rename to tests/core/_device/_signal/test_signal.py index da59937546..2309f2396d 100644 --- a/tests/core/signals/test_signal.py +++ b/tests/core/_device/_signal/test_signal.py @@ -4,10 +4,10 @@ import pytest -from ophyd_async.core.backends import SimSignalBackend -from ophyd_async.core.signals import ( +from ophyd_async.core import ( Signal, SignalRW, + SimSignalBackend, set_and_wait_for_value, set_sim_put_proceeds, set_sim_value, diff --git a/tests/core/devices/test_device.py b/tests/core/_device/test_device.py similarity index 93% rename from tests/core/devices/test_device.py rename to tests/core/_device/test_device.py index b8c62b886c..7e9c280d75 100644 --- a/tests/core/devices/test_device.py +++ b/tests/core/_device/test_device.py @@ -3,9 +3,13 @@ import pytest -from ophyd_async.core.device_collector import DeviceCollector -from ophyd_async.core.devices import Device, DeviceVector, get_device_children -from ophyd_async.core.utils import wait_for_connection +from ophyd_async.core import ( + Device, + DeviceCollector, + DeviceVector, + get_device_children, + wait_for_connection, +) class DummyBaseDevice(Device): diff --git a/tests/core/_device/test_device_collector.py b/tests/core/_device/test_device_collector.py new file mode 100644 index 0000000000..65c1cf9997 --- /dev/null +++ b/tests/core/_device/test_device_collector.py @@ -0,0 +1,14 @@ +import pytest + +from ophyd_async.core import Device, DeviceCollector + + +class Dummy(Device): + def connect(self, sim: bool = False): + raise AttributeError() + + +def test_device_collector_propagates_error(RE): + with pytest.raises(AttributeError): + with DeviceCollector(): + _ = Dummy("somename") diff --git a/tests/core/test_async_status.py b/tests/core/test_async_status.py index cfd3c15707..35b074bb2c 100644 --- a/tests/core/test_async_status.py +++ b/tests/core/test_async_status.py @@ -7,8 +7,7 @@ from bluesky.protocols import Movable, Status from bluesky.utils import FailedStatus -from ophyd_async.core.async_status import AsyncStatus -from ophyd_async.core.devices import Device +from ophyd_async.core import AsyncStatus, Device async def test_async_status_success(): @@ -131,3 +130,9 @@ async def test_status_propogates_traceback_under_RE(RE) -> None: assert expected_call_stack == [ x.name for x in traceback.extract_tb(exception.__traceback__) ] + + +async def test_async_status_exception_timeout(): + st = AsyncStatus(asyncio.sleep(0.1)) + with pytest.raises(Exception): + st.exception(timeout=1.0) diff --git a/tests/epics/areadetector/__init__.py b/tests/epics/areadetector/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/devices/test_area_detector.py b/tests/epics/areadetector/test_hdf_streamer_det.py similarity index 72% rename from tests/devices/test_area_detector.py rename to tests/epics/areadetector/test_hdf_streamer_det.py index 891e17483b..70edde20fd 100644 --- a/tests/devices/test_area_detector.py +++ b/tests/epics/areadetector/test_hdf_streamer_det.py @@ -6,41 +6,18 @@ import bluesky.plans as bp import bluesky.preprocessors as bpp import pytest -from ophyd_async.core.device_collector import DeviceCollector -from ophyd_async.core.signals import set_sim_put_proceeds, set_sim_value -from ophyd_async.devices.areadetector import ( +from ophyd_async.core import DeviceCollector, set_sim_put_proceeds, set_sim_value +from ophyd_async.epics.areadetector import ( ADDriver, FileWriteMode, HDFStreamerDet, ImageMode, NDFileHDF, - NDPluginStats, - SingleTriggerDet, TmpDirectoryProvider, ) -@pytest.fixture -async def single_trigger_det(): - async with DeviceCollector(sim=True): - stats = NDPluginStats("PREFIX:STATS") - det = SingleTriggerDet( - drv=ADDriver("PREFIX:DRV"), stats=stats, read_uncached=[stats.unique_id] - ) - - assert det.name == "det" - assert stats.name == "det-stats" - # Set non-default values to check they are set back - # These are using set_sim_value to simulate the backend IOC being setup - # in a particular way, rather than values being set by the Ophyd signals - set_sim_value(det.drv.acquire_time, 0.5) - set_sim_value(det.drv.array_counter, 1) - set_sim_value(det.drv.image_mode, ImageMode.continuous) - set_sim_value(stats.unique_id, 3) - yield det - - class DocHolder: def __init__(self): self.names = [] @@ -51,24 +28,9 @@ def append(self, name, doc): self.docs.append(doc) -async def test_single_trigger_det(single_trigger_det: SingleTriggerDet, RE): - d = DocHolder() - RE(bp.count([single_trigger_det]), d.append) - - drv = single_trigger_det.drv - assert 1 == await drv.acquire.get_value() - assert ImageMode.single == await drv.image_mode.get_value() - assert True is await drv.wait_for_plugins.get_value() - - assert d.names == ["start", "descriptor", "event", "stop"] - _, descriptor, event, _ = d.docs - assert descriptor["configuration"]["det"]["data"]["det-drv-acquire_time"] == 0.5 - assert ( - descriptor["data_keys"]["det-stats-unique_id"]["source"] - == "sim://PREFIX:STATSUniqueId_RBV" - ) - assert event["data"]["det-drv-array_counter"] == 1 - assert event["data"]["det-stats-unique_id"] == 3 +@pytest.fixture +def doc_holder(): + return DocHolder() @pytest.fixture @@ -103,9 +65,10 @@ async def hdf_streamer_dets(): yield deta, detb -async def test_hdf_streamer_dets_step(hdf_streamer_dets: List[HDFStreamerDet], RE): - d = DocHolder() - RE(bp.count(hdf_streamer_dets), d.append) +async def test_hdf_streamer_dets_step( + hdf_streamer_dets: List[HDFStreamerDet], RE, doc_holder: DocHolder +): + RE(bp.count(hdf_streamer_dets), doc_holder.append) drv = hdf_streamer_dets[0].drv assert 1 == await drv.acquire.get_value() @@ -118,7 +81,7 @@ async def test_hdf_streamer_dets_step(hdf_streamer_dets: List[HDFStreamerDet], R assert 0 == await hdf.num_capture.get_value() assert FileWriteMode.stream == await hdf.file_write_mode.get_value() - assert d.names == [ + assert doc_holder.names == [ "start", "descriptor", "stream_resource", @@ -128,7 +91,7 @@ async def test_hdf_streamer_dets_step(hdf_streamer_dets: List[HDFStreamerDet], R "event", "stop", ] - _, descriptor, sra, sda, srb, sdb, event, _ = d.docs + _, descriptor, sra, sda, srb, sdb, event, _ = doc_holder.docs assert descriptor["configuration"]["deta"]["data"]["deta-drv-acquire_time"] == 0.8 assert descriptor["configuration"]["detb"]["data"]["detb-drv-acquire_time"] == 1.8 assert descriptor["data_keys"]["deta"]["shape"] == [768, 1024] @@ -146,9 +109,8 @@ async def test_hdf_streamer_dets_step(hdf_streamer_dets: List[HDFStreamerDet], R # TODO: write test where they are in the same stream after # https://github.com/bluesky/bluesky/issues/1558 async def test_hdf_streamer_dets_fly_different_streams( - hdf_streamer_dets: List[HDFStreamerDet], RE + hdf_streamer_dets: List[HDFStreamerDet], RE, doc_holder: DocHolder ): - d = DocHolder() deta, detb = hdf_streamer_dets for det in hdf_streamer_dets: @@ -174,10 +136,10 @@ def fly_det(num: int): yield from bps.collect(det, stream=True, return_payload=False) yield from bps.wait(group="complete") - RE(fly_det(5), d.append) + RE(fly_det(5), doc_holder.append) # TODO: stream_* will come after descriptor soon - assert d.names == [ + assert doc_holder.names == [ "start", "stream_resource", "stream_datum", @@ -199,7 +161,7 @@ def fly_det(num: int): assert 0 == await hdf.num_capture.get_value() assert FileWriteMode.stream == await hdf.file_write_mode.get_value() - _, sra, sda, descriptora, srb, sdb, descriptorb, _ = d.docs + _, sra, sda, descriptora, srb, sdb, descriptorb, _ = doc_holder.docs assert descriptora["configuration"]["deta"]["data"]["deta-drv-acquire_time"] == 0.8 assert descriptorb["configuration"]["detb"]["data"]["detb-drv-acquire_time"] == 1.8 @@ -220,7 +182,7 @@ async def test_hdf_streamer_dets_timeout(hdf_streamer_dets: List[HDFStreamerDet] set_sim_put_proceeds(det.drv.acquire, False) await det.kickoff() t = time.monotonic() - with patch("ophyd_async.devices.areadetector.FRAME_TIMEOUT", 0.1): + with patch("ophyd_async.epics.areadetector.hdf_streamer_det.FRAME_TIMEOUT", 0.1): with pytest.raises(TimeoutError, match="deta-hdf: writing stalled on frame 1"): await det.complete() assert 1.0 < time.monotonic() - t < 1.1 diff --git a/tests/epics/areadetector/test_single_trigger_det.py b/tests/epics/areadetector/test_single_trigger_det.py new file mode 100644 index 0000000000..2545a8e1d0 --- /dev/null +++ b/tests/epics/areadetector/test_single_trigger_det.py @@ -0,0 +1,66 @@ +import bluesky.plans as bp +import pytest + +from ophyd_async.core import DeviceCollector, set_sim_value +from ophyd_async.epics.areadetector import ( + ADDriver, + ImageMode, + NDPluginStats, + SingleTriggerDet, +) + + +class DocHolder: + def __init__(self): + self.names = [] + self.docs = [] + + def append(self, name, doc): + self.names.append(name) + self.docs.append(doc) + + +@pytest.fixture +def doc_holder(): + return DocHolder() + + +@pytest.fixture +async def single_trigger_det(): + async with DeviceCollector(sim=True): + stats = NDPluginStats("PREFIX:STATS") + det = SingleTriggerDet( + drv=ADDriver("PREFIX:DRV"), stats=stats, read_uncached=[stats.unique_id] + ) + + assert det.name == "det" + assert stats.name == "det-stats" + # Set non-default values to check they are set back + # These are using set_sim_value to simulate the backend IOC being setup + # in a particular way, rather than values being set by the Ophyd signals + set_sim_value(det.drv.acquire_time, 0.5) + set_sim_value(det.drv.array_counter, 1) + set_sim_value(det.drv.image_mode, ImageMode.continuous) + set_sim_value(stats.unique_id, 3) + yield det + + +async def test_single_trigger_det( + single_trigger_det: SingleTriggerDet, RE, doc_holder: DocHolder +): + RE(bp.count([single_trigger_det]), doc_holder.append) + + drv = single_trigger_det.drv + assert 1 == await drv.acquire.get_value() + assert ImageMode.single == await drv.image_mode.get_value() + assert True is await drv.wait_for_plugins.get_value() + + assert doc_holder.names == ["start", "descriptor", "event", "stop"] + _, descriptor, event, _ = doc_holder.docs + assert descriptor["configuration"]["det"]["data"]["det-drv-acquire_time"] == 0.5 + assert ( + descriptor["data_keys"]["det-stats-unique_id"]["source"] + == "sim://PREFIX:STATSUniqueId_RBV" + ) + assert event["data"]["det-drv-array_counter"] == 1 + assert event["data"]["det-stats-unique_id"] == 3 diff --git a/tests/epics/motion/__init__.py b/tests/epics/motion/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/devices/test_motor.py b/tests/epics/motion/test_motor.py similarity index 95% rename from tests/devices/test_motor.py rename to tests/epics/motion/test_motor.py index fcfeebcc80..7706099295 100644 --- a/tests/devices/test_motor.py +++ b/tests/epics/motion/test_motor.py @@ -4,10 +4,9 @@ import pytest from bluesky.protocols import Reading -from ophyd_async.core.device_collector import DeviceCollector -from ophyd_async.core.signals import set_sim_put_proceeds, set_sim_value -from ophyd_async.devices import motor +from ophyd_async.core import DeviceCollector, set_sim_put_proceeds, set_sim_value +from ophyd_async.epics.motion import motor # Long enough for multiple asyncio event loop cycles to run so # all the tasks have a chance to run diff --git a/tests/core/test_epicsdemo.py b/tests/epics/test_demo.py similarity index 85% rename from tests/core/test_epicsdemo.py rename to tests/epics/test_demo.py index b6d6504ffe..8914758798 100644 --- a/tests/core/test_epicsdemo.py +++ b/tests/epics/test_demo.py @@ -8,10 +8,10 @@ from ophyd_async.core import ( DeviceCollector, NotConnected, - epicsdemo, set_sim_callback, set_sim_value, ) +from ophyd_async.epics import demo # Long enough for multiple asyncio event loop cycles to run so # all the tasks have a chance to run @@ -21,7 +21,7 @@ @pytest.fixture async def sim_mover(): async with DeviceCollector(sim=True): - sim_mover = epicsdemo.Mover("BLxxI-MO-TABLE-01:X:") + sim_mover = demo.Mover("BLxxI-MO-TABLE-01:X:") # Signals connected here assert sim_mover.name == "sim_mover" @@ -34,14 +34,14 @@ async def sim_mover(): @pytest.fixture async def sim_sensor(): async with DeviceCollector(sim=True): - sim_sensor = epicsdemo.Sensor("SIM:SENSOR:") + sim_sensor = demo.Sensor("SIM:SENSOR:") # Signals connected here assert sim_sensor.name == "sim_sensor" yield sim_sensor -async def test_mover_moving_well(sim_mover: epicsdemo.Mover) -> None: +async def test_mover_moving_well(sim_mover: demo.Mover) -> None: s = sim_mover.set(0.55) watcher = Mock() s.watch(watcher) @@ -85,7 +85,7 @@ async def test_mover_moving_well(sim_mover: epicsdemo.Mover) -> None: done2.assert_called_once_with(s) -async def test_mover_stopped(sim_mover: epicsdemo.Mover): +async def test_mover_stopped(sim_mover: demo.Mover): callbacks = [] set_sim_callback(sim_mover.stop_, lambda r, v: callbacks.append(v)) @@ -94,7 +94,7 @@ async def test_mover_stopped(sim_mover: epicsdemo.Mover): assert callbacks == [None, None] -async def test_read_mover(sim_mover: epicsdemo.Mover): +async def test_read_mover(sim_mover: demo.Mover): await sim_mover.stage() assert (await sim_mover.read())["sim_mover"]["value"] == 0.0 assert (await sim_mover.describe())["sim_mover"][ @@ -111,7 +111,7 @@ async def test_read_mover(sim_mover: epicsdemo.Mover): assert await sim_mover.describe() -async def test_set_velocity(sim_mover: epicsdemo.Mover) -> None: +async def test_set_velocity(sim_mover: demo.Mover) -> None: v = sim_mover.velocity assert (await v.describe())["sim_mover-velocity"][ "source" @@ -130,15 +130,15 @@ async def test_set_velocity(sim_mover: epicsdemo.Mover) -> None: async def test_mover_disconncted(): with pytest.raises(NotConnected, match="Not all Devices connected"): async with DeviceCollector(timeout=0.1): - m = epicsdemo.Mover("ca://PRE:", name="mover") + m = demo.Mover("ca://PRE:", name="mover") assert m.name == "mover" -async def test_sensor_disconncted(): - with patch("ophyd_async.core.device_collector.logging") as mock_logging: +async def test_sensor_disconnected(): + with patch("ophyd_async.core._device.device_collector.logging") as mock_logging: with pytest.raises(NotConnected, match="Not all Devices connected"): async with DeviceCollector(timeout=0.1): - s = epicsdemo.Sensor("ca://PRE:", name="sensor") + s = demo.Sensor("ca://PRE:", name="sensor") mock_logging.error.assert_called_once_with( """\ 1 Devices did not connect: @@ -149,7 +149,7 @@ async def test_sensor_disconncted(): assert s.name == "sensor" -async def test_read_sensor(sim_sensor: epicsdemo.Sensor): +async def test_read_sensor(sim_sensor: demo.Sensor): sim_sensor.stage() assert (await sim_sensor.read())["sim_sensor-value"]["value"] == 0 assert (await sim_sensor.describe())["sim_sensor-value"][ @@ -157,19 +157,19 @@ async def test_read_sensor(sim_sensor: epicsdemo.Sensor): ] == "sim://SIM:SENSOR:Value" assert (await sim_sensor.read_configuration())["sim_sensor-mode"][ "value" - ] == epicsdemo.EnergyMode.low + ] == demo.EnergyMode.low desc = (await sim_sensor.describe_configuration())["sim_sensor-mode"] assert desc["dtype"] == "string" assert desc["choices"] == ["Low Energy", "High Energy"] # type: ignore - set_sim_value(sim_sensor.mode, epicsdemo.EnergyMode.high) + set_sim_value(sim_sensor.mode, demo.EnergyMode.high) assert (await sim_sensor.read_configuration())["sim_sensor-mode"][ "value" - ] == epicsdemo.EnergyMode.high + ] == demo.EnergyMode.high await sim_sensor.unstage() async def test_assembly_renaming() -> None: - thing = epicsdemo.SampleStage("PRE") + thing = demo.SampleStage("PRE") await thing.connect(sim=True) assert thing.x.name == "" assert thing.x.velocity.name == "" @@ -182,7 +182,7 @@ async def test_assembly_renaming() -> None: assert thing.x.stop_.name == "foo-x-stop" -def test_mover_in_re(sim_mover: epicsdemo.Mover, RE) -> None: +def test_mover_in_re(sim_mover: demo.Mover, RE) -> None: sim_mover.move(0) def my_plan(): diff --git a/tests/core/signals/test_records.db b/tests/epics/test_records.db similarity index 100% rename from tests/core/signals/test_records.db rename to tests/epics/test_records.db diff --git a/tests/core/signals/test_epics.py b/tests/epics/test_signals.py similarity index 98% rename from tests/core/signals/test_epics.py rename to tests/epics/test_signals.py index 9fb821eede..fc61ac39f1 100644 --- a/tests/core/signals/test_epics.py +++ b/tests/epics/test_signals.py @@ -17,7 +17,8 @@ from bluesky.protocols import Reading from ophyd_async.core import NotConnected, SignalBackend, T, get_dtype -from ophyd_async.core.signals.epics import EpicsTransport, _make_backend +from ophyd_async.epics.signal._epics_transport import EpicsTransport +from ophyd_async.epics.signal.signal import _make_backend RECORDS = str(Path(__file__).parent / "test_records.db") PV_PREFIX = "".join(random.choice(string.ascii_lowercase) for _ in range(12)) diff --git a/tests/devices/db/panda.db b/tests/panda/db/panda.db similarity index 100% rename from tests/devices/db/panda.db rename to tests/panda/db/panda.db diff --git a/tests/devices/test_panda.py b/tests/panda/test_panda.py similarity index 94% rename from tests/devices/test_panda.py rename to tests/panda/test_panda.py index 71d4eeb650..630385dc83 100644 --- a/tests/devices/test_panda.py +++ b/tests/panda/test_panda.py @@ -4,9 +4,9 @@ import numpy as np import pytest -from ophyd_async.core.device_collector import DeviceCollector -from ophyd_async.devices.panda import PandA, PVIEntry, SeqTable, SeqTrigger, pvi_get +from ophyd_async.core import DeviceCollector +from ophyd_async.panda import PandA, PVIEntry, SeqTable, SeqTrigger, pvi class DummyDict: @@ -57,7 +57,7 @@ async def test_pvi_get_for_inconsistent_blocks(): "sfp3_sync_out": {}, } - resulting_pvi = await pvi_get("", MockCtxt(dummy_pvi)) + resulting_pvi = await pvi("", MockCtxt(dummy_pvi)) assert "sfp3_sync_out1" not in resulting_pvi assert "pcap1" not in resulting_pvi