From f603e8de19323c285701dbe552ea1b3c27c7ad31 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Tue, 23 Feb 2021 15:15:46 +0000 Subject: [PATCH 01/19] Removed EPICS build system --- Makefile | 46 --------- configure/CONFIG | 30 ------ configure/CONFIG_SITE | 20 ---- configure/Makefile | 8 -- configure/RELEASE | 27 ------ configure/RULES | 7 -- configure/RULES.ioc | 2 - configure/RULES_DIRS | 2 - configure/RULES_TOP | 3 - example/runtest | 3 - example/versions.py | 8 -- python/Makefile | 7 -- softIocApp/Makefile | 81 ---------------- softIocApp/softMain.c | 97 ------------------- {python/softioc => softioc}/README | 0 {python/softioc => softioc}/__init__.py | 0 {python => softioc}/access.acf | 0 {python/softioc => softioc}/alarm.py | 0 {python/softioc => softioc}/builder.py | 0 {python => softioc}/device.dbd | 0 {python/softioc => softioc}/device.py | 0 {python/softioc => softioc}/device_core.py | 0 .../PythonSupport.c => softioc/extension.c | 0 {python/softioc => softioc}/fields.py | 0 {python/softioc => softioc}/imports.py | 0 {python/softioc => softioc}/pvlog.py | 0 {python/softioc => softioc}/pythonSoftIoc.py | 0 {python/softioc => softioc}/softioc.py | 0 {example => tests}/graph.edl | 0 example/testing.py => tests/sim_records.py | 0 example/createdb.py => tests/test_createdb.py | 0 example/startup.py => tests/test_ioc.py | 0 32 files changed, 341 deletions(-) delete mode 100644 Makefile delete mode 100644 configure/CONFIG delete mode 100644 configure/CONFIG_SITE delete mode 100644 configure/Makefile delete mode 100644 configure/RELEASE delete mode 100644 configure/RULES delete mode 100644 configure/RULES.ioc delete mode 100644 configure/RULES_DIRS delete mode 100644 configure/RULES_TOP delete mode 100755 example/runtest delete mode 100644 example/versions.py delete mode 100644 python/Makefile delete mode 100644 softIocApp/Makefile delete mode 100644 softIocApp/softMain.c rename {python/softioc => softioc}/README (100%) rename {python/softioc => softioc}/__init__.py (100%) rename {python => softioc}/access.acf (100%) rename {python/softioc => softioc}/alarm.py (100%) rename {python/softioc => softioc}/builder.py (100%) rename {python => softioc}/device.dbd (100%) rename {python/softioc => softioc}/device.py (100%) rename {python/softioc => softioc}/device_core.py (100%) rename softIocApp/PythonSupport.c => softioc/extension.c (100%) rename {python/softioc => softioc}/fields.py (100%) rename {python/softioc => softioc}/imports.py (100%) rename {python/softioc => softioc}/pvlog.py (100%) rename {python/softioc => softioc}/pythonSoftIoc.py (100%) rename {python/softioc => softioc}/softioc.py (100%) rename {example => tests}/graph.edl (100%) rename example/testing.py => tests/sim_records.py (100%) rename example/createdb.py => tests/test_createdb.py (100%) rename example/startup.py => tests/test_ioc.py (100%) diff --git a/Makefile b/Makefile deleted file mode 100644 index fe067a48..00000000 --- a/Makefile +++ /dev/null @@ -1,46 +0,0 @@ -#Makefile at top of application tree -TOP = . -include $(TOP)/configure/CONFIG -DIRS += configure -DIRS += softIocApp -DIRS += python -include $(TOP)/configure/RULES_TOP - - -# Note that we use pythonIoc to build its own documentation so that it can -# succuessfully import the softioc library. -SPHINX_BUILD := $(shell readlink -f $$(which sphinx-build)) - -PYTHONHOME := $(shell $(PYTHON) -c 'import sys; print(sys.exec_prefix)') - -BUILD_DOCS ?= 1 - -install: pythonIoc -ifeq ($(BUILD_DOCS),1) -install: docs -endif - -clean: clean-pythonIoc - -# Commands for creating startup script with correct paths to EPICS. -define SED_EDIT_COMMANDS -s:@@EPICS_BASE@@:$(EPICS_BASE):; \ -s:@@EPICS_HOST_ARCH@@:$(EPICS_HOST_ARCH):; \ -s:@@PYTHONHOME@@:$(PYTHONHOME): -endef - -# Ensure we get the build time EPICS_BASE into the executable -pythonIoc: pythonIoc.in - sed '$(SED_EDIT_COMMANDS)' $^ >$@ - chmod +x $@ - -clean-pythonIoc: - rm -f pythonIoc - -docs: pythonIoc - ./pythonIoc $(SPHINX_BUILD) -b html docs docs/html - -clean-docs: - rm -rf docs/html - -.PHONY: clean-pythonIoc clean-docs docs diff --git a/configure/CONFIG b/configure/CONFIG deleted file mode 100644 index 722d9caa..00000000 --- a/configure/CONFIG +++ /dev/null @@ -1,30 +0,0 @@ -# CONFIG - Load build configuration data -# -# Do not make changes to this file! - -# Allow user to override where the build rules come from -RULES = $(EPICS_BASE) - -# RELEASE files point to other application tops -include $(TOP)/configure/RELEASE --include $(TOP)/configure/RELEASE.$(EPICS_HOST_ARCH).Common -ifdef T_A --include $(TOP)/configure/RELEASE.Common.$(T_A) --include $(TOP)/configure/RELEASE.$(EPICS_HOST_ARCH).$(T_A) -endif - -CONFIG = $(RULES)/configure -include $(CONFIG)/CONFIG --include $(CONFIG)/CONFIG.Dls - -# Override the Base definition: -INSTALL_LOCATION = $(TOP) - -# CONFIG_SITE files contain other build configuration settings -include $(TOP)/configure/CONFIG_SITE --include $(TOP)/configure/CONFIG_SITE.$(EPICS_HOST_ARCH).Common -ifdef T_A - -include $(TOP)/configure/CONFIG_SITE.Common.$(T_A) - -include $(TOP)/configure/CONFIG_SITE.$(EPICS_HOST_ARCH).$(T_A) -endif - diff --git a/configure/CONFIG_SITE b/configure/CONFIG_SITE deleted file mode 100644 index 364df815..00000000 --- a/configure/CONFIG_SITE +++ /dev/null @@ -1,20 +0,0 @@ -# CONFIG_SITE - -# Make any application-specific changes to the EPICS build -# configuration variables in this file. -# -# Host/target specific settings can be specified in files named -# CONFIG_SITE.$(EPICS_HOST_ARCH).Common -# CONFIG_SITE.Common.$(T_A) -# CONFIG_SITE.$(EPICS_HOST_ARCH).$(T_A) - -# Native build only for this application -CROSS_COMPILER_TARGET_ARCHS = - -# PYTHON = /scratch/local/prefix/python3/bin/python3 -PYTHON = dls-python -# PYTHON = python - -# Definition of python-config application. This depends on PYTHON defined in -# one of the host specific PYTHON_CONFIG files. -PYTHON_CONFIG = $(PYTHON)-config diff --git a/configure/Makefile b/configure/Makefile deleted file mode 100644 index 92543094..00000000 --- a/configure/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -TOP=.. - -include $(TOP)/configure/CONFIG - -TARGETS = $(CONFIG_TARGETS) -CONFIGS += $(subst ../,,$(wildcard $(CONFIG_INSTALLS))) - -include $(TOP)/configure/RULES diff --git a/configure/RELEASE b/configure/RELEASE deleted file mode 100644 index 16497f20..00000000 --- a/configure/RELEASE +++ /dev/null @@ -1,27 +0,0 @@ -# RELEASE - Location of external support modules -# -# IF YOU MAKE ANY CHANGES to this file you must subsequently -# do a "gnumake rebuild" in this application's top level -# directory. -# -# The build process does not check dependencies against files -# that are outside this application, thus you should do a -# "gnumake rebuild" in the top level directory after EPICS_BASE -# or any other external module pointed to below is rebuilt. -# -# Host- or target-specific settings can be given in files named -# RELEASE.$(EPICS_HOST_ARCH).Common -# RELEASE.Common.$(T_A) -# RELEASE.$(EPICS_HOST_ARCH).$(T_A) -# -# This file should ONLY define paths to other support modules, -# or include statements that pull in similar RELEASE files. -# Build settings that are NOT module paths should appear in a -# CONFIG_SITE file. - -# Note that DEVIOCSTATS is entirely optional. If not required, just leave this -# symbol undefined. -DEVIOCSTATS = /dls_sw/prod/R3.14.12.7/support/devIocStats/3-1-14dls3-0 - -# EPICS_BASE usually appears last so other apps can override stuff: -EPICS_BASE=/dls_sw/epics/R3.14.12.7/base diff --git a/configure/RULES b/configure/RULES deleted file mode 100644 index 3425740d..00000000 --- a/configure/RULES +++ /dev/null @@ -1,7 +0,0 @@ -# RULES - --include $(CONFIG)/RULES.Dls -include $(CONFIG)/RULES - -# Library should be rebuilt because LIBOBJS may have changed. -$(LIBNAME): ../Makefile diff --git a/configure/RULES.ioc b/configure/RULES.ioc deleted file mode 100644 index 901987c6..00000000 --- a/configure/RULES.ioc +++ /dev/null @@ -1,2 +0,0 @@ -#RULES.ioc -include $(CONFIG)/RULES.ioc diff --git a/configure/RULES_DIRS b/configure/RULES_DIRS deleted file mode 100644 index 3ba269dc..00000000 --- a/configure/RULES_DIRS +++ /dev/null @@ -1,2 +0,0 @@ -#RULES_DIRS -include $(CONFIG)/RULES_DIRS diff --git a/configure/RULES_TOP b/configure/RULES_TOP deleted file mode 100644 index d09d668d..00000000 --- a/configure/RULES_TOP +++ /dev/null @@ -1,3 +0,0 @@ -#RULES_TOP -include $(CONFIG)/RULES_TOP - diff --git a/example/runtest b/example/runtest deleted file mode 100755 index b4b5bcf4..00000000 --- a/example/runtest +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -cd "$(dirname "$0")" -../pythonIoc startup.py "$@" diff --git a/example/versions.py b/example/versions.py deleted file mode 100644 index c1a47f67..00000000 --- a/example/versions.py +++ /dev/null @@ -1,8 +0,0 @@ -'''Version definitions for softIOC. This is normally the first module -imported, and should only be used to establish module versions.''' - -from pkg_resources import require - -require('numpy==1.11.1') -require('cothread==2.14') -require('epicsdbbuilder==1.2') diff --git a/python/Makefile b/python/Makefile deleted file mode 100644 index 26940bc3..00000000 --- a/python/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -TOP = .. -include $(TOP)/configure/CONFIG - -# The only thing we need to do is deploy the .dbd in its proper place. -DBD += device.dbd - -include $(TOP)/configure/RULES diff --git a/softIocApp/Makefile b/softIocApp/Makefile deleted file mode 100644 index 8babbc53..00000000 --- a/softIocApp/Makefile +++ /dev/null @@ -1,81 +0,0 @@ -########################################################################## -# Copyright (c) 2003 The University of Chicago, as Operator of Argonne -# National Laboratory. -# Copyright (c) 2003 The Regents of the University of California, as -# Operator of Los Alamos National Laboratory. -# EPICS BASE Versions 3.13.7 and higher are distributed subject to the -# Software License Agreement found in the file LICENSE that is included -# with this distribution. -########################################################################## - -TOP=.. - -include $(TOP)/configure/CONFIG - - -# Discover the correct name for the python library and its location. This is -# surprisingly tricky, as we want to support quite a number of different -# configurations of Python interpreter. -PYTHON_LIB := $(shell $(PYTHON) -c \ - 'from distutils import sysconfig; \ - print(sysconfig.get_config_var("LIBRARY")[3:-2])') -PYTHON_LIB_DIR := $(shell $(PYTHON) -c \ - 'from distutils import sysconfig; \ - print(sysconfig.get_config_var("LIBDIR"))') - - -USR_CFLAGS += -std=gnu99 -Werror - -# We just want the include path for the python library, that should be enough. -# There are a lot of other settings which may conflict with EPICS build -# settings, so we omit them. -USR_CFLAGS += $(shell $(PYTHON_CONFIG) --cflags | tr ' ' '\n' | grep '^-I') - -# This tells the compiler to ignore errors generated by EPICS includes. We need -# this because the EPICS headers have non strict prototypes in places. -USR_CPPFLAGS += -isystem $(EPICS_BASE)/include - - -PROD_IOC_DEFAULT = softIoc -PROD_IOC_vxWorks = -nil- - -DBD += softIoc.dbd -softIoc_DBD += base.dbd - -USR_CFLAGS += -DEPICS_BASE='"$(shell cd "$(EPICS_BASE)" && pwd)"' - -softIoc_SRCS += softIoc_registerRecordDeviceDriver.cpp -softIoc_SRCS += softMain.c - -softIoc_LIBS += $(EPICS_BASE_IOC_LIBS) -softIoc_LIBS += PythonSupport -softIoc_LIBS += $(PYTHON_LIB) - -softIoc_LDFLAGS += $(shell $(PYTHON_CONFIG) --ldflags) -# For some reason python-config --ldflags doesn't actually tell us where to find -# the python library if we need telling, so this has to be computed here. -$(PYTHON_LIB)_DIR = $(PYTHON_LIB_DIR) - -# Support for devIocStats -ifdef DEVIOCSTATS -softIoc_DBD += devIocStats.dbd -softIoc_LIBS += devIocStats -DB += $(DEVIOCSTATS)/db/ioc.db -endif - - -# ---------------------------------------------------------------------- -# Support library for python component, dynamically loaded to assist with -# access to record fields. - - -LIBRARY_IOC += PythonSupport - -# The following are compiled and added to the support library -PythonSupport_SRCS += PythonSupport.c - -PythonSupport_LIBS += $(EPICS_BASE_IOC_LIBS) - - -# include $(TOP)/configure/RULES_TOP -include $(TOP)/configure/RULES diff --git a/softIocApp/softMain.c b/softIocApp/softMain.c deleted file mode 100644 index c5e20a30..00000000 --- a/softIocApp/softMain.c +++ /dev/null @@ -1,97 +0,0 @@ -/* Soft IOC initialisation. - * - * This is really pretty well as simple as possible: all real work is - * delegated to the invoked Python script. */ - -/* pyconfig.h (included from Python.h) redefines _POSIX_C_SOURCE and - * _XOPEN_SOURCE, and in ways which conflict with the definitions provided by - * EPICS. To avoid messages, we let Python.h have its way here. - * Note also that Python recommends that Python.h be included first. */ -#undef _POSIX_C_SOURCE -#undef _XOPEN_SOURCE -#include - -#include - -#include -#include -#include -#include -#include - -#include "dbAccess.h" -#include "iocInit.h" - - -/* The global IOC registration function is automatically constructed by the - * EPICS IOC build process, and this call completes the registration - * process. - * Note that although this is declared to return a value, in fact it - * unconditionally returns 0, so it might as well be a void! */ -extern int softIoc_registerRecordDeviceDriver(struct dbBase *pdbbase); - - - -/* Loads the global IOC dbd definitions and registers them. */ - -static bool LoadAndRegisterDbd(void) -{ - const char *here = getenv("HERE"); - if (here == NULL) - { - fprintf(stderr, "Environment variable HERE must be defined\n"); - return false; - } - char softIoc_dbd[PATH_MAX]; - bool Ok = snprintf( - softIoc_dbd, PATH_MAX, "%s/dbd/softIoc.dbd", here) < PATH_MAX; - if (!Ok) - { - fprintf(stderr, "Path to dbd too long.\n"); - return false; - } - - int status = dbLoadDatabase(softIoc_dbd, NULL, NULL); - if (status != 0) - fprintf(stderr, - "Error (%d) loading dbd file \"%s\"\n", status, softIoc_dbd); - if (status == 0) - softIoc_registerRecordDeviceDriver(pdbbase); - return status == 0; -} - - -int main(int argc, char *argv[]) -{ -#if PY_MAJOR_VERSION == 2 - char **python_argv = argv; - -#else - /* Alas, for Python3 we need convert argv from char** to wchar_t**. */ - wchar_t **python_argv = PyMem_Malloc(sizeof(wchar_t *) * (argc + 1)); - python_argv[argc] = NULL; - -#if PY_MINOR_VERSION < 5 - /* This is a tricky space: we're supposed to use Py_DecodeLocale(), but - * these versions of Python3 don't implement it yet. Do the simplest - * workaround we can. This code is lifted from Python 3.4 Modules/python.c - * and simplified as much as possible. */ - char *oldloc = strdup(setlocale(LC_ALL, NULL)); - setlocale(LC_ALL, ""); - for (int i = 0; i < argc; i ++) - python_argv[i] = _Py_char2wchar(argv[i], NULL); - setlocale(LC_ALL, oldloc); - free(oldloc); - -#else - /* This seems to be the "correct" Python 3 way. */ - for (int i = 0; i < argc; i ++) - python_argv[i] = Py_DecodeLocale(argv[i], NULL); -#endif -#endif - - if (LoadAndRegisterDbd()) - return Py_Main(argc, python_argv); - else - return 3; -} diff --git a/python/softioc/README b/softioc/README similarity index 100% rename from python/softioc/README rename to softioc/README diff --git a/python/softioc/__init__.py b/softioc/__init__.py similarity index 100% rename from python/softioc/__init__.py rename to softioc/__init__.py diff --git a/python/access.acf b/softioc/access.acf similarity index 100% rename from python/access.acf rename to softioc/access.acf diff --git a/python/softioc/alarm.py b/softioc/alarm.py similarity index 100% rename from python/softioc/alarm.py rename to softioc/alarm.py diff --git a/python/softioc/builder.py b/softioc/builder.py similarity index 100% rename from python/softioc/builder.py rename to softioc/builder.py diff --git a/python/device.dbd b/softioc/device.dbd similarity index 100% rename from python/device.dbd rename to softioc/device.dbd diff --git a/python/softioc/device.py b/softioc/device.py similarity index 100% rename from python/softioc/device.py rename to softioc/device.py diff --git a/python/softioc/device_core.py b/softioc/device_core.py similarity index 100% rename from python/softioc/device_core.py rename to softioc/device_core.py diff --git a/softIocApp/PythonSupport.c b/softioc/extension.c similarity index 100% rename from softIocApp/PythonSupport.c rename to softioc/extension.c diff --git a/python/softioc/fields.py b/softioc/fields.py similarity index 100% rename from python/softioc/fields.py rename to softioc/fields.py diff --git a/python/softioc/imports.py b/softioc/imports.py similarity index 100% rename from python/softioc/imports.py rename to softioc/imports.py diff --git a/python/softioc/pvlog.py b/softioc/pvlog.py similarity index 100% rename from python/softioc/pvlog.py rename to softioc/pvlog.py diff --git a/python/softioc/pythonSoftIoc.py b/softioc/pythonSoftIoc.py similarity index 100% rename from python/softioc/pythonSoftIoc.py rename to softioc/pythonSoftIoc.py diff --git a/python/softioc/softioc.py b/softioc/softioc.py similarity index 100% rename from python/softioc/softioc.py rename to softioc/softioc.py diff --git a/example/graph.edl b/tests/graph.edl similarity index 100% rename from example/graph.edl rename to tests/graph.edl diff --git a/example/testing.py b/tests/sim_records.py similarity index 100% rename from example/testing.py rename to tests/sim_records.py diff --git a/example/createdb.py b/tests/test_createdb.py similarity index 100% rename from example/createdb.py rename to tests/test_createdb.py diff --git a/example/startup.py b/tests/test_ioc.py similarity index 100% rename from example/startup.py rename to tests/test_ioc.py From b6857651bfeb47f8f3c5f137d8e71d0bc6d339e5 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Thu, 25 Feb 2021 14:20:09 +0000 Subject: [PATCH 02/19] Convert to use epicscorelibs --- .gitattributes | 1 + .gitignore | 20 +- .gitmodules | 3 + Pipfile | 22 ++ Pipfile.lock | 491 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 3 + pythonIoc.in | 41 ---- setup.cfg | 50 ++++ setup.py | 95 ++++++++ softioc/__init__.py | 2 + softioc/__main__.py | 25 ++ softioc/_version_git.py | 97 ++++++++ softioc/builder.py | 16 +- softioc/device.py | 8 +- softioc/extension.c | 124 ++++++---- softioc/fields.py | 37 +-- softioc/imports.py | 88 ++----- softioc/iocStats | 1 + softioc/pvlog.py | 5 +- softioc/softioc.py | 6 +- tests/test_ioc.py | 4 +- 21 files changed, 937 insertions(+), 202 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitmodules create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 pyproject.toml delete mode 100644 pythonIoc.in create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 softioc/__main__.py create mode 100644 softioc/_version_git.py create mode 160000 softioc/iocStats diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..41a13933 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*/_version_git.py export-subst diff --git a/.gitignore b/.gitignore index 390de251..185e73b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,17 @@ -/bin -/dbd -/lib -/db -/docs/html -/pythonIoc +/.vscode +/venv *.pyc -O.* +/docs/html/ +/softioc/_extension.* + +# Dist build output +/build +/dist +/softioc.egg-info/ + +# Coverage reports +.coverage +cov.xml /docs/papers/*/*.aux /docs/papers/*/*.pdf diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..4c41b1de --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "iocStats"] + path = softioc/iocStats + url = https://github.com/epics-modules/iocStats.git diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..77850d53 --- /dev/null +++ b/Pipfile @@ -0,0 +1,22 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +pytest-cov = "*" +pytest-flake8 = "*" +sphinx-rtd-theme = "*" +# switch to main repo after PR https://github.com/Holzhaus/sphinx-multiversion/pull/60 is merged +sphinx-multiversion = {editable = true,git = "https://github.com/dls-controls/sphinx-multiversion.git",ref = "only-arg"} +setuptools-dso = "*" + +[packages] +# All other package requirements from setup.py +softioc = {editable = true,path = "."} + +[scripts] +# Put coverage here so we don't interfere with debugging in the IDE +tests = "python -m pytest --cov=epicsdbbuilder --cov-report term" +docs = "sphinx-build -EWT --keep-going docs build/html" +clean = "rm -rf build prefix */__pycache__ .coverage cov.xml *.egg-info .mypy_cache .pytest_cache" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..e9f0a69f --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,491 @@ +{ + "_meta": { + "hash": { + "sha256": "facfc60cd8deb78e8b7b88e3c72257aaa7c5faf1db8d612961fa1073173ef2dd" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "epicscorelibs": { + "hashes": [ + "sha256:1a131556d13775f820db2e474a117e47b0d743ef262f0ebb58207d0bd7a2c6be", + "sha256:1b235379d23bb2f5746a791cf4abc982d0252e84d8d6f1ce52f3d028cf1c02c2", + "sha256:25a1f8f6cf97bee3513bcc81f17eb6acbe8ceb6a9d00d2bb713989cf120f5f9a", + "sha256:304cf11a9b05b20ba925564ff55ac524a80365cb27c0cc8df7f902345893c783", + "sha256:3221c993009ffd893b13d136e4c31e3dcf2e7b34dc3321e583534185e521c0b5", + "sha256:328276f0aa4dcf367c3c96d0aa7bc7f89b845d7c3c5ac2bd0eacd960c880006c", + "sha256:37c0f6889cebd2d8138d457d4a884f96854e9b43eafc8deaf20161d6ca86f733", + "sha256:39bdfb7b931dceecb8dff3fb6ed9689aad542ff1b4f4f48fd89cb71bb565f14c", + "sha256:46a4e6f1c26eb460d56a55f93230185555cb7e9874859996fc8828515616673e", + "sha256:4dafa4f28ce858338bb04398da4a67014b3f6522c609ed9c3c9317ad59cc4045", + "sha256:55cdeaa11292db310273c1e408d09030c1a71b27237818ea2cd22b6e03859ba6", + "sha256:56ba9513d9e44b19992e4a7e975c059b669d04f705e72e2967cc71c02c5b7a5b", + "sha256:5769b8aea4427513c3fd9e8dd08612bdaa94e2ee4975ae9436a8cb6f633e3ecb", + "sha256:5a72ef5e9aa2a467dbc31fb1a6bfcac44eeb2d50802991061a130773565a67f0", + "sha256:5f165e984c123164d58afeb0d8ca8cfddb20bc3603d21837f7232afc1cff0471", + "sha256:6b8e63342c2d0778d81735fd7bba0467778ddf0d2997a7ee30308b0322ed7e88", + "sha256:6e111d499db1a496bf3331db781894d2da301b1439e32aa464bfd30bf1bae192", + "sha256:8afa93e18d26e25168cda51e5aa72054c35d50029f7057d5ba3c9108fdb2751f", + "sha256:9b4be973536644aab49e67a42c41f4922f32b7106cde8f360b9ffe198f971c1a", + "sha256:a28f9c45b2c4a273ac9735245145b80247908c9aceab3f01550756023d1c61fd", + "sha256:a5e4bc0722c48e872f0fc5752b514fe9167ab1dce6cfdad79687f8d2a130d0c1", + "sha256:db59c3bf4fda65eb49ad5ab4933aa182c764200c8ed57d8e5474d7accbbcb609", + "sha256:e6dd41d204565bccc7126eb43a19187c6ff4a2c828bf2a79161cb4cef19cbef9", + "sha256:f7360c05368df5036d87b48a8b97c1d45970dfe4fca9f853b862a0609f2cf093", + "sha256:fa30a31d60173a83c185caf9e77c2666f3f20dd9b7c6ffd3aa49ef511199f258" + ], + "markers": "python_version >= '2.7'", + "version": "==7.0.4.99.0.0a1" + }, + "epicsdbbuilder": { + "hashes": [ + "sha256:0c3bb0ae55d618474880c1004218a3d3ac640235d04426df6622b75229c563fc", + "sha256:be4c37aead8f2bb5500ef3e7693eee97ecd4fb3c1090324689062d0505de3325" + ], + "version": "==1.4" + }, + "numpy": { + "hashes": [ + "sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29", + "sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab", + "sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04", + "sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d", + "sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590", + "sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f", + "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a", + "sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845", + "sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090", + "sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b", + "sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e", + "sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc", + "sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257", + "sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e", + "sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff", + "sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9", + "sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b", + "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e", + "sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0", + "sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb", + "sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f", + "sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2", + "sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14", + "sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9" + ], + "markers": "python_version >= '3.7'", + "version": "==1.20.1" + }, + "softioc": { + "editable": true, + "path": "." + } + }, + "develop": { + "alabaster": { + "hashes": [ + "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", + "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02" + ], + "version": "==0.7.12" + }, + "attrs": { + "hashes": [ + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" + }, + "babel": { + "hashes": [ + "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", + "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0" + }, + "certifi": { + "hashes": [ + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + ], + "version": "==2020.12.5" + }, + "chardet": { + "hashes": [ + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" + }, + "coverage": { + "hashes": [ + "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7", + "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5", + "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f", + "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde", + "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f", + "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f", + "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c", + "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66", + "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90", + "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337", + "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d", + "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4", + "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409", + "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37", + "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1", + "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247", + "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39", + "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c", + "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994", + "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c", + "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb", + "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc", + "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f", + "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca", + "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135", + "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3", + "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339", + "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9", + "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9", + "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af", + "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370", + "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19", + "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3", + "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44", + "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3", + "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a", + "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c", + "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b", + "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9", + "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8", + "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22", + "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f", + "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345", + "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880", + "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0", + "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b", + "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec", + "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3", + "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==5.4" + }, + "docutils": { + "hashes": [ + "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", + "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.16" + }, + "flake8": { + "hashes": [ + "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", + "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==3.8.4" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "imagesize": { + "hashes": [ + "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", + "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.2.0" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "jinja2": { + "hashes": [ + "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", + "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.11.3" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", + "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", + "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", + "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", + "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", + "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", + "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", + "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", + "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "packaging": { + "hashes": [ + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.9" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", + "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.6.0" + }, + "pyflakes": { + "hashes": [ + "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", + "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.2.0" + }, + "pygments": { + "hashes": [ + "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", + "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" + ], + "markers": "python_version >= '3.5'", + "version": "==2.8.0" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", + "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" + ], + "markers": "python_version >= '3.6'", + "version": "==6.2.2" + }, + "pytest-cov": { + "hashes": [ + "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", + "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" + ], + "index": "pypi", + "version": "==2.11.1" + }, + "pytest-flake8": { + "hashes": [ + "sha256:c28cf23e7d359753c896745fd4ba859495d02e16c84bac36caa8b1eec58f5bc1", + "sha256:f0259761a903563f33d6f099914afef339c085085e643bee8343eb323b32dd6b" + ], + "index": "pypi", + "version": "==1.0.7" + }, + "pytz": { + "hashes": [ + "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", + "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + ], + "version": "==2021.1" + }, + "requests": { + "hashes": [ + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.25.1" + }, + "setuptools-dso": { + "hashes": [ + "sha256:93a2faddf76b447c0aa58d42ccb22dbc7d6ce46d6e0b32dfacc143c6acdc016c" + ], + "index": "pypi", + "version": "==1.7" + }, + "snowballstemmer": { + "hashes": [ + "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", + "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" + ], + "version": "==2.1.0" + }, + "sphinx": { + "hashes": [ + "sha256:11d521e787d9372c289472513d807277caafb1684b33eb4f08f7574c405893a9", + "sha256:e90161222e4d80ce5fc811ace7c6787a226b4f5951545f7f42acf97277bfc35c" + ], + "markers": "python_version >= '3.5'", + "version": "==3.5.1" + }, + "sphinx-multiversion": { + "editable": true, + "git": "https://github.com/dls-controls/sphinx-multiversion.git", + "ref": "3e72beb7f8f0a76b53722fb2eb3daeed3d2a3d31" + }, + "sphinx-rtd-theme": { + "hashes": [ + "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5", + "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113" + ], + "index": "pypi", + "version": "==0.5.1" + }, + "sphinxcontrib-applehelp": { + "hashes": [ + "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", + "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.2" + }, + "sphinxcontrib-devhelp": { + "hashes": [ + "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", + "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.2" + }, + "sphinxcontrib-htmlhelp": { + "hashes": [ + "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", + "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.3" + }, + "sphinxcontrib-jsmath": { + "hashes": [ + "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", + "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.1" + }, + "sphinxcontrib-qthelp": { + "hashes": [ + "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", + "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.3" + }, + "sphinxcontrib-serializinghtml": { + "hashes": [ + "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", + "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" + ], + "markers": "python_version >= '3.5'", + "version": "==1.1.4" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, + "urllib3": { + "hashes": [ + "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", + "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.3" + } + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..6536485d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel", "setuptools_dso", "epicscorelibs>=7.0.4.99.0.0a1"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/pythonIoc.in b/pythonIoc.in deleted file mode 100644 index d0a3ac31..00000000 --- a/pythonIoc.in +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# Wrapper for invoking the Python softIoc. This IOC needs to be called with a -# complete path and with PYTHONPATH set up correctly. It's easier to -# configure this here in shell script than in the program itself! - -export HERE="$(readlink -fn "$(dirname "$0")")" -export PYTHONPATH="$PYTHONPATH${PYTHONPATH:+:}$HERE/python" -export EPICS_BASE='@@EPICS_BASE@@' -export EPICS_HOST_ARCH='@@EPICS_HOST_ARCH@@' -export PYTHONHOME='@@PYTHONHOME@@' - -case "$1" in - --debug) - # Run under gdb - shift - TEMP="$(mktemp)" - trap 'rm -f "$TEMP"' EXIT - echo run "$@" >>"$TEMP" - gdb -x "$TEMP" "$HERE/bin/$EPICS_HOST_ARCH/softIoc" - ;; - - --valgrind) - # Run under valgrind with Python suppression file. - shift - PYTHON_SRC=/dls_sw/prod/tools/RHEL5/src/Python-2.6.4 - SUPP="$PYTHON_SRC"/Misc/valgrind-python.supp - TEMP=$(mktemp) - trap 'rm -f "$TEMP"' EXIT - # Hack up the suppression file. Unfortunately this only works with the - # one particular version of the file (line numbers!) - sed '127,161{/^###/s///}' "$SUPP" >"$TEMP" - valgrind --tool=memcheck --suppressions="$TEMP" \ - "$HERE/bin/$EPICS_HOST_ARCH/softIoc" "$@" - ;; - - *) - # Normal operation - exec "$HERE/bin/$EPICS_HOST_ARCH/softIoc" "$@" - ;; -esac diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..d906dcc8 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,50 @@ +[metadata] +name = softioc +description = EPICS IOC with Python Device Support +url = https://github.com/dls-controls/pythonIoc +author = Michael Abbott +author_email = Michael.Abbott@diamond.ac.uk +license = Apache License 2.0 +long_description = file: README.rst +long_description_content_type = text/x-rst +classifiers = + Development Status :: 5 - Production/Stable + License :: OSI Approved :: Apache Software License + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + +[options] +packages = find: + +[options.packages.find] +# Don't include our test directory in the distribution +exclude = tests + +[options.entry_points] +# Include a command line script +console_scripts = + pythonIoc = softioc.__main__:main + +[options.package_data] +softioc = + softioc/access.acf + softioc/device.dbd + softioc/iocStats/devIocStats/src/devIocStats.dbd + softioc/iocStats/iocAdmin/Db/*.template + +[flake8] +max-line-length = 80 +extend-ignore = + F401 F403 F405 # Allow from module import * + E251 # Allow call(param = value) + E301 E302 E303 E305 # Allow any number of blank lines + +[tool:pytest] +# Run pytest with all our checkers, and don't spam us with massive tracebacks on error +addopts = --tb=native -vv --flake8 --doctest-modules + +[coverage:run] +# This is covered in the versiongit test suite so exclude it here +omit = */_version_git.py \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..baad602a --- /dev/null +++ b/setup.py @@ -0,0 +1,95 @@ +import os +import sys + +from setuptools.command.develop import develop +import epicscorelibs.path +import epicscorelibs.version +from setuptools_dso import DSO, Extension, setup +from epicscorelibs.config import get_config_var + +# Place the directory containing _version_git on the path +for path, _, filenames in os.walk(os.path.dirname(os.path.abspath(__file__))): + if "_version_git.py" in filenames: + sys.path.append(path) + break + +from _version_git import __version__, get_cmdclass # noqa + +devIocStats_OSI = [ + "devIocStatsAnalog.c", + "devIocStatsString.c", + "devIocStatsWaveform.c", + "devIocStatsSub.c", + "devIocStatsTest.c" +] + +devIocStats_OSD = [ + "osdCpuUsage.c", + "osdCpuUtilization.c", + "osdFdUsage.c", + "osdMemUsage.c", + "osdWorkspaceUsage.c", + "osdClustInfo.c", + "osdSuspTasks.c", + "osdIFErrors.c", + "osdBootInfo.c", + "osdSystemInfo.c", + "osdHostInfo.c", + "osdPIDInfo.c", +] + +devIocStats_srcs = [] +devIocStats_src = os.path.join("softioc", "iocStats", "devIocStats") +devIocStats_default = os.path.join(devIocStats_src, "os", "default") +devIocStats_os = os.path.join(devIocStats_src, "os", get_config_var('OS_CLASS')) + +for f in devIocStats_OSI: + devIocStats_srcs.append(os.path.join(devIocStats_src, f)) +for f in devIocStats_OSD: + if os.path.exists(os.path.join(devIocStats_os, f)): + devIocStats_srcs.append(os.path.join(devIocStats_os, f)) + else: + devIocStats_srcs.append(os.path.join(devIocStats_default, f)) + +#dso = DSO( +# 'softioc.lib.devIocStats', +# devIocStats_srcs, +# include_dirs=[epicscorelibs.path.include_path, devIocStats_src, devIocStats_os, devIocStats_default], +# dsos=['epicscorelibs.lib.Com'] +#) + +# Extension with all our C code +ext = Extension( + name='softioc._extension', + sources = ['softioc/extension.c'] + devIocStats_srcs, + include_dirs=[epicscorelibs.path.include_path, devIocStats_src, devIocStats_os, devIocStats_default], + dsos = ['epicscorelibs.lib.dbCore', 'epicscorelibs.lib.Com'], + define_macros = get_config_var('CPPFLAGS'), + extra_compile_args = get_config_var('CXXFLAGS'), + extra_link_args = get_config_var('LDFLAGS'), +) + +# Add custom develop to add soft link to epicscorelibs in . +class Develop(develop): + def install_for_development(self): + develop.install_for_development(self) + # Make a link here to epicscorelibs so `pip install -e .` works + # If we don't do this dbCore can't be found when _extension is + # built into . + link = os.path.join(self.egg_path, "epicscorelibs") + if not os.path.exists(link): + os.symlink(os.path.join(self.install_dir, "epicscorelibs"), link) + +setup( + cmdclass=dict(develop=Develop, **get_cmdclass()), + version=__version__, + ext_modules = [ext], + install_requires = [ + "epicscorelibs==7.0.4.99.0.0a1", + #epicscorelibs.version.abi_requires(), + "numpy>=1.18", + "epicsdbbuilder>=1.4" + ], + #x_dsos = [dso], + zip_safe = False, # setuptools_dso is not compatible with eggs! +) diff --git a/softioc/__init__.py b/softioc/__init__.py index 7b9a7500..bb8d9bf2 100644 --- a/softioc/__init__.py +++ b/softioc/__init__.py @@ -1 +1,3 @@ '''Python soft IOC module.''' + +from ._version_git import __version__ diff --git a/softioc/__main__.py b/softioc/__main__.py new file mode 100644 index 00000000..e15de04a --- /dev/null +++ b/softioc/__main__.py @@ -0,0 +1,25 @@ +import os +import sys +from argparse import ArgumentParser + +from softioc import __version__ + + +def main(args=None): + parser = ArgumentParser() + parser.add_argument("--version", action="version", version=__version__) + parser.add_argument("script", help="The python script to run") + parsed_args = parser.parse_args(args) + # Insert the directory containing script onto the path in case we do + # any imports + sys.path.insert(0, os.path.dirname(os.path.abspath(parsed_args.script))) + if sys.version_info < (3, 0): + # Python 2 + execfile(parsed_args.script) + else: + # Python 3 + exec(open(parsed_args.script).read()) + + +if __name__ == "__main__": + main() diff --git a/softioc/_version_git.py b/softioc/_version_git.py new file mode 100644 index 00000000..ec811d1b --- /dev/null +++ b/softioc/_version_git.py @@ -0,0 +1,97 @@ +# Compute a version number from a git repo or archive + +# This file is released into the public domain. Generated by: +# versiongit-1.0 (https://github.com/dls-controls/versiongit) +import os +import re +import sys +from subprocess import STDOUT, CalledProcessError, check_output + +# These will be filled in if git archive is run or by setup.py cmdclasses +GIT_REFS = "$Format:%D$" +GIT_SHA1 = "$Format:%h$" + +# Git describe gives us sha1, last version-like tag, and commits since then +CMD = "git describe --tags --dirty --always --long --match=[0-9]*[-.][0-9]*" + + +def get_version_from_git(path=None): + """Try to parse version from git describe, fallback to git archive tags""" + tag, plus, suffix = "0.0", "untagged", "" + if not GIT_SHA1.startswith("$"): + # git archive or the cmdclasses below have filled in these strings + sha1 = GIT_SHA1 + for ref_name in GIT_REFS.split(", "): + if ref_name.startswith("tag: "): + # git from 1.8.3 onwards labels archive tags "tag: TAGNAME" + tag, plus = ref_name[5:], "0" + else: + if path is None: + # If no path to git repo, choose the directory this file is in + path = os.path.dirname(os.path.abspath(__file__)) + # output is TAG-NUM-gHEX[-dirty] or HEX[-dirty] + try: + cmd_out = check_output(CMD.split(), stderr=STDOUT, cwd=path) + except Exception as e: + sys.stderr.write("%s: %s\n" % (type(e).__name__, str(e))) + if isinstance(e, CalledProcessError): + sys.stderr.write("-> %s" % e.output.decode()) + return "0.0+unknown", None, e + else: + out = cmd_out.decode().strip() + if out.endswith("-dirty"): + out = out[:-6] + suffix = ".dirty" + if "-" in out: + # There is a tag, extract it and the other pieces + match = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", out) + tag, plus, sha1 = match.groups() + else: + # No tag, just sha1 + sha1 = out + # Replace dashes in tag for dots + tag = tag.replace("-", ".") + if plus != "0" or suffix: + # Not on a tag, add additional info + tag = "%(tag)s+%(plus)s.g%(sha1)s%(suffix)s" % locals() + return tag, sha1, None + + +__version__, git_sha1, git_error = get_version_from_git() + + +def get_cmdclass(build_py=None, sdist=None): + """Create cmdclass dict to pass to setuptools.setup that will write a + _version_static.py file in our resultant sdist, wheel or egg""" + if build_py is None: + from setuptools.command.build_py import build_py + if sdist is None: + from setuptools.command.sdist import sdist + + def make_version_static(base_dir, pkg): + vg = os.path.join(base_dir, pkg.split(".")[0], "_version_git.py") + if os.path.isfile(vg): + lines = open(vg).readlines() + with open(vg, "w") as f: + for line in lines: + # Replace GIT_* with static versions + if line.startswith("GIT_SHA1 = "): + f.write("GIT_SHA1 = '%s'\n" % git_sha1) + elif line.startswith("GIT_REFS = "): + f.write("GIT_REFS = 'tag: %s'\n" % __version__) + else: + f.write(line) + + class BuildPy(build_py): + def run(self): + build_py.run(self) + for pkg in self.packages: + make_version_static(self.build_lib, pkg) + + class Sdist(sdist): + def make_release_tree(self, base_dir, files): + sdist.make_release_tree(self, base_dir, files) + for pkg in self.distribution.packages: + make_version_static(base_dir, pkg) + + return dict(build_py=BuildPy, sdist=Sdist) diff --git a/softioc/builder.py b/softioc/builder.py index 48fc1cd1..9aee2a8d 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -5,8 +5,20 @@ from .softioc import dbLoadDatabase from epicsdbbuilder import * -InitialiseDbd(os.environ['EPICS_BASE'], os.environ['EPICS_HOST_ARCH']) -LoadDbdFile(os.path.join(os.environ['HERE'], 'dbd/device.dbd')) +from epicscorelibs import path +from epicscorelibs.ioc import iocshRegisterCommon, registerRecordDeviceDriver, pdbbase + +iocshRegisterCommon() +dbLoadDatabase("base.dbd", os.path.join(path.base_path, "dbd")) + +dbd_dir = os.path.join(os.path.dirname(__file__), "iocStats", "devIocStats") +dbLoadDatabase("devIocStats.dbd", dbd_dir) + +if registerRecordDeviceDriver(pdbbase): + raise RuntimeError('Error registering') + +InitialiseDbd() +LoadDbdFile(os.path.join(os.path.dirname(__file__), 'device.dbd')) from . import pythonSoftIoc PythonDevice = pythonSoftIoc.PythonDevice() diff --git a/softioc/device.py b/softioc/device.py index db044108..d3f1d4ef 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -8,11 +8,9 @@ from . import alarm from .fields import DbfCodeToNumpy, DbrToDbfCode -from .imports import dbLoadDatabase, recGblResetAlarms +from .imports import dbLoadDatabase, recGblResetAlarms, db_put_field from .device_core import DeviceSupportCore, RecordLookup -from . import imports - class ProcessDeviceSupportCore(DeviceSupportCore, RecordLookup): '''Implements canonical default processing for records with a _process @@ -204,7 +202,7 @@ def set(self, value, process=True): else: datatype, length, data, array = self.value_to_dbr(value) self.__enable_write = process - imports.db_put_field( + db_put_field( _record.NAME, DbrToDbfCode[datatype], data, length) self.__enable_write = True @@ -352,4 +350,4 @@ class waveform_out(WaveformBase, ProcessDeviceSupportOut): # Ensure the .dbd file is loaded. -dbLoadDatabase(os.path.join(os.environ['HERE'], 'dbd/device.dbd'), None, None) +dbLoadDatabase("device.dbd", os.path.dirname(__file__), None) diff --git a/softioc/extension.c b/softioc/extension.c index 7a3dfb37..cbeb44c4 100644 --- a/softioc/extension.c +++ b/softioc/extension.c @@ -1,6 +1,5 @@ -/* See note in softMain.c about these #undefs. */ -#undef _POSIX_C_SOURCE -#undef _XOPEN_SOURCE + +#define PY_SSIZE_T_CLEAN #include #include @@ -10,23 +9,9 @@ #include #include #include - - -/* The interface to the caput event callback has changed as of EPICS 3.15, and - * we need to compile as appropriate. */ -#define BASE_3_15 (EPICS_VERSION * 100 + EPICS_REVISION >= 315) -#if BASE_3_15 #include -#endif - - - -/* Returns the EPICS_BASE path used to build this IOC. */ -const char *get_EPICS_BASE(void) -{ - return EPICS_BASE; // Passed as #define from Makefile -} - +#include +#include /* In Python3 this function has been renamed. */ #if PY_MAJOR_VERSION >= 3 @@ -40,9 +25,8 @@ const char *get_EPICS_BASE(void) /* Alas, EPICS has changed the numerical assignments of the DBF_ enums between * versions, so to avoid unpleasant surprises, we compute thes values here in C * and pass them back to the Python layer. */ -PyObject *get_DBF_values(void) +static PyObject *get_DBF_values(PyObject *self, PyObject *args) { - PyGILState_STATE gstate = PyGILState_Ensure(); PyObject *dict = PyDict_New(); ADD_ENUM(dict, DBF_STRING); ADD_ENUM(dict, DBF_CHAR); @@ -60,7 +44,6 @@ PyObject *get_DBF_values(void) ADD_ENUM(dict, DBF_OUTLINK); ADD_ENUM(dict, DBF_FWDLINK); ADD_ENUM(dict, DBF_NOACCESS); - PyGILState_Release(gstate); return dict; } @@ -68,11 +51,15 @@ PyObject *get_DBF_values(void) /* Given an array of field names, this routine looks up each field name in * the EPICS database and returns the corresponding field offset. */ -void get_field_offsets( - const char * record_type, const char * field_names[], int field_count, - short field_offset[], short field_size[], short field_type[]) +static PyObject *get_field_offsets(PyObject *self, PyObject *args) { int status; + const char *record_type; + PyObject *dict = PyDict_New(); + + if (!PyArg_ParseTuple(args, "s", &record_type)) + return NULL; + DBENTRY dbentry; dbInitEntry(pdbbase, &dbentry); @@ -85,33 +72,36 @@ void get_field_offsets( while (status == 0) { const char * field_name = dbGetFieldName(&dbentry); - int i; - for (i = 0; i < field_count; i ++) - { - if (strcmp(field_names[i], field_name) == 0) - { - field_offset[i] = dbentry.pflddes->offset; - field_size[i] = dbentry.pflddes->size; - field_type[i] = dbentry.pflddes->field_type; - } - } + PyObject *ost = Py_BuildValue("iii", + dbentry.pflddes->offset, + dbentry.pflddes->size, + dbentry.pflddes->field_type); + PyDict_SetItemString(dict, field_name, ost); status = dbNextField(&dbentry, 0); } dbFinishEntry(&dbentry); + return dict; } /* Updates PV field with integrated db lookup. Safer to do this in C as we need * an intermediate copy of the dbAddr structure, which changes size between * EPICS releases. */ -int db_put_field(const char *name, short dbrType, void *pbuffer, long length) +static PyObject *db_put_field(PyObject *self, PyObject *args) { + const char *name; + short dbrType; + void *pbuffer; + long length; + if (!PyArg_ParseTuple(args, "shnl", &name, &dbrType, &pbuffer, &length)) + return NULL; + struct dbAddr dbAddr; int rc = dbNameToAddr(name, &dbAddr); if (rc == 0) rc = dbPutField(&dbAddr, dbrType, pbuffer, length); - return rc; + return Py_BuildValue("i", rc); } @@ -178,12 +168,8 @@ static void PrintValue(struct formatted *formatted) void EpicsPvPutHook(struct asTrapWriteMessage *pmessage, int after) { -#if BASE_3_15 struct dbChannel *pchan = pmessage->serverSpecific; dbAddr *dbaddr = &pchan->addr; -#else - dbAddr *dbaddr = pmessage->serverSpecific; -#endif struct formatted *value = FormatValue(dbaddr); if (after) @@ -205,3 +191,59 @@ void EpicsPvPutHook(struct asTrapWriteMessage *pmessage, int after) /* Just save the old value for logging after. */ pmessage->userPvt = value; } + +static PyObject *install_pv_logging(PyObject *self, PyObject *args) +{ + const char *acf_file; + + if (!PyArg_ParseTuple(args, "s", &acf_file)) + return NULL; + + asSetFilename(acf_file); + asTrapWriteRegisterListener(EpicsPvPutHook); + Py_RETURN_NONE; +} + +static struct PyMethodDef softioc_methods[] = { + {"get_DBF_values", get_DBF_values, METH_VARARGS, + "Get a map of DBF names to values"}, + {"get_field_offsets", get_field_offsets, METH_VARARGS, + "Get offset, size and type for each record field"}, + {"db_put_field", db_put_field, METH_VARARGS, + "Put a database field to a value"}, + {"install_pv_logging", install_pv_logging, METH_VARARGS, + "Install caput logging to stdout"}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +#if PY_MAJOR_VERSION >= 3 +static struct PyModuleDef softioc_module = { + PyModuleDef_HEAD_INIT, + "softioc._extension", + NULL, + -1, + softioc_methods, +}; +#endif + +#if PY_MAJOR_VERSION >= 3 +# define PyMOD(NAME) PyObject* PyInit_##NAME (void) +#else +# define PyMOD(NAME) void init##NAME (void) +#endif + +PyMOD(_extension) +{ +#if PY_MAJOR_VERSION >= 3 + PyObject *mod = PyModule_Create(&softioc_module); +#else + PyObject *mod = Py_InitModule("softioc._extension", softioc_methods); +#endif + if(mod) { + } +#if PY_MAJOR_VERSION >= 3 + return mod; +#else + (void)mod; +#endif +} \ No newline at end of file diff --git a/softioc/fields.py b/softioc/fields.py index 6a598773..5a6dd74d 100644 --- a/softioc/fields.py +++ b/softioc/fields.py @@ -7,8 +7,8 @@ from .imports import get_field_offsets, get_DBF_values import numpy -from cothread.dbr import * -from cothread.dbr import ca_timestamp, EPICS_epoch +from epicscorelibs.ca.dbr import * +from epicscorelibs.ca.dbr import ca_timestamp, EPICS_epoch # Pick up the DBF_ definitions from the C helper layer. This is safer than @@ -82,35 +82,10 @@ class RecordFactory(object): def __init__(self, record_type, fields): '''Uses the EPICS static database to discover the offset in the record type and the size of each of the specified fields.''' - length = len(fields) - field_name_strings = [ - create_string_buffer(field.encode()) - for field in fields] - - field_names = (c_void_p * len(field_name_strings))() - for i, field in enumerate(field_name_strings): - field_names[i] = addressof(field) - - field_offsets = numpy.empty(length, dtype = numpy.int16) - field_sizes = numpy.zeros(length, dtype = numpy.int16) - field_types = numpy.empty(length, dtype = numpy.int16) - - get_field_offsets( - record_type, field_names, length, - field_offsets.ctypes.data_as(c_void_p), - field_sizes.ctypes.data_as(c_void_p), - field_types.ctypes.data_as(c_void_p)) - assert field_sizes.all(), 'At least one field seems to be missing' - - # The following rather convoluted expression converts the separate - # arrays of field names and attributes into a dictionary looking up - # each field and returning the appropriate list of attributes. - # One final adjustment needed is that all the numpy.int16 values - # need to be converted back into regular integers to ensure good - # processing downstream. - self.fields = dict(zip(fields, zip( - *map(lambda l: map(int, l), - (field_offsets, field_sizes, field_types))))) + self.fields = get_field_offsets(record_type) + missing = set(fields) - set(self.fields) + assert not missing, \ + "Fields not supported by %s: %s" % (record_type, sorted(missing)) def __call__(self, record): '''Converts a raw pointer to a record structure into a _Record object diff --git a/softioc/imports.py b/softioc/imports.py index c15a5e9a..74af7716 100644 --- a/softioc/imports.py +++ b/softioc/imports.py @@ -6,6 +6,26 @@ import sys from ctypes import * +from epicscorelibs import path + +from . import _extension + +# These are in the extension +def get_DBF_values(): + """Return {DBF_name: DBF_int_value} mapping""" + return _extension.get_DBF_values() + +def get_field_offsets(record_type): + """Return {field_name: (offset, size, field_type)}""" + return _extension.get_field_offsets(record_type) + +def db_put_field(name, dbr_type, pbuffer, length): + """Put field where pbuffer is void* pointer. Returns RC""" + return _extension.db_put_field(name, dbr_type, pbuffer, length) + +def install_pv_logging(acf_file): + """Install pv logging""" + _extension.install_pv_logging(acf_file) def expect_success(status, function, args): assert status == 0, 'Expected success' @@ -37,56 +57,9 @@ def auto_decode(result, func, args): return result.decode() -libPythonSupport = CDLL('libPythonSupport.so') - -# void get_field_offsets( -# const char * record_type, const char * field_names[], int field_count, -# short field_offset[], short field_size[], short field_type[]) -# -# Looks up field offset, size and type values for the given record type and -# the given list of field names. -get_field_offsets = libPythonSupport.get_field_offsets -get_field_offsets.argtypes = ( - auto_encode, c_void_p, c_int, c_void_p, c_void_p, c_void_p) -get_field_offsets.restype = None - -# int db_put_field(const char *name, short dbrType, void *pbuffer, long length) -# -# Updates value in given field through channel access, so notifications are -# generated as appropriate. -db_put_field = libPythonSupport.db_put_field -db_put_field.argtypes = (auto_encode, c_int, c_void_p, c_long) -db_put_field.errcheck = expect_success - - -# const char *get_EPICS_BASE(void) -# -# Returns the path to EPICS_BASE -get_EPICS_BASE = libPythonSupport.get_EPICS_BASE -get_EPICS_BASE.argtypes = () -get_EPICS_BASE.restype = c_char_p -get_EPICS_BASE.errcheck = auto_decode - - -# PyObject *get_DBF_values(void) -# -# Returns dictionary mapping DBF_ enum names to values -get_DBF_values = libPythonSupport.get_DBF_values -get_DBF_values.restype = py_object - - -# void EpicsPvPutHook(struct asTrapWriteMessage *pmessage, int after) -# -# Hook for logging EPICS caput events -EpicsPvPutHook = libPythonSupport.EpicsPvPutHook - - -EPICS_BASE = get_EPICS_BASE() -EPICS_HOST_ARCH = os.environ['EPICS_HOST_ARCH'] - def EpicsDll(dll): - return CDLL( - os.path.join(EPICS_BASE, 'lib', EPICS_HOST_ARCH, 'lib%s.so' % dll)) + return CDLL(path.get_lib(dll)) + # A bit tricky: in more recent versions of EPICS all the entry points we want # have been gathered into a single .so, but previously they were split among @@ -96,14 +69,11 @@ def EpicsDll(dll): libregistryIoc = libdbCore libdbIoc = libdbCore libmiscIoc = libdbCore - libasIoc = libdbCore except OSError: # Ok, no dbCore, then we should find everything in these four instead. libregistryIoc = EpicsDll('registryIoc') libdbIoc = EpicsDll('dbIoc') libmiscIoc = EpicsDll('miscIoc') - libasIoc = EpicsDll('asIoc') - # int registryDeviceSupportAdd( @@ -150,20 +120,6 @@ def EpicsDll(dll): epicsExit.argtypes = () -# Import for libas - -# int asSetFilename(const char *acf) -# -# Set access control file -asSetFilename = libasIoc.asSetFilename -asSetFilename.argtypes = (auto_encode,) - -# asTrapWriteId asTrapWriteRegisterListener(asTrapWriteListener func) -# -# Install caput hook -asTrapWriteRegisterListener = libasIoc.asTrapWriteRegisterListener - - __all__ = [ 'get_field_offsets', 'registryDeviceSupportAdd', diff --git a/softioc/iocStats b/softioc/iocStats new file mode 160000 index 00000000..4df9e878 --- /dev/null +++ b/softioc/iocStats @@ -0,0 +1 @@ +Subproject commit 4df9e87815f6a9432955a3ddb45fafa9fe4a4d40 diff --git a/softioc/pvlog.py b/softioc/pvlog.py index b245ead1..6a4fb9aa 100644 --- a/softioc/pvlog.py +++ b/softioc/pvlog.py @@ -5,6 +5,5 @@ import os from . import imports -imports.asSetFilename( - os.path.join(os.path.dirname(__file__), '..', 'access.acf')) -imports.asTrapWriteRegisterListener(imports.EpicsPvPutHook) +imports.install_pv_logging( + os.path.join(os.path.dirname(__file__), 'access.acf')) diff --git a/softioc/softioc.py b/softioc/softioc.py index 1a1798fd..7b19d1f6 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -157,9 +157,9 @@ def dbLoadDatabase(database, path = None, substitutions = None): def devIocStats(ioc_name): dbLoadDatabase( - 'ioc.db', os.path.join(os.getenv('HERE'), 'db'), - 'IOCNAME=%s,name=' % ioc_name) - + "ioc.template", + os.path.join(os.path.dirname(__file__), "iocStats", "iocAdmin", "Db"), + 'IOCNAME=' + ioc_name + ',TODFORMAT=%m/%d/%Y %H:%M:%S') def interactive_ioc(context = {}, call_exit = True): '''Fires up the interactive IOC prompt with the given context.''' diff --git a/tests/test_ioc.py b/tests/test_ioc.py index 68b5d089..418d5ba1 100644 --- a/tests/test_ioc.py +++ b/tests/test_ioc.py @@ -1,11 +1,9 @@ # Simple example script for building an example soft IOC. -import versions - import cothread from softioc import softioc, builder, pvlog -from testing import * +from sim_records import * softioc.devIocStats('TS-DI-TEST-01') From 31fd6117d1d57f90273f75c77c16a2db67f63023 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 26 Feb 2021 09:26:40 +0000 Subject: [PATCH 03/19] Don't leak when PyDict_SetItemString called --- softioc/extension.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/softioc/extension.c b/softioc/extension.c index cbeb44c4..4b41fd28 100644 --- a/softioc/extension.c +++ b/softioc/extension.c @@ -18,9 +18,16 @@ #define PyInt_FromLong(value) PyLong_FromLong(value) #endif +/* Reference stealing version of PyDict_SetItemString */ +static void set_dict_item_steal(PyObject *dict, const char *name, PyObject *py_value) +{ + PyDict_SetItemString(dict, name, py_value); + Py_DECREF(py_value); +} + /* Helper for function below. */ #define ADD_ENUM(dict, name) \ - PyDict_SetItemString(dict, #name, PyInt_FromLong(name)) + set_dict_item_steal(dict, #name, PyInt_FromLong(name)) /* Alas, EPICS has changed the numerical assignments of the DBF_ enums between * versions, so to avoid unpleasant surprises, we compute thes values here in C @@ -76,7 +83,7 @@ static PyObject *get_field_offsets(PyObject *self, PyObject *args) dbentry.pflddes->offset, dbentry.pflddes->size, dbentry.pflddes->field_type); - PyDict_SetItemString(dict, field_name, ost); + set_dict_item_steal(dict, field_name, ost); status = dbNextField(&dbentry, 0); } From 6c4a5107a6e3534d3baa0d88c3ea751083d45954 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 5 Mar 2021 12:18:48 +0000 Subject: [PATCH 04/19] Bump requirements for epicscorelibs Fixes subroutine not found error --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index baad602a..b243c4de 100644 --- a/setup.py +++ b/setup.py @@ -85,7 +85,7 @@ def install_for_development(self): version=__version__, ext_modules = [ext], install_requires = [ - "epicscorelibs==7.0.4.99.0.0a1", + "epicscorelibs==7.0.4.99.1.0a1", #epicscorelibs.version.abi_requires(), "numpy>=1.18", "epicsdbbuilder>=1.4" From 3584f8a0c18028e62737c1a2bddc2b4837951cf2 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 5 Mar 2021 12:51:55 +0000 Subject: [PATCH 05/19] Fix review suggestions --- pyproject.toml | 2 +- softioc/extension.c | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6536485d..ed3ef262 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] requires = ["setuptools", "wheel", "setuptools_dso", "epicscorelibs>=7.0.4.99.0.0a1"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" diff --git a/softioc/extension.c b/softioc/extension.c index 4b41fd28..511e707c 100644 --- a/softioc/extension.c +++ b/softioc/extension.c @@ -105,10 +105,11 @@ static PyObject *db_put_field(PyObject *self, PyObject *args) return NULL; struct dbAddr dbAddr; - int rc = dbNameToAddr(name, &dbAddr); - if (rc == 0) - rc = dbPutField(&dbAddr, dbrType, pbuffer, length); - return Py_BuildValue("i", rc); + if (dbNameToAddr(name, &dbAddr)) + return PyErr_Format(PyExc_RuntimeError, "dbNameToAddr failed for %s", name); + if (dbPutField(&dbAddr, dbrType, pbuffer, length)) + return PyErr_Format(PyExc_RuntimeError, "dbPutField failed for %s", name); + Py_RETURN_NONE; } @@ -253,4 +254,4 @@ PyMOD(_extension) #else (void)mod; #endif -} \ No newline at end of file +} From 434d15f45d0e9139f0ead8716ee912c8e8c3de33 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 30 Apr 2021 14:10:34 +0100 Subject: [PATCH 06/19] Move devIocStats to the root and support ioc.template direct loading --- .gitmodules | 2 +- setup.cfg | 18 +++++++---------- setup.py | 43 +++++++++++++++++++++++------------------ softioc/builder.py | 4 +--- softioc/devIocStats.dbd | 1 + softioc/iocStats | 1 - softioc/iocStatsDb | 1 + softioc/softioc.py | 31 +++++++++++++++++++++++++---- 8 files changed, 62 insertions(+), 39 deletions(-) create mode 120000 softioc/devIocStats.dbd delete mode 160000 softioc/iocStats create mode 120000 softioc/iocStatsDb diff --git a/.gitmodules b/.gitmodules index 4c41b1de..6363345a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "iocStats"] - path = softioc/iocStats + path = iocStats url = https://github.com/epics-modules/iocStats.git diff --git a/setup.cfg b/setup.cfg index d906dcc8..676a93a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,11 +16,7 @@ classifiers = Programming Language :: Python :: 3.8 [options] -packages = find: - -[options.packages.find] -# Don't include our test directory in the distribution -exclude = tests +packages = softioc [options.entry_points] # Include a command line script @@ -29,10 +25,10 @@ console_scripts = [options.package_data] softioc = - softioc/access.acf - softioc/device.dbd - softioc/iocStats/devIocStats/src/devIocStats.dbd - softioc/iocStats/iocAdmin/Db/*.template + access.acf + device.dbd + devIocStats.dbd + iocStatsDb/* [flake8] max-line-length = 80 @@ -43,8 +39,8 @@ extend-ignore = [tool:pytest] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error -addopts = --tb=native -vv --flake8 --doctest-modules +addopts = --tb=native -vv --flake8 --doctest-modules --ignore=iocStats --ignore=epicscorelibs [coverage:run] # This is covered in the versiongit test suite so exclude it here -omit = */_version_git.py \ No newline at end of file +omit = */_version_git.py diff --git a/setup.py b/setup.py index b243c4de..fc1d3d95 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools.command.develop import develop import epicscorelibs.path import epicscorelibs.version -from setuptools_dso import DSO, Extension, setup +from setuptools_dso import Extension, setup from epicscorelibs.config import get_config_var # Place the directory containing _version_git on the path @@ -15,12 +15,15 @@ from _version_git import __version__, get_cmdclass # noqa +sources = ['softioc/extension.c'] + devIocStats_OSI = [ "devIocStatsAnalog.c", "devIocStatsString.c", "devIocStatsWaveform.c", "devIocStatsSub.c", - "devIocStatsTest.c" + "devIocStatsTest.c", + "devIocStats.h", ] devIocStats_OSD = [ @@ -36,32 +39,35 @@ "osdSystemInfo.c", "osdHostInfo.c", "osdPIDInfo.c", + "devIocStatsOSD.h", ] -devIocStats_srcs = [] -devIocStats_src = os.path.join("softioc", "iocStats", "devIocStats") -devIocStats_default = os.path.join(devIocStats_src, "os", "default") +devIocStats_src = os.path.join("iocStats", "devIocStats") devIocStats_os = os.path.join(devIocStats_src, "os", get_config_var('OS_CLASS')) +devIocStats_default = os.path.join(devIocStats_src, "os", "default") + +def _add_file(f): + if f.endswith(".h"): + # Only add header files if making an sdist + # https://github.com/pypa/packaging-problems/issues/84#issuecomment-383718492 + should_add = "sdist" in sys.argv + else: + should_add = True + if should_add: + sources.append(f) for f in devIocStats_OSI: - devIocStats_srcs.append(os.path.join(devIocStats_src, f)) + _add_file(os.path.join(devIocStats_src, f)) for f in devIocStats_OSD: if os.path.exists(os.path.join(devIocStats_os, f)): - devIocStats_srcs.append(os.path.join(devIocStats_os, f)) + _add_file(os.path.join(devIocStats_os, f)) else: - devIocStats_srcs.append(os.path.join(devIocStats_default, f)) - -#dso = DSO( -# 'softioc.lib.devIocStats', -# devIocStats_srcs, -# include_dirs=[epicscorelibs.path.include_path, devIocStats_src, devIocStats_os, devIocStats_default], -# dsos=['epicscorelibs.lib.Com'] -#) + _add_file(os.path.join(devIocStats_default, f)) # Extension with all our C code ext = Extension( name='softioc._extension', - sources = ['softioc/extension.c'] + devIocStats_srcs, + sources = sources, include_dirs=[epicscorelibs.path.include_path, devIocStats_src, devIocStats_os, devIocStats_default], dsos = ['epicscorelibs.lib.dbCore', 'epicscorelibs.lib.Com'], define_macros = get_config_var('CPPFLAGS'), @@ -85,11 +91,10 @@ def install_for_development(self): version=__version__, ext_modules = [ext], install_requires = [ - "epicscorelibs==7.0.4.99.1.0a1", - #epicscorelibs.version.abi_requires(), + # Dependency declared in pyproject.toml + epicscorelibs.version.abi_requires(), "numpy>=1.18", "epicsdbbuilder>=1.4" ], - #x_dsos = [dso], zip_safe = False, # setuptools_dso is not compatible with eggs! ) diff --git a/softioc/builder.py b/softioc/builder.py index 9aee2a8d..029f9134 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -10,9 +10,7 @@ iocshRegisterCommon() dbLoadDatabase("base.dbd", os.path.join(path.base_path, "dbd")) - -dbd_dir = os.path.join(os.path.dirname(__file__), "iocStats", "devIocStats") -dbLoadDatabase("devIocStats.dbd", dbd_dir) +dbLoadDatabase("devIocStats.dbd", os.path.dirname(__file__)) if registerRecordDeviceDriver(pdbbase): raise RuntimeError('Error registering') diff --git a/softioc/devIocStats.dbd b/softioc/devIocStats.dbd new file mode 120000 index 00000000..275179c0 --- /dev/null +++ b/softioc/devIocStats.dbd @@ -0,0 +1 @@ +../iocStats/devIocStats/devIocStats.dbd \ No newline at end of file diff --git a/softioc/iocStats b/softioc/iocStats deleted file mode 160000 index 4df9e878..00000000 --- a/softioc/iocStats +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4df9e87815f6a9432955a3ddb45fafa9fe4a4d40 diff --git a/softioc/iocStatsDb b/softioc/iocStatsDb new file mode 120000 index 00000000..c17da227 --- /dev/null +++ b/softioc/iocStatsDb @@ -0,0 +1 @@ +../iocStats/iocAdmin/Db \ No newline at end of file diff --git a/softioc/softioc.py b/softioc/softioc.py index 7b19d1f6..80029849 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -4,6 +4,8 @@ import sys from ctypes import * +from epicsdbbuilder.recordset import recordset + from . import imports __all__ = ['dbLoadDatabase', 'iocInit', 'interactive_ioc'] @@ -155,11 +157,32 @@ def dbLoadDatabase(database, path = None, substitutions = None): '''Loads a database file and applies any given substitutions.''' imports.dbLoadDatabase(database, path, substitutions) +def _add_records_from_file(dir, file, macros): + # This is very naive, for instance macros are added to but never removed, + # but it works well enough for devIocStats + with open(os.path.join(dir, file)) as f: + for line in f.readlines(): + line = line.rstrip() + if line.startswith('substitute'): + # substitute "QUEUE=scanOnce, QUEUE_CAPS=SCANONCE + for sub in line.split('"')[1].split(','): + k, v = sub.split('=') + macros[k.strip()] = v.strip() + elif line.startswith('include'): + # include "iocQueue.db" + _add_records_from_file(dir, line.split('"')[1], macros) + else: + # A record line + for k, v in macros.items(): + line = line.replace('$(%s)' % k, v) + recordset.AddBodyLine(line) + + def devIocStats(ioc_name): - dbLoadDatabase( - "ioc.template", - os.path.join(os.path.dirname(__file__), "iocStats", "iocAdmin", "Db"), - 'IOCNAME=' + ioc_name + ',TODFORMAT=%m/%d/%Y %H:%M:%S') + macros = dict(IOCNAME=ioc_name, TODFORMAT='%m/%d/%Y %H:%M:%S') + iocstats_dir = os.path.join(os.path.dirname(__file__), 'iocStatsDb') + _add_records_from_file(iocstats_dir, 'ioc.template', macros) + def interactive_ioc(context = {}, call_exit = True): '''Fires up the interactive IOC prompt with the given context.''' From f02e08e76a49f12844839fdf7552bbd76caf27c9 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 30 Apr 2021 14:10:51 +0100 Subject: [PATCH 07/19] Change cli to do a subprocess call --- softioc/__main__.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/softioc/__main__.py b/softioc/__main__.py index e15de04a..1281195f 100644 --- a/softioc/__main__.py +++ b/softioc/__main__.py @@ -1,6 +1,7 @@ import os import sys from argparse import ArgumentParser +import subprocess from softioc import __version__ @@ -9,16 +10,10 @@ def main(args=None): parser = ArgumentParser() parser.add_argument("--version", action="version", version=__version__) parser.add_argument("script", help="The python script to run") + parser.add_argument("arg", help="Any arguments to pass to the script", nargs="*") parsed_args = parser.parse_args(args) - # Insert the directory containing script onto the path in case we do - # any imports - sys.path.insert(0, os.path.dirname(os.path.abspath(parsed_args.script))) - if sys.version_info < (3, 0): - # Python 2 - execfile(parsed_args.script) - else: - # Python 3 - exec(open(parsed_args.script).read()) + # Execute as subprocess + subprocess.Popen([sys.executable, parsed_args.script, *parsed_args.arg]).communicate() if __name__ == "__main__": From 78c351f1df57c244d93e015afb90d5f394561427 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 30 Apr 2021 14:19:41 +0100 Subject: [PATCH 08/19] Add some tests --- tests/expected_records.db | 715 ++++++++++++++++++++++++++++++++++++++ tests/sim_records.py | 37 +- tests/test_createdb.py | 9 - tests/test_ioc.py | 13 - tests/test_softioc.py | 54 +++ 5 files changed, 793 insertions(+), 35 deletions(-) create mode 100644 tests/expected_records.db mode change 100644 => 100755 tests/sim_records.py delete mode 100644 tests/test_createdb.py delete mode 100644 tests/test_ioc.py create mode 100644 tests/test_softioc.py diff --git a/tests/expected_records.db b/tests/expected_records.db new file mode 100644 index 00000000..53c7c1ea --- /dev/null +++ b/tests/expected_records.db @@ -0,0 +1,715 @@ +# This file was automatically generated on Thu 29 Apr 2021 14:11:13 BST. +# +# *** Please do not edit this file: edit the source file instead. *** +# + +# Used by Channel Access Security to determine access to this IOC. +record(mbbo, "TS-DI-TEST-01:ACCESS") +{ + field(DESC, "TS-DI-TEST-01 Acc Mode") + field(PINI, "YES") + field(ZRST, "Running") + field(ZRSV, "NO_ALARM") + field(ONST, "Maintenance") + field(ONSV, "MINOR") + field(TWST, "Test") + field(TWSV, "MINOR") + field(THST, "OFFLINE") + field(THSV, "MAJOR") + info(autosaveFields, "VAL") +} +record(stringin, "TS-DI-TEST-01:STARTTOD") +{ + field(DESC, "Time and date of startup") + field(DTYP, "Soft Timestamp") + field(PINI, "YES") + field(INP, "@%m/%d/%Y %H:%M:%S") +} +record(stringin, "TS-DI-TEST-01:TOD") +{ + field(DESC, "Current time and date") + field(DTYP, "Soft Timestamp") + field(SCAN, "1 second") + field(INP, "@%m/%d/%Y %H:%M:%S") +} +record(calcout, "TS-DI-TEST-01:HEARTBEAT") +{ + field(DESC, "1 Hz counter since startup") + field(CALC, "(A<2147483647)?A+1:1") + field(SCAN, "1 second") + field(INPA, "TS-DI-TEST-01:HEARTBEAT") +} +# if autosave is working, START_CNT creates a running count of +# number of times the IOC was started. +record(calcout, "TS-DI-TEST-01:START_CNT") +{ + field(DESC, "Increments at startup") + field(CALC, "A+1") + field(PINI, "YES") + field(INPA, "TS-DI-TEST-01:START_CNT") + info(autosaveFields_pass0, "VAL") +} +# +# Using an existing internal set of subroutines, this +# PV updates the Access Security mechanism dynamically. +# The .acf file is re-read. +# +record( sub, "TS-DI-TEST-01:READACF") +{ + field( DESC, "TS-DI-TEST-01 ACF Update") + field( INAM, "asSubInit") + field( SNAM, "asSubProcess") + field( BRSV, "INVALID") +} +record(sub, "TS-DI-TEST-01:SYSRESET") +{ + alias("TS-DI-TEST-01:SysReset") + field(DESC, "IOC Restart" ) + field(SNAM, "rebootProc") + field(BRSV,"INVALID") + field(L,"1") +} + +record(ai, "TS-DI-TEST-01:CA_CLNT_CNT") { + field(DESC, "Number of CA Clients") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@ca_clients") + field(HOPR, "200") + field(HIHI, "175") + field(HIGH, "100") + field(HHSV, "MAJOR") + field(HSV, "MINOR") + info(autosaveFields_pass0, "HOPR LOPR HIHI HIGH LOW LOLO HHSV HSV LSV LLSV") +} + +record(ai, "TS-DI-TEST-01:CA_CONN_CNT") { + field(DESC, "Number of CA Connections") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@ca_connections") + field(HOPR, "5000") + field(HIHI, "4500") + field(HIGH, "4000") + field(HHSV, "MAJOR") + field(HSV, "MINOR") + info(autosaveFields_pass0, "HOPR LOPR HIHI HIGH LOW LOLO HHSV HSV LSV LLSV") +} + +record(ai, "TS-DI-TEST-01:RECORD_CNT") { + field(DESC, "Number of Records") + field(PINI, "YES") + field(DTYP, "IOC stats") + field(INP, "@records") +} + +record(ai, "TS-DI-TEST-01:FD_MAX") { + field(DESC, "Max File Descriptors") + field(PINI, "YES") + field(DTYP, "IOC stats") + field(INP, "@maxfd") +} + +record(ai, "TS-DI-TEST-01:FD_CNT") { + field(DESC, "Allocated File Descriptors") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(FLNK, "TS-DI-TEST-01:FD_FREE PP MS") + field(INP, "@fd") +} + +record(calc, "TS-DI-TEST-01:FD_FREE") { + field(DESC, "Available FDs") + field(CALC, "B>0?B-A:C") + field(INPA, "TS-DI-TEST-01:FD_CNT NPP MS") + field(INPB, "TS-DI-TEST-01:FD_MAX NPP MS") + field(INPC, "1000") + field(HOPR, "150") + field(LOLO, "5") + field(LOW, "20") + field(LLSV, "MAJOR") + field(LSV, "MINOR") + info(autosaveFields_pass0, "HOPR LOPR LOW LOLO LSV LLSV") +} + +record(ai, "TS-DI-TEST-01:SYS_CPU_LOAD") { + field(DESC, "System CPU Load") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@sys_cpuload") + field(EGU, "%") + field(PREC, "1") + field(HOPR, "100") + field(HIHI, "80") + field(HIGH, "70") + field(HHSV, "MAJOR") + field(HSV, "MINOR") + info(autosaveFields_pass0, "HOPR LOPR HIHI HIGH LOW LOLO HHSV HSV LSV LLSV") +} + +record(ai, "TS-DI-TEST-01:IOC_CPU_LOAD") { + alias("TS-DI-TEST-01:LOAD") + field(DESC, "IOC CPU Load") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@ioc_cpuload") + field(EGU, "%") + field(PREC, "1") + field(HOPR, "100") + field(HIHI, "80") + field(HIGH, "70") + field(HHSV, "MAJOR") + field(HSV, "MINOR") + info(autosaveFields_pass0, "HOPR LOPR HIHI HIGH LOW LOLO HHSV HSV LSV LLSV") +} + +record(ai, "TS-DI-TEST-01:CPU_CNT") { + field(DESC, "Number of CPUs") + field(DTYP, "IOC stats") + field(INP, "@no_of_cpus") + field(PINI, "YES") +} + +record(ai, "TS-DI-TEST-01:SUSP_TASK_CNT") { + field(DESC, "Number Suspended Tasks") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@suspended_tasks") + field(HIHI, "1") + field(HHSV, "MAJOR") + info(autosaveFields_pass0, "HOPR LOPR HIHI HIGH LOW LOLO HHSV HSV LSV LLSV") +} + +record(ai, "TS-DI-TEST-01:MEM_USED") { + field(DESC, "Allocated Memory") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@allocated_bytes") + field(EGU, "byte") +} + +record(ai, "TS-DI-TEST-01:MEM_FREE") { + field(DESC, "Free Memory") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@free_bytes") + field(EGU, "byte") + field(LLSV, "MAJOR") + field(LSV, "MINOR") + info(autosaveFields_pass0, "HOPR LOPR LOW LOLO LSV LLSV") +} + +record(ai, "TS-DI-TEST-01:MEM_MAX") { + field(DESC, "Maximum Memory") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@total_bytes") + field(EGU, "byte") +} + +record(ao, "TS-DI-TEST-01:CA_UPD_TIME") { + field(DESC, "CA Check Update Period") + field(DTYP, "IOC stats") + field(OUT, "@ca_scan_rate") + field(EGU, "sec") + field(DRVH, "60") + field(DRVL, "1") + field(HOPR, "60") + field(VAL, "15") + field(PINI, "YES") +} + +record(ao, "TS-DI-TEST-01:FD_UPD_TIME") { + field(DESC, "FD Check Update Period") + field(DTYP, "IOC stats") + field(OUT, "@fd_scan_rate") + field(EGU, "sec") + field(DRVH, "60") + field(DRVL, "1") + field(HOPR, "60") + field(VAL, "20") + field(PINI, "YES") +} + +record(ao, "TS-DI-TEST-01:LOAD_UPD_TIME") { + field(DESC, "CPU Check Update Period") + field(DTYP, "IOC stats") + field(OUT, "@cpu_scan_rate") + field(EGU, "sec") + field(DRVH, "60") + field(DRVL, "1") + field(HOPR, "60") + field(VAL, "10") + field(PINI, "YES") +} + +record(ao, "TS-DI-TEST-01:MEM_UPD_TIME") { + field(DESC, "Memory Check Update Period") + field(DTYP, "IOC stats") + field(OUT, "@memory_scan_rate") + field(EGU, "sec") + field(DRVH, "60") + field(DRVL, "1") + field(HOPR, "60") + field(VAL, "10") + field(PINI, "YES") +} + +record(stringin, "TS-DI-TEST-01:ST_SCRIPT1") { + field(DESC, "Startup Script Part1") + field(DTYP, "IOC stats") + field(INP, "@startup_script_1") + field(PINI, "YES") +} + +record(stringin, "TS-DI-TEST-01:ST_SCRIPT2") { + field(DESC, "Startup Script Part2") + field(DTYP, "IOC stats") + field(INP, "@startup_script_2") + field(PINI, "YES") +} + +record(waveform, "TS-DI-TEST-01:ST_SCRIPT") { + field(DESC, "Startup Script") + field(DTYP, "IOC stats") + field(INP, "@startup_script") + field(NELM, "120") + field(FTVL, "CHAR") + field(PINI, "YES") +} + +record(stringin, "TS-DI-TEST-01:KERNEL_VERS") { + field(DESC, "Kernel Version") + field(DTYP, "IOC stats") + field(INP, "@kernel_ver") + field(PINI, "YES") +} + +record(stringin, "TS-DI-TEST-01:EPICS_VERS") { + field(DESC, "EPICS Version") + field(DTYP, "IOC stats") + field(INP, "@epics_ver") + field(PINI, "YES") +} + +record(waveform, "TS-DI-TEST-01:EPICS_VERSION") { + field(DESC, "EPICS Version") + field(DTYP, "IOC stats") + field(INP, "@epics_ver") + field(NELM, "120") + field(FTVL, "CHAR") + field(PINI, "YES") +} + +record(stringin, "TS-DI-TEST-01:HOSTNAME") { + field(DESC, "Host Name") + field(DTYP, "IOC stats") + field(INP, "@hostname") + field(PINI, "YES") +} + +record(stringin, "TS-DI-TEST-01:APP_DIR1") { + field(DESC, "Application Directory Part 1") + field(DTYP, "IOC stats") + field(INP, "@pwd1") + field(PINI, "YES") +} + +record(stringin, "TS-DI-TEST-01:APP_DIR2") { + field(DESC, "Application Directory Part 2") + field(DTYP, "IOC stats") + field(INP, "@pwd2") + field(PINI, "YES") +} + +record(waveform, "TS-DI-TEST-01:APP_DIR") { + field(DESC, "Application Directory") + field(DTYP, "IOC stats") + field(INP, "@pwd") + field(NELM, "160") + field(FTVL, "CHAR") + field(PINI, "YES") +} + +record(stringin, "TS-DI-TEST-01:UPTIME") { + field(DESC, "Elapsed Time since Start") + field(SCAN, "1 second") + field(DTYP, "IOC stats") + field(INP, "@up_time") + field(PINI, "YES") +} + +record(stringin, "TS-DI-TEST-01:ENGINEER") { + field(DESC, "Engineer") + field(DTYP, "IOC stats") + field(INP, "@engineer") + field(PINI, "YES") +} + +record(stringin, "TS-DI-TEST-01:LOCATION") { + field(DESC, "Location") + field(DTYP, "IOC stats") + field(INP, "@location") + field(PINI, "YES") +} + +record(ai, "TS-DI-TEST-01:PROCESS_ID") { + field(DESC, "Process ID") + field(PINI, "YES") + field(DTYP, "IOC stats") + field(INP, "@proc_id") +} + +record(ai, "TS-DI-TEST-01:PARENT_ID") { + field(DESC, "Parent Process ID") + field(PINI, "YES") + field(DTYP, "IOC stats") + field(INP, "@parent_proc_id") +} + +record(ai, "TS-DI-TEST-01:SCANONCE_Q_SIZE") { + field(DESC, "max # entries in IOC scanOnce queue") + field(DTYP, "IOC stats") + field(INP, "@scanOnceQueueSize") + field(PINI, "YES") +} +record(ai, "TS-DI-TEST-01:CB_Q_SIZE") { + field(DESC, "max # entries in IOC callback queues") + field(DTYP, "IOC stats") + field(INP, "@cbQueueSize") + field(PINI, "YES") +} + +record(ai, "TS-DI-TEST-01:SCANONCE_Q_HIGH") { + field(DESC, "max # of elmts in IOC's scanOnce queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@scanOnceQueueHiWtrMrk") + field(PINI, "YES") + field(FLNK, "TS-DI-TEST-01:SCANONCE_Q_HIGHPER") +} + +record(calc, "TS-DI-TEST-01:SCANONCE_Q_HIGHPER") { + field(DESC, "Max. usage of IOC's scanOnce queue") + field(INPA, "TS-DI-TEST-01:SCANONCE_Q_HIGH NPP MS") + field(INPB, "TS-DI-TEST-01:SCANONCE_Q_SIZE NPP MS") + field(CALC, "100*A/B") + field(LOPR, "0") + field(HOPR, "100") + field(EGU, "%") +} + +record(ai, "TS-DI-TEST-01:SCANONCE_Q_USED") { + field(DESC, "# of entries in IOC's scanOnce queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@scanOnceQueueUsed") + field(PINI, "YES") + field(FLNK, "TS-DI-TEST-01:SCANONCE_Q_USEDPER") +} + +record(calc, "TS-DI-TEST-01:SCANONCE_Q_USEDPER") { + field(DESC, "Percentage of IOC's scanOnce queue used") + field(INPA, "TS-DI-TEST-01:SCANONCE_Q_USED NPP MS") + field(INPB, "TS-DI-TEST-01:SCANONCE_Q_SIZE NPP MS") + field(CALC, "100*A/B") + field(LOPR, "0") + field(HOPR, "100") + field(EGU, "%") +} + +record(ai, "TS-DI-TEST-01:SCANONCE_Q_OVERRUNS") { + field(DESC, "# of overruns of IOC's scanOnce queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@scanOnceQueueOverruns") + field(PINI, "YES") +} + +record(ai, "TS-DI-TEST-01:CBLOW_Q_HIGH") { + field(DESC, "max # of elmts in IOC's cbLow queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@cbLowQueueHiWtrMrk") + field(PINI, "YES") + field(FLNK, "TS-DI-TEST-01:CBLOW_Q_HIGHPER") +} + +record(calc, "TS-DI-TEST-01:CBLOW_Q_HIGHPER") { + field(DESC, "Max. usage of IOC's cbLow queue") + field(INPA, "TS-DI-TEST-01:CBLOW_Q_HIGH NPP MS") + field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") + field(CALC, "100*A/B") + field(LOPR, "0") + field(HOPR, "100") + field(EGU, "%") +} + +record(ai, "TS-DI-TEST-01:CBLOW_Q_USED") { + field(DESC, "# of entries in IOC's cbLow queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@cbLowQueueUsed") + field(PINI, "YES") + field(FLNK, "TS-DI-TEST-01:CBLOW_Q_USEDPER") +} + +record(calc, "TS-DI-TEST-01:CBLOW_Q_USEDPER") { + field(DESC, "Percentage of IOC's cbLow queue used") + field(INPA, "TS-DI-TEST-01:CBLOW_Q_USED NPP MS") + field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") + field(CALC, "100*A/B") + field(LOPR, "0") + field(HOPR, "100") + field(EGU, "%") +} + +record(ai, "TS-DI-TEST-01:CBLOW_Q_OVERRUNS") { + field(DESC, "# of overruns of IOC's cbLow queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@cbLowQueueOverruns") + field(PINI, "YES") +} + +record(ai, "TS-DI-TEST-01:CBMEDIUM_Q_HIGH") { + field(DESC, "max # of elmts in IOC's cbMedium queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@cbMediumQueueHiWtrMrk") + field(PINI, "YES") + field(FLNK, "TS-DI-TEST-01:CBMEDIUM_Q_HIGHPER") +} + +record(calc, "TS-DI-TEST-01:CBMEDIUM_Q_HIGHPER") { + field(DESC, "Max. usage of IOC's cbMedium queue") + field(INPA, "TS-DI-TEST-01:CBMEDIUM_Q_HIGH NPP MS") + field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") + field(CALC, "100*A/B") + field(LOPR, "0") + field(HOPR, "100") + field(EGU, "%") +} + +record(ai, "TS-DI-TEST-01:CBMEDIUM_Q_USED") { + field(DESC, "# of entries in IOC's cbMedium queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@cbMediumQueueUsed") + field(PINI, "YES") + field(FLNK, "TS-DI-TEST-01:CBMEDIUM_Q_USEDPER") +} + +record(calc, "TS-DI-TEST-01:CBMEDIUM_Q_USEDPER") { + field(DESC, "Percentage of IOC's cbMedium queue used") + field(INPA, "TS-DI-TEST-01:CBMEDIUM_Q_USED NPP MS") + field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") + field(CALC, "100*A/B") + field(LOPR, "0") + field(HOPR, "100") + field(EGU, "%") +} + +record(ai, "TS-DI-TEST-01:CBMEDIUM_Q_OVERRUNS") { + field(DESC, "# of overruns of IOC's cbMedium queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@cbMediumQueueOverruns") + field(PINI, "YES") +} + +record(ai, "TS-DI-TEST-01:CBHIGH_Q_HIGH") { + field(DESC, "max # of elmts in IOC's cbHigh queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@cbHighQueueHiWtrMrk") + field(PINI, "YES") + field(FLNK, "TS-DI-TEST-01:CBHIGH_Q_HIGHPER") +} + +record(calc, "TS-DI-TEST-01:CBHIGH_Q_HIGHPER") { + field(DESC, "Max. usage of IOC's cbHigh queue") + field(INPA, "TS-DI-TEST-01:CBHIGH_Q_HIGH NPP MS") + field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") + field(CALC, "100*A/B") + field(LOPR, "0") + field(HOPR, "100") + field(EGU, "%") +} + +record(ai, "TS-DI-TEST-01:CBHIGH_Q_USED") { + field(DESC, "# of entries in IOC's cbHigh queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@cbHighQueueUsed") + field(PINI, "YES") + field(FLNK, "TS-DI-TEST-01:CBHIGH_Q_USEDPER") +} + +record(calc, "TS-DI-TEST-01:CBHIGH_Q_USEDPER") { + field(DESC, "Percentage of IOC's cbHigh queue used") + field(INPA, "TS-DI-TEST-01:CBHIGH_Q_USED NPP MS") + field(INPB, "TS-DI-TEST-01:CB_Q_SIZE NPP MS") + field(CALC, "100*A/B") + field(LOPR, "0") + field(HOPR, "100") + field(EGU, "%") +} + +record(ai, "TS-DI-TEST-01:CBHIGH_Q_OVERRUNS") { + field(DESC, "# of overruns of IOC's cbHigh queue") + field(SCAN, "I/O Intr") + field(DTYP, "IOC stats") + field(INP, "@cbHighQueueOverruns") + field(PINI, "YES") +} + +record(ai, "TS-DI-TEST-01:AI") +{ + field(DTYP, "Python") + field(INP, "@TS-DI-TEST-01:AI") + field(PINI, "YES") + field(SCAN, "I/O Intr") +} + +record(ao, "TS-DI-TEST-01:AO") +{ + field(DTYP, "Python") + field(OUT, "@TS-DI-TEST-01:AO") +} + +record(bi, "TS-DI-TEST-01:BOOLIN") +{ + field(DTYP, "Python") + field(INP, "@TS-DI-TEST-01:BOOLIN") + field(ONAM, "False") + field(PINI, "YES") + field(SCAN, "I/O Intr") + field(ZNAM, "True") +} + +record(bo, "TS-DI-TEST-01:BOOLOUT") +{ + field(DTYP, "Python") + field(OMSL, "supervisory") + field(ONAM, "One") + field(OUT, "@TS-DI-TEST-01:BOOLOUT") + field(ZNAM, "Zero") +} + +record(longin, "TS-DI-TEST-01:LONGIN") +{ + field(DTYP, "Python") + field(INP, "@TS-DI-TEST-01:LONGIN") + field(MDEL, "-1") + field(PINI, "YES") + field(SCAN, "I/O Intr") +} + +record(longout, "TS-DI-TEST-01:LONGOUT") +{ + field(DTYP, "Python") + field(OMSL, "supervisory") + field(OUT, "@TS-DI-TEST-01:LONGOUT") +} + +record(waveform, "TS-DI-TEST-01:LONGSTRING") +{ + field(DTYP, "Python") + field(FTVL, "UCHAR") + field(INP, "@TS-DI-TEST-01:LONGSTRING") + field(NELM, "256") + field(SCAN, "I/O Intr") +} + +record(mbbi, "TS-DI-TEST-01:MBBI") +{ + field(DTYP, "Python") + field(INP, "@TS-DI-TEST-01:MBBI") + field(ONST, "Two") + field(ONVL, "1") + field(PINI, "YES") + field(SCAN, "I/O Intr") + field(TWST, "Three") + field(TWVL, "2") + field(ZRST, "One") + field(ZRVL, "0") +} + +record(mbbo, "TS-DI-TEST-01:MBBO") +{ + field(DTYP, "Python") + field(OMSL, "supervisory") + field(ONST, "Zwei") + field(ONVL, "1") + field(OUT, "@TS-DI-TEST-01:MBBO") + field(TWST, "Drei") + field(TWVL, "2") + field(ZRST, "Ein") + field(ZRVL, "0") +} + +record(waveform, "TS-DI-TEST-01:SIN") +{ + field(DTYP, "Python") + field(FTVL, "DOUBLE") + field(INP, "@TS-DI-TEST-01:SIN") + field(NELM, "1024") + field(SCAN, "I/O Intr") +} + +record(longout, "TS-DI-TEST-01:SINN") +{ + field(DRVH, "1024") + field(DRVL, "0") + field(DTYP, "Python") + field(OMSL, "supervisory") + field(OUT, "@TS-DI-TEST-01:SINN") +} + +record(ao, "TS-DI-TEST-01:SINP") +{ + field(DTYP, "Python") + field(OUT, "@TS-DI-TEST-01:SINP") +} + +record(stringin, "TS-DI-TEST-01:STRINGIN") +{ + field(DTYP, "Python") + field(INP, "@TS-DI-TEST-01:STRINGIN") + field(PINI, "YES") + field(SCAN, "I/O Intr") +} + +record(stringout, "TS-DI-TEST-01:STRINGOUT") +{ + field(DTYP, "Python") + field(OUT, "@TS-DI-TEST-01:STRINGOUT") +} + +record(waveform, "TS-DI-TEST-01:WAVEFORM") +{ + field(DTYP, "Python") + field(FTVL, "DOUBLE") + field(INP, "@TS-DI-TEST-01:WAVEFORM") + field(NELM, "32") + field(PINI, "YES") + field(SCAN, "I/O Intr") +} + +record(waveform, "TS-DI-TEST-01:WAVEFORM2") +{ + field(DTYP, "Python") + field(FTVL, "FLOAT") + field(INP, "@TS-DI-TEST-01:WAVEFORM2") + field(NELM, "10") + field(SCAN, "I/O Intr") +} + +record(waveform, "TS-DI-TEST-01:WAVEFORM_OUT") +{ + field(DTYP, "PythonWfOut") + field(FTVL, "DOUBLE") + field(INP, "@TS-DI-TEST-01:WAVEFORM_OUT") + field(NELM, "32") +} diff --git a/tests/sim_records.py b/tests/sim_records.py old mode 100644 new mode 100755 index 0aaae69d..abbd826d --- a/tests/sim_records.py +++ b/tests/sim_records.py @@ -2,11 +2,18 @@ from __future__ import print_function +from epicsdbbuilder.recordnames import GetRecordNames + +from softioc import softioc from softioc.builder import * import numpy -SetDeviceName('TS-DI-TEST-01') - +names = GetRecordNames() +if names.prefix: + ioc_name = names.prefix[0] +else: + ioc_name = 'TS-DI-TEST-01' + SetDeviceName(ioc_name) def on_update(value): print('on_update', repr(value)) @@ -14,11 +21,11 @@ def on_update(value): def on_update_name(value, name): print('on_update', name, ':', repr(value)) -t_ai = aIn('AI') -t_boolin = boolIn('BOOLIN', 'True', 'False') -t_longin = longIn('LONGIN') -t_stringin = stringIn('STRINGIN') -t_mbbi = mbbIn('MBBI', 'One', 'Two', 'Three') +t_ai = aIn('AI', initial_value=12.34) +t_boolin = boolIn('BOOLIN', 'True', 'False', initial_value=False) +t_longin = longIn('LONGIN', initial_value=33) +t_stringin = stringIn('STRINGIN', initial_value="Testing string") +t_mbbi = mbbIn('MBBI', 'One', 'Two', 'Three', initial_value=2) t_ao = aOut ('AO', initial_value = 12.45, on_update_name = on_update_name) @@ -40,12 +47,6 @@ def update_sin_wf(value): initial_value = 1024, on_update = update_sin_wf) sin_ph = aOut('SINP', initial_value = 0.0, on_update = update_sin_wf) -t_ai.set(12.34) -t_boolin.set(False) -t_longin.set(33) -t_stringin.set('Testing string') -t_mbbi.set(2) - wf_len = 32 wf = numpy.sin(numpy.linspace(0, 2*numpy.pi, wf_len)) @@ -70,6 +71,7 @@ def UpdateOut(): t_stringout.set('Another different string') t_mbbo.set(2) +softioc.devIocStats(ioc_name) __all__ = [ 't_ai', 't_boolin', 't_longin', 't_stringin', 't_mbbi', @@ -78,3 +80,12 @@ def UpdateOut(): 'wf_len', 'Update', 'UpdateOut' ] + + +if __name__ == "__main__": + # Simple example script for building an example soft IOC. + from softioc import pvlog, builder + builder.WriteRecords("expected_records.db") + LoadDatabase() + softioc.iocInit() + softioc.interactive_ioc(globals()) diff --git a/tests/test_createdb.py b/tests/test_createdb.py deleted file mode 100644 index 50d65060..00000000 --- a/tests/test_createdb.py +++ /dev/null @@ -1,9 +0,0 @@ -# Python script to create a .db file - -import sys - -import versions -import testing - -from softioc.builder import WriteRecords -WriteRecords(sys.argv[1]) diff --git a/tests/test_ioc.py b/tests/test_ioc.py deleted file mode 100644 index 418d5ba1..00000000 --- a/tests/test_ioc.py +++ /dev/null @@ -1,13 +0,0 @@ -# Simple example script for building an example soft IOC. - -import cothread -from softioc import softioc, builder, pvlog - -from sim_records import * - -softioc.devIocStats('TS-DI-TEST-01') - -builder.LoadDatabase() -softioc.iocInit() - -softioc.interactive_ioc(globals()) diff --git a/tests/test_softioc.py b/tests/test_softioc.py new file mode 100644 index 00000000..031c2b78 --- /dev/null +++ b/tests/test_softioc.py @@ -0,0 +1,54 @@ +from importlib import reload +import random +import string +import os +import sys + +from softioc import softioc, builder +from cothread.catools import caget, caput + +import sim_records + +PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + + +def test_records(tmp_path): + path = tmp_path / "records.db" + builder.WriteRecords(path) + expected = os.path.join(os.path.dirname(__file__), "expected_records.db") + assert open(path).readlines()[4:] == open(expected).readlines()[4:] + + +def assert_record_equals(record, value): + assert record.get() == caget(record.name) == value + +def test_ioc(capsys): + builder.ResetRecords() + builder.SetDeviceName(PV_PREFIX) + reload(sim_records) + builder.LoadDatabase() + softioc.iocInit() + out, err = capsys.readouterr() + assert not out + assert not err + assert caget(PV_PREFIX + ":UPTIME") in ["00:00:00", "00:00:01"] + # AI + assert_record_equals(sim_records.t_ai, 12.34) + sim_records.t_ai.set(34) + assert_record_equals(sim_records.t_ai, 34) + # STRINGOUT + assert_record_equals(sim_records.t_stringout, "watevah") + caput(PV_PREFIX + ":STRINGOUT", "something", wait=True) + out, err = capsys.readouterr() + assert out == "on_update 'something'\n" + assert not err + assert_record_equals(sim_records.t_stringout, "something") + sim_records.t_stringout.set(b"something else") + assert_record_equals(sim_records.t_stringout, "something else") + + + + + +if __name__ == "__main__": + softioc.interactive_ioc(globals()) From 0448475a3f0cdb9a7288e6cce01a3c61e6ddaf5f Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Tue, 4 May 2021 10:14:51 +0100 Subject: [PATCH 09/19] Style changes --- setup.py | 7 +++-- softioc/__main__.py | 6 ++-- softioc/alarm.py | 51 +++++++++++++++++----------------- softioc/builder.py | 28 +++++++++---------- softioc/device.py | 59 ++++++++++++++++++++++------------------ softioc/device_core.py | 5 ++-- softioc/extension.c | 18 ++++++------ softioc/fields.py | 58 +++++++++++++++++++-------------------- softioc/pythonSoftIoc.py | 4 +-- softioc/softioc.py | 57 +++++++++++++++++++------------------- tests/sim_records.py | 36 ++++++++++++------------ 11 files changed, 171 insertions(+), 158 deletions(-) diff --git a/setup.py b/setup.py index fc1d3d95..43ea801e 100644 --- a/setup.py +++ b/setup.py @@ -68,7 +68,10 @@ def _add_file(f): ext = Extension( name='softioc._extension', sources = sources, - include_dirs=[epicscorelibs.path.include_path, devIocStats_src, devIocStats_os, devIocStats_default], + include_dirs=[ + epicscorelibs.path.include_path, + devIocStats_src, devIocStats_os, devIocStats_default + ], dsos = ['epicscorelibs.lib.dbCore', 'epicscorelibs.lib.Com'], define_macros = get_config_var('CPPFLAGS'), extra_compile_args = get_config_var('CXXFLAGS'), @@ -96,5 +99,5 @@ def install_for_development(self): "numpy>=1.18", "epicsdbbuilder>=1.4" ], - zip_safe = False, # setuptools_dso is not compatible with eggs! + zip_safe = False, # setuptools_dso is not compatible with eggs! ) diff --git a/softioc/__main__.py b/softioc/__main__.py index 1281195f..a091af43 100644 --- a/softioc/__main__.py +++ b/softioc/__main__.py @@ -10,10 +10,12 @@ def main(args=None): parser = ArgumentParser() parser.add_argument("--version", action="version", version=__version__) parser.add_argument("script", help="The python script to run") - parser.add_argument("arg", help="Any arguments to pass to the script", nargs="*") + parser.add_argument( + "arg", help="Any arguments to pass to the script", nargs="*") parsed_args = parser.parse_args(args) # Execute as subprocess - subprocess.Popen([sys.executable, parsed_args.script, *parsed_args.arg]).communicate() + cmd = [sys.executable, parsed_args.script, *parsed_args.arg] + subprocess.Popen(cmd).communicate() if __name__ == "__main__": diff --git a/softioc/alarm.py b/softioc/alarm.py index c6ade99c..e1366c3f 100644 --- a/softioc/alarm.py +++ b/softioc/alarm.py @@ -1,29 +1,28 @@ # Severity code definitions taken from EPICS alarm.h -NO_ALARM = 0 -MINOR_ALARM = 1 -MAJOR_ALARM = 2 -INVALID_ALARM = 3 +NO_ALARM = 0 +MINOR_ALARM = 1 +MAJOR_ALARM = 2 +INVALID_ALARM = 3 # Some alarm code definitions taken from EPICS alarm.h -READ_ALARM = 1 -WRITE_ALARM = 2 -HIHI_ALARM = 3 -HIGH_ALARM = 4 -LOLO_ALARM = 5 -LOW_ALARM = 6 -STATE_ALARM = 7 -COS_ALARM = 8 -COMM_ALARM = 9 -TIMEOUT_ALARM = 10 -HW_LIMIT_ALARM = 11 -CALC_ALARM = 12 -SCAN_ALARM = 13 -LINK_ALARM = 14 -SOFT_ALARM = 15 -BAD_SUB_ALARM = 16 -UDF_ALARM = 17 -DISABLE_ALARM = 18 -SIMM_ALARM = 19 -READ_ACCESS_ALARM = 20 -WRITE_ACCESS_ALARM = 21 - +READ_ALARM = 1 +WRITE_ALARM = 2 +HIHI_ALARM = 3 +HIGH_ALARM = 4 +LOLO_ALARM = 5 +LOW_ALARM = 6 +STATE_ALARM = 7 +COS_ALARM = 8 +COMM_ALARM = 9 +TIMEOUT_ALARM = 10 +HW_LIMIT_ALARM = 11 +CALC_ALARM = 12 +SCAN_ALARM = 13 +LINK_ALARM = 14 +SOFT_ALARM = 15 +BAD_SUB_ALARM = 16 +UDF_ALARM = 17 +DISABLE_ALARM = 18 +SIMM_ALARM = 19 +READ_ACCESS_ALARM = 20 +WRITE_ACCESS_ALARM = 21 diff --git a/softioc/builder.py b/softioc/builder.py index 029f9134..a0ef979f 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -6,7 +6,8 @@ from epicsdbbuilder import * from epicscorelibs import path -from epicscorelibs.ioc import iocshRegisterCommon, registerRecordDeviceDriver, pdbbase +from epicscorelibs.ioc import \ + iocshRegisterCommon, registerRecordDeviceDriver, pdbbase iocshRegisterCommon() dbLoadDatabase("base.dbd", os.path.join(path.base_path, "dbd")) @@ -18,7 +19,7 @@ InitialiseDbd() LoadDbdFile(os.path.join(os.path.dirname(__file__), 'device.dbd')) -from . import pythonSoftIoc +from . import pythonSoftIoc # noqa PythonDevice = pythonSoftIoc.PythonDevice() @@ -40,34 +41,33 @@ def _in_record(record, name, **fields): def aIn(name, LOPR=None, HOPR=None, **fields): - return _in_record('ai', name, - LOPR = LOPR, HOPR = HOPR, **fields) + return _in_record( + 'ai', name, LOPR = LOPR, HOPR = HOPR, **fields) def aOut(name, LOPR=None, HOPR=None, **fields): fields.setdefault('DRVL', LOPR) fields.setdefault('DRVH', HOPR) - return PythonDevice.ao(name, - LOPR = LOPR, HOPR = HOPR, **fields) + return PythonDevice.ao( + name, LOPR = LOPR, HOPR = HOPR, **fields) def boolIn(name, ZNAM=None, ONAM=None, **fields): return _in_record('bi', name, ZNAM = ZNAM, ONAM = ONAM, **fields) def boolOut(name, ZNAM=None, ONAM=None, **fields): - return PythonDevice.bo(name, - OMSL = 'supervisory', - ZNAM = ZNAM, ONAM = ONAM, **fields) + return PythonDevice.bo( + name, OMSL = 'supervisory', ZNAM = ZNAM, ONAM = ONAM, **fields) def longIn(name, LOPR=None, HOPR=None, EGU=None, **fields): fields.setdefault('MDEL', -1) - return _in_record('longin', name, - EGU = EGU, LOPR = LOPR, HOPR = HOPR, **fields) + return _in_record( + 'longin', name, EGU = EGU, LOPR = LOPR, HOPR = HOPR, **fields) def longOut(name, DRVL=None, DRVH=None, EGU=None, **fields): - return PythonDevice.longout(name, - OMSL = 'supervisory', - DRVL = DRVL, DRVH = DRVH, EGU = EGU, **fields) + return PythonDevice.longout( + name, OMSL = 'supervisory', DRVL = DRVL, DRVH = DRVH, EGU = EGU, + **fields) # Field name prefixes for mbbi/mbbo records. diff --git a/softioc/device.py b/softioc/device.py index d3f1d4ef..710d0791 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -96,7 +96,7 @@ def __init__(self, name, **kargs): else: self.__on_update = None - self.__validate = kargs.pop('validate', None) + self.__validate = kargs.pop('validate', None) self.__always_update = kargs.pop('always_update', False) self._value = kargs.pop('initial_value', None) self.__enable_write = True @@ -213,32 +213,36 @@ def get(self): def _Device(Base, record_type, rval=False, mlst=False, default=0): '''Wrapper for generating simple records.''' val_field = 'RVAL' if rval else 'VAL' + class GenericDevice(Base): _record_type_ = record_type _device_name_ = 'devPython_' + record_type - _val_field_ = val_field - _default_ = default - _fields_ = ['UDF', val_field] - if mlst: _fields_.append('MLST') + _val_field_ = val_field + _default_ = default + _fields_ = ['UDF', val_field] + if mlst: + _fields_.append('MLST') GenericDevice.__name__ = record_type return GenericDevice -_In = ProcessDeviceSupportIn +_In = ProcessDeviceSupportIn _Out = ProcessDeviceSupportOut -def _Device_In (type, **kargs): + +def _Device_In(type, **kargs): return _Device(_In, type, **kargs) + def _Device_Out(type, rval=False, mlst=True): return _Device(_Out, type, rval=rval, mlst=mlst, default=None) -longin = _Device_In ('longin') -longout = _Device_Out('longout') -bi = _Device_In ('bi', rval=True) -bo = _Device_Out('bo', rval=True) -stringin = _Device_In ('stringin', mlst=False, default='') -stringout = _Device_Out('stringout', mlst=False) -mbbi = _Device_In ('mbbi', rval=True) -mbbo = _Device_Out('mbbo', rval=True) +longin = _Device_In('longin') +longout = _Device_Out('longout') +bi = _Device_In('bi', rval=True) +bo = _Device_Out('bo', rval=True) +stringin = _Device_In('stringin', mlst=False, default='') +stringout = _Device_Out('stringout', mlst=False) +mbbi = _Device_In('mbbi', rval=True) +mbbo = _Device_Out('mbbo', rval=True) NO_CONVERT = 2 @@ -254,10 +258,10 @@ def _Device_Out(type, rval=False, mlst=True): class ai(ProcessDeviceSupportIn): _record_type_ = 'ai' _device_name_ = 'devPython_ai' - _val_field_ = 'VAL' - _default_ = 0.0 - _fields_ = ['UDF', 'VAL'] - _dset_extra_ = dset_process_linconv + _val_field_ = 'VAL' + _default_ = 0.0 + _fields_ = ['UDF', 'VAL'] + _dset_extra_ = dset_process_linconv def _process(self, record): _value = self._value @@ -271,9 +275,9 @@ def _process(self, record): class ao(ProcessDeviceSupportOut): _record_type_ = 'ao' _device_name_ = 'devPython_ao' - _val_field_ = 'VAL' - _fields_ = ['UDF', 'VAL', 'MLST'] - _dset_extra_ = dset_process_linconv + _val_field_ = 'VAL' + _fields_ = ['UDF', 'VAL', 'MLST'] + _dset_extra_ = dset_process_linconv def init_record(self, record): self.__super.init_record(record) @@ -304,12 +308,14 @@ def _read_value(self, record): def _write_value(self, record, value): value = numpy.require(value, dtype = self.dtype) - if value.shape == (): value.shape = (1,) + if value.shape == (): + value.shape = (1,) assert value.ndim == 1, 'Can\'t write multidimensional arrays' nelm = record.NELM nord = len(value) - if nord > nelm: nord = nelm + if nord > nelm: + nord = nelm memmove( record.BPTR, value.ctypes.data_as(c_void_p), self.dtype.itemsize * nord) @@ -319,7 +325,7 @@ def _write_value(self, record, value): class waveform(WaveformBase, ProcessDeviceSupportIn): _record_type_ = 'waveform' _device_name_ = 'devPython_waveform' - _default_ = () + _default_ = () # Because arrays are mutable values it's ever so easy to accidentially call # set() with a value which subsequently changes. To avoid this common class @@ -338,7 +344,8 @@ def set(self, value, value = numpy.require(value, dtype = self.dtype) self._value = (+value, severity, alarm, timestamp) - if value.shape == (): value.shape = (1,) + if value.shape == (): + value.shape = (1,) assert value.ndim == 1, 'Can\'t write multidimensional arrays' self.trigger() diff --git a/softioc/device_core.py b/softioc/device_core.py index 9024fa59..ab2ce92f 100644 --- a/softioc/device_core.py +++ b/softioc/device_core.py @@ -28,7 +28,8 @@ def __init__(cls, name, bases, dict): # Binds cls.__super_cls().method to the appropriate superclass # class method. Unfortunately the .__super form doesn't work # with class methods, only instance methods. - setattr(cls, '_%s__super_cls' % name, + setattr( + cls, '_%s__super_cls' % name, classmethod(lambda child: super(cls, child))) # Finally call the class initialisatio nmethod. cls.__init_class__() @@ -104,7 +105,7 @@ def __init_class__(cls): # Now construct the device support table. We build on the common # base and add any device specific definitions here. class DSET(DSET_BASE): - _fields_ = cls._dset_extra_[0] + _fields_ = cls._dset_extra_[0] _record_offsets_ = cls._dset_extra_[1] dset = DSET( number = len(DSET_BASE._fields_) + len(DSET._fields_) - 1) diff --git a/softioc/extension.c b/softioc/extension.c index 511e707c..d7bb3a3e 100644 --- a/softioc/extension.c +++ b/softioc/extension.c @@ -19,7 +19,8 @@ #endif /* Reference stealing version of PyDict_SetItemString */ -static void set_dict_item_steal(PyObject *dict, const char *name, PyObject *py_value) +static void set_dict_item_steal( + PyObject *dict, const char *name, PyObject *py_value) { PyDict_SetItemString(dict, name, py_value); Py_DECREF(py_value); @@ -57,25 +58,23 @@ static PyObject *get_DBF_values(PyObject *self, PyObject *args) /* Given an array of field names, this routine looks up each field name in * the EPICS database and returns the corresponding field offset. */ - static PyObject *get_field_offsets(PyObject *self, PyObject *args) { - int status; const char *record_type; - PyObject *dict = PyDict_New(); - if (!PyArg_ParseTuple(args, "s", &record_type)) return NULL; DBENTRY dbentry; dbInitEntry(pdbbase, &dbentry); - status = dbFindRecordType(&dbentry, record_type); + int status = dbFindRecordType(&dbentry, record_type); if (status != 0) printf("Unable to find record type \"%s\" (error %d)\n", record_type, status); else status = dbFirstField(&dbentry, 0); + + PyObject *dict = PyDict_New(); while (status == 0) { const char * field_name = dbGetFieldName(&dbentry); @@ -106,9 +105,11 @@ static PyObject *db_put_field(PyObject *self, PyObject *args) struct dbAddr dbAddr; if (dbNameToAddr(name, &dbAddr)) - return PyErr_Format(PyExc_RuntimeError, "dbNameToAddr failed for %s", name); + return PyErr_Format( + PyExc_RuntimeError, "dbNameToAddr failed for %s", name); if (dbPutField(&dbAddr, dbrType, pbuffer, length)) - return PyErr_Format(PyExc_RuntimeError, "dbPutField failed for %s", name); + return PyErr_Format( + PyExc_RuntimeError, "dbPutField failed for %s", name); Py_RETURN_NONE; } @@ -200,6 +201,7 @@ void EpicsPvPutHook(struct asTrapWriteMessage *pmessage, int after) pmessage->userPvt = value; } + static PyObject *install_pv_logging(PyObject *self, PyObject *args) { const char *acf_file; diff --git a/softioc/fields.py b/softioc/fields.py index 5a6dd74d..103b74ab 100644 --- a/softioc/fields.py +++ b/softioc/fields.py @@ -26,43 +26,43 @@ # downstream for no good purpose -- for example, enums are of type DBF_ULONG, # but this cannot be written with caput.) DbfCodeToCtypes = { - DBF_CHAR : c_byte, - DBF_UCHAR : c_ubyte, - DBF_SHORT : c_int16, - DBF_USHORT : c_uint16, - DBF_LONG : c_int32, - DBF_ULONG : c_int32, # Should be uint32, but causes trouble later. - DBF_FLOAT : c_float, - DBF_DOUBLE : c_double, - DBF_ENUM : c_uint16, - DBF_MENU : c_uint16, - DBF_INLINK : c_char_p, - DBF_OUTLINK : c_char_p, - DBF_NOACCESS : c_void_p, + DBF_CHAR: c_byte, + DBF_UCHAR: c_ubyte, + DBF_SHORT: c_int16, + DBF_USHORT: c_uint16, + DBF_LONG: c_int32, + DBF_ULONG: c_int32, # Should be uint32, but causes trouble later. + DBF_FLOAT: c_float, + DBF_DOUBLE: c_double, + DBF_ENUM: c_uint16, + DBF_MENU: c_uint16, + DBF_INLINK: c_char_p, + DBF_OUTLINK: c_char_p, + DBF_NOACCESS: c_void_p, } # Mapping for record field type to numpy type. DbfCodeToNumpy = { - DBF_STRING : numpy.dtype('S40'), - DBF_CHAR : numpy.dtype('int8'), - DBF_UCHAR : numpy.dtype('uint8'), - DBF_SHORT : numpy.dtype('int16'), - DBF_USHORT : numpy.dtype('uint16'), - DBF_LONG : numpy.dtype('int32'), - DBF_ULONG : numpy.dtype('uint32'), - DBF_FLOAT : numpy.dtype('float32'), - DBF_DOUBLE : numpy.dtype('float64'), + DBF_STRING: numpy.dtype('S40'), + DBF_CHAR: numpy.dtype('int8'), + DBF_UCHAR: numpy.dtype('uint8'), + DBF_SHORT: numpy.dtype('int16'), + DBF_USHORT: numpy.dtype('uint16'), + DBF_LONG: numpy.dtype('int32'), + DBF_ULONG: numpy.dtype('uint32'), + DBF_FLOAT: numpy.dtype('float32'), + DBF_DOUBLE: numpy.dtype('float64'), } # Mapping from basic DBR_ codes to DBF_ values DbrToDbfCode = { - DBR_STRING : DBF_STRING, - DBR_SHORT : DBF_SHORT, - DBR_FLOAT : DBF_FLOAT, - DBR_ENUM : DBF_ENUM, - DBR_CHAR : DBF_CHAR, - DBR_LONG : DBF_LONG, - DBR_DOUBLE : DBF_DOUBLE + DBR_STRING: DBF_STRING, + DBR_SHORT: DBF_SHORT, + DBR_FLOAT: DBF_FLOAT, + DBR_ENUM: DBF_ENUM, + DBR_CHAR: DBF_CHAR, + DBR_LONG: DBF_LONG, + DBR_DOUBLE: DBF_DOUBLE } diff --git a/softioc/pythonSoftIoc.py b/softioc/pythonSoftIoc.py index 49200588..84880c3e 100644 --- a/softioc/pythonSoftIoc.py +++ b/softioc/pythonSoftIoc.py @@ -87,7 +87,7 @@ def __init_class__(cls): 'ai', 'bi', 'longin', 'mbbi', 'stringin', 'ao', 'bo', 'longout', 'mbbo', 'stringout', 'waveform']: builder = getattr(epicsdbbuilder.records, name) - record = getattr(device, name) + record = getattr(device, name) setattr(cls, name, cls.makeRecord(builder, record)) cls.waveform_out = cls.makeRecord( epicsdbbuilder.records.waveform, device.waveform_out, @@ -96,7 +96,7 @@ def __init_class__(cls): class makeRecord: def __init__(self, builder, record, dtyp = 'Python'): self.builder = builder - self.record = record + self.record = record self.dtyp = dtyp def __call__(self, name, **fields): diff --git a/softioc/softioc.py b/softioc/softioc.py index 80029849..ee01f72a 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -38,6 +38,7 @@ def ExportTest(name, argtypes, defaults=(), description='no description yet'): f.restype = None length = len(argtypes) + def call_f(*args): missing = length - len(args) if missing > 0: @@ -54,41 +55,41 @@ def call_f(*args): auto_encode = imports.auto_encode -ExportTest('dba', (auto_encode,), (), - '''dba(field) +ExportTest('dba', (auto_encode,), (), '''\ +dba(field) Prints value of each field in dbAddr structure associated with field.''') -ExportTest('dbl', (auto_encode, auto_encode,), ('', ''), - '''dbl(pattern='', fields='') +ExportTest('dbl', (auto_encode, auto_encode,), ('', ''), '''\ +dbl(pattern='', fields='') Prints the names of records in the database matching pattern. If a (space separated) list of fields is also given then the values of the fields are also printed.''') -ExportTest('dbnr', (c_int,), (0,), - '''dbnr(all=0) +ExportTest('dbnr', (c_int,), (0,), '''\ +dbnr(all=0) Print number of records of each record type.''') -ExportTest('dbgrep', (auto_encode,), (), - '''dbgrep(pattern) +ExportTest('dbgrep', (auto_encode,), (), '''\ +dbgrep(pattern) Lists all record names that match the pattern. * matches any number of characters in a record name.''') -ExportTest('dbgf', (auto_encode,), (), - '''dbgf(field) +ExportTest('dbgf', (auto_encode,), (), '''\ +dbgf(field) Prints field type and value.''') -ExportTest('dbpf', (auto_encode, auto_encode,), (), - '''dbpf(field, value) +ExportTest('dbpf', (auto_encode, auto_encode,), (), '''\ +dbpf(field, value) Writes the given value into the field.''') -ExportTest('dbpr', (auto_encode, c_int,), (0,), - '''dbpr(record, interest=0) +ExportTest('dbpr', (auto_encode, c_int,), (0,), '''\ +dbpr(record, interest=0) Prints all the fields in record up to the indicated interest level: @@ -98,16 +99,16 @@ def call_f(*args): 3 System developer fields of minor interest 4 All other fields.''') -ExportTest('dbtr', (auto_encode,), (), - '''dbtr(record) +ExportTest('dbtr', (auto_encode,), (), '''\ +dbtr(record) Tests processing of the specified record.''') ExportTest('dbtgf', (auto_encode,)) ExportTest('dbtpf', (auto_encode, auto_encode,)) -ExportTest('dbior', (auto_encode, c_int,), ('', 0,), - '''dbior(driver='', interest=0) +ExportTest('dbior', (auto_encode, c_int,), ('', 0,), '''\ +dbior(driver='', interest=0) Prints driver reports for the selected driver (or all drivers if driver is omitted) at the given interest level.''') @@ -121,24 +122,24 @@ def call_f(*args): ExportTest('dblsr', (auto_encode, c_int,)) ExportTest('dbLockShowLocked', (c_int,)) -ExportTest('scanppl', (c_double,), (0.0,), - '''scanppl(rate=0.0) +ExportTest('scanppl', (c_double,), (0.0,), '''\ +scanppl(rate=0.0) Prints all records with the selected scan rate (or all if rate=0).''') -ExportTest('scanpel', (c_int,), (0,), - '''scanpel(event=0) +ExportTest('scanpel', (c_int,), (0,), '''\ +scanpel(event=0) Prints all records with selected event number (or all if event=0).''') -ExportTest('scanpiol', (), (), - '''Prints all records in the I/O event scan lists.''') +ExportTest('scanpiol', (), (), '''\ +Prints all records in the I/O event scan lists.''') -ExportTest('generalTimeReport', (c_int,), (0,), - '''Displays time providers and their status''') +ExportTest('generalTimeReport', (c_int,), (0,), '''\ +Displays time providers and their status''') -ExportTest('eltc', (c_int,), (), - '''Turn EPICS logging on or off.''') +ExportTest('eltc', (c_int,), (), '''\ +Turn EPICS logging on or off.''') # Hacked up exit object so that when soft IOC framework sends us an exit command diff --git a/tests/sim_records.py b/tests/sim_records.py index abbd826d..c8062f0e 100755 --- a/tests/sim_records.py +++ b/tests/sim_records.py @@ -21,36 +21,34 @@ def on_update(value): def on_update_name(value, name): print('on_update', name, ':', repr(value)) -t_ai = aIn('AI', initial_value=12.34) -t_boolin = boolIn('BOOLIN', 'True', 'False', initial_value=False) -t_longin = longIn('LONGIN', initial_value=33) -t_stringin = stringIn('STRINGIN', initial_value="Testing string") -t_mbbi = mbbIn('MBBI', 'One', 'Two', 'Three', initial_value=2) - -t_ao = aOut ('AO', - initial_value = 12.45, on_update_name = on_update_name) -t_boolout = boolOut ('BOOLOUT', 'Zero', 'One', - initial_value = True, on_update = on_update) -t_longout = longOut ('LONGOUT', - initial_value = 2008, on_update = on_update) -t_stringout = stringOut ('STRINGOUT', - initial_value = 'watevah', on_update = on_update) -t_mbbo = mbbOut ('MBBO', 'Ein', 'Zwei', 'Drei', - initial_value = 1, on_update = on_update) +t_ai = aIn('AI', initial_value=12.34) +t_boolin = boolIn('BOOLIN', 'True', 'False', initial_value=False) +t_longin = longIn('LONGIN', initial_value=33) +t_stringin = stringIn('STRINGIN', initial_value="Testing string") +t_mbbi = mbbIn('MBBI', 'One', 'Two', 'Three', initial_value=2) + +t_ao = aOut('AO', initial_value=12.45, on_update_name=on_update_name) +t_boolout = boolOut( + 'BOOLOUT', 'Zero', 'One', initial_value=True, on_update=on_update) +t_longout = longOut('LONGOUT', initial_value=2008, on_update=on_update) +t_stringout = stringOut( + 'STRINGOUT', initial_value='watevah', on_update=on_update) +t_mbbo = mbbOut( + 'MBBO', 'Ein', 'Zwei', 'Drei', initial_value=1, on_update=on_update) def update_sin_wf(value): print('update_sin_wf', value) sin_wf.set(numpy.sin( numpy.linspace(0, 2*numpy.pi*sin_ph.get(), sin_len.get()))) sin_wf = Waveform('SIN', datatype = float, length = 1024) -sin_len = longOut('SINN', 0, 1024, - initial_value = 1024, on_update = update_sin_wf) +sin_len = longOut( + 'SINN', 0, 1024, initial_value=1024, on_update=update_sin_wf) sin_ph = aOut('SINP', initial_value = 0.0, on_update = update_sin_wf) wf_len = 32 wf = numpy.sin(numpy.linspace(0, 2*numpy.pi, wf_len)) -t_waveform_in = Waveform('WAVEFORM', wf) +t_waveform_in = Waveform('WAVEFORM', wf) t_waveform_out = WaveformOut('WAVEFORM_OUT', wf, on_update = on_update) t_waveform_in2 = Waveform('WAVEFORM2', length = 10) From af652c271f686879aa127b866584544210eb4715 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Wed, 5 May 2021 16:00:03 +0100 Subject: [PATCH 10/19] Move registerRecordDeviceDriver to imports.py Otherwise import softioc.device will seg fault --- softioc/builder.py | 10 ---------- softioc/imports.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/softioc/builder.py b/softioc/builder.py index a0ef979f..cfd7efd0 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -5,16 +5,6 @@ from .softioc import dbLoadDatabase from epicsdbbuilder import * -from epicscorelibs import path -from epicscorelibs.ioc import \ - iocshRegisterCommon, registerRecordDeviceDriver, pdbbase - -iocshRegisterCommon() -dbLoadDatabase("base.dbd", os.path.join(path.base_path, "dbd")) -dbLoadDatabase("devIocStats.dbd", os.path.dirname(__file__)) - -if registerRecordDeviceDriver(pdbbase): - raise RuntimeError('Error registering') InitialiseDbd() LoadDbdFile(os.path.join(os.path.dirname(__file__), 'device.dbd')) diff --git a/softioc/imports.py b/softioc/imports.py index 74af7716..dd62934f 100644 --- a/softioc/imports.py +++ b/softioc/imports.py @@ -7,9 +7,12 @@ from ctypes import * from epicscorelibs import path +from epicscorelibs.ioc import \ + iocshRegisterCommon, registerRecordDeviceDriver, pdbbase from . import _extension + # These are in the extension def get_DBF_values(): """Return {DBF_name: DBF_int_value} mapping""" @@ -119,6 +122,13 @@ def EpicsDll(dll): epicsExit = libmiscIoc.epicsExit epicsExit.argtypes = () +iocshRegisterCommon() +dbLoadDatabase("base.dbd", os.path.join(path.base_path, "dbd"), None) +dbLoadDatabase("devIocStats.dbd", os.path.dirname(__file__), None) + +if registerRecordDeviceDriver(pdbbase): + raise RuntimeError('Error registering') + __all__ = [ 'get_field_offsets', From e801a500e40067c1606917fc439f60b7bee058da Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Wed, 5 May 2021 16:54:30 +0100 Subject: [PATCH 11/19] Build system for pipenv, docs, CI --- .github/pages/index.html | 9 + .github/workflows/code.yml | 109 +++++++++ .github/workflows/docs.yml | 69 ++++++ .gitignore | 69 +++++- Pipfile | 13 +- Pipfile.lock | 391 ++++++++++++++++++++----------- README.rst | 120 ++++------ docs/_static/theme_overrides.css | 4 + docs/_templates/layout.html | 23 ++ docs/conf.py | 216 ++++++----------- docs/images/softioc-favicon.ico | Bin 0 -> 16958 bytes docs/images/softioc-logo.svg | 315 +++++++++++++++++++++++++ docs/index.rst | 10 +- docs/pythonsoftioc.rst | 99 +++++++- docs/records.rst | 10 +- docs/softioc.rst | 26 +- iocStats | 1 + pyproject.toml | 2 +- setup.cfg | 7 +- setup.py | 6 +- softioc/__init__.py | 1 - softioc/imports.py | 35 +-- softioc/softioc.py | 9 +- tests/test_softioc.py | 8 +- 24 files changed, 1113 insertions(+), 439 deletions(-) create mode 100644 .github/pages/index.html create mode 100644 .github/workflows/code.yml create mode 100644 .github/workflows/docs.yml create mode 100644 docs/_static/theme_overrides.css create mode 100644 docs/_templates/layout.html create mode 100644 docs/images/softioc-favicon.ico create mode 100644 docs/images/softioc-logo.svg create mode 160000 iocStats diff --git a/.github/pages/index.html b/.github/pages/index.html new file mode 100644 index 00000000..cc33127d --- /dev/null +++ b/.github/pages/index.html @@ -0,0 +1,9 @@ + + + + Redirecting to master branch + + + + + diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml new file mode 100644 index 00000000..9dd39fb9 --- /dev/null +++ b/.github/workflows/code.yml @@ -0,0 +1,109 @@ +name: Code CI + +on: + push: + pull_request: + +jobs: + lint: + runs-on: "ubuntu-latest" + steps: + - name: Checkout Source + uses: actions/checkout@v2 + + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + + - name: Install Python Dependencies + run: pip install flake8 + + - name: Lint + run: flake8 + + build: + name: ${{ matrix.os }}/${{ matrix.python }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python: [cp27, cp37, cp38, cp39] + + include: + - os: macos-latest + TEST_REQUIRES: pytest-cov cothread + # Put coverage straight in the dist dir + COV_FILE: '{project}/dist/coverage.xml' + + - os: ubuntu-latest + TEST_REQUIRES: pytest-cov cothread + # Put coverage in the output dir mounted in docker + COV_FILE: '/output/coverage.xml' + + - os: windows-latest + # cothread doesn't work on windows + TEST_REQUIRES: pytest-cov + # Put coverage straight in the dist dir + COV_FILE: '{project}/dist/coverage.xml' + + steps: + - name: Checkout Source + uses: actions/checkout@v2 + with: + submodules: true + + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + + - name: Install Python Dependencies + run: pip install twine build cibuildwheel + + - name: Build Sdist + run: python -m build --sdist . + + - name: Build Wheel + run: cibuildwheel --output-dir dist + env: + CIBW_BUILD: ${{ matrix.python }}*64 + CIBW_TEST_REQUIRES: ${{ matrix.TEST_REQUIRES }} + CIBW_TEST_COMMAND: pytest --cov=softioc {project}/tests --cov-report xml:${{ matrix.COV_FILE }} + # Disable auditwheel as it isn't compatible with setuptools_dso approach + # https://github.com/mdavidsaver/setuptools_dso/issues/17 + CIBW_REPAIR_WHEEL_COMMAND: '' + + - name: Upload Wheel and Sdist + uses: actions/upload-artifact@v2 + with: + path: dist/softioc* + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + name: ${{ matrix.os }}/${{ matrix.python }} + directory: dist + + upload_pypi: + needs: [build] + runs-on: ubuntu-latest + # upload to PyPI on every tag + #if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + steps: + - uses: actions/download-artifact@v2 + with: + path: dist + + - name: Display structure of downloaded files + run: ls -R + + - name: Publish to PyPI + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.pypi_token }} + run: twine upload dist/* + diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..7f454cdb --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,69 @@ + +name: Docs CI + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Source + uses: actions/checkout@v2 + with: + # require all of history to see all tagged versions' docs + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.7" + + - name: Install Packages + # Can delete this if you don't use graphviz in your docs + run: sudo apt-get install graphviz + + - name: Install Python Dependencies + run: | + pip install pipenv + pipenv install --dev --deploy --python $(which python) && pipenv graph + + - name: Deploy index + # We pin to the SHA, not the tag, for security reasons. + # https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions + uses: peaceiris/actions-gh-pages@bbdfb200618d235585ad98e965f4aafc39b4c501 # v3.7.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: .github/pages + keep_files: true + + - name: Checkout gh-pages + # As we already did a deploy of gh-pages above, it is guaranteed to be there + # so check it out so we can selectively build docs below + uses: actions/checkout@v2 + with: + ref: gh-pages + path: build/html + + - name: Maybe use sphinx-multiversion + # If we are building master or a tag we will publish + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags') + # So use the args we normally pass to sphinx-build, but run sphinx-multiversion + run: mv $(pipenv --venv)/bin/sphinx-multiversion $(pipenv --venv)/bin/sphinx-build + + - name: Build Docs + run: pipenv run docs + + - name: Publish Docs to gh-pages + # Only master and tags are published + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags') + # We pin to the SHA, not the tag, for security reasons. + # https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions + uses: peaceiris/actions-gh-pages@bbdfb200618d235585ad98e965f4aafc39b4c501 # v3.7.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: build/html + keep_files: true + diff --git a/.gitignore b/.gitignore index 185e73b6..a39f5419 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,58 @@ -/.vscode -/venv -*.pyc -/docs/html/ -/softioc/_extension.* - -# Dist build output -/build -/dist -/softioc.egg-info/ - -# Coverage reports +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ .coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +*.mypy_cache +*.pytest_cache cov.xml +# DLS build dir and virtual environment +/prefix/ +/venv/ +/lightweight-venv/ +/installed.files + +# Docs output /docs/papers/*/*.aux /docs/papers/*/*.pdf /docs/papers/*/*.nav @@ -20,3 +60,8 @@ cov.xml /docs/papers/*/*.snm /docs/papers/*/*.toc /docs/papers/*/*.vrb + +# setup.py develop (via pipenv install) places this link here +# so editable installs work. Related: +# https://github.com/mdavidsaver/setuptools_dso/issues/11 +/epicscorelibs diff --git a/Pipfile b/Pipfile index 77850d53..ee099401 100644 --- a/Pipfile +++ b/Pipfile @@ -10,13 +10,20 @@ sphinx-rtd-theme = "*" # switch to main repo after PR https://github.com/Holzhaus/sphinx-multiversion/pull/60 is merged sphinx-multiversion = {editable = true,git = "https://github.com/dls-controls/sphinx-multiversion.git",ref = "only-arg"} setuptools-dso = "*" +aioca = "*" [packages] # All other package requirements from setup.py -softioc = {editable = true,path = "."} +softioc = {editable = true, path = "."} +# Apart from the epicscorelibs which comes from pyproject.toml so needs to be here too +epicscorelibs = "*" +# Add some other useful extras +cothread = "*" +scipy = "*" [scripts] # Put coverage here so we don't interfere with debugging in the IDE -tests = "python -m pytest --cov=epicsdbbuilder --cov-report term" +# Put flake8 here so we don't run it during cibuildwheel tests +tests = "python -m pytest --cov=softioc --cov-report term --flake8" docs = "sphinx-build -EWT --keep-going docs build/html" -clean = "rm -rf build prefix */__pycache__ .coverage cov.xml *.egg-info .mypy_cache .pytest_cache" +gitclean = "git clean -fdX" diff --git a/Pipfile.lock b/Pipfile.lock index e9f0a69f..e139a117 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "facfc60cd8deb78e8b7b88e3c72257aaa7c5faf1db8d612961fa1073173ef2dd" + "sha256": "7232f8aab61630fe29934f2891416df7f518e8aa75e0562764772f98027b6d60" }, "pipfile-spec": 6, "requires": {}, @@ -14,36 +14,43 @@ ] }, "default": { + "cothread": { + "hashes": [ + "sha256:50cf5b5a65e7fcfd113f06c2d6b1f363e37b0caee412c75ac7608377aa0bd10a" + ], + "index": "pypi", + "version": "==2.17" + }, "epicscorelibs": { "hashes": [ - "sha256:1a131556d13775f820db2e474a117e47b0d743ef262f0ebb58207d0bd7a2c6be", - "sha256:1b235379d23bb2f5746a791cf4abc982d0252e84d8d6f1ce52f3d028cf1c02c2", - "sha256:25a1f8f6cf97bee3513bcc81f17eb6acbe8ceb6a9d00d2bb713989cf120f5f9a", - "sha256:304cf11a9b05b20ba925564ff55ac524a80365cb27c0cc8df7f902345893c783", - "sha256:3221c993009ffd893b13d136e4c31e3dcf2e7b34dc3321e583534185e521c0b5", - "sha256:328276f0aa4dcf367c3c96d0aa7bc7f89b845d7c3c5ac2bd0eacd960c880006c", - "sha256:37c0f6889cebd2d8138d457d4a884f96854e9b43eafc8deaf20161d6ca86f733", - "sha256:39bdfb7b931dceecb8dff3fb6ed9689aad542ff1b4f4f48fd89cb71bb565f14c", - "sha256:46a4e6f1c26eb460d56a55f93230185555cb7e9874859996fc8828515616673e", - "sha256:4dafa4f28ce858338bb04398da4a67014b3f6522c609ed9c3c9317ad59cc4045", - "sha256:55cdeaa11292db310273c1e408d09030c1a71b27237818ea2cd22b6e03859ba6", - "sha256:56ba9513d9e44b19992e4a7e975c059b669d04f705e72e2967cc71c02c5b7a5b", - "sha256:5769b8aea4427513c3fd9e8dd08612bdaa94e2ee4975ae9436a8cb6f633e3ecb", - "sha256:5a72ef5e9aa2a467dbc31fb1a6bfcac44eeb2d50802991061a130773565a67f0", - "sha256:5f165e984c123164d58afeb0d8ca8cfddb20bc3603d21837f7232afc1cff0471", - "sha256:6b8e63342c2d0778d81735fd7bba0467778ddf0d2997a7ee30308b0322ed7e88", - "sha256:6e111d499db1a496bf3331db781894d2da301b1439e32aa464bfd30bf1bae192", - "sha256:8afa93e18d26e25168cda51e5aa72054c35d50029f7057d5ba3c9108fdb2751f", - "sha256:9b4be973536644aab49e67a42c41f4922f32b7106cde8f360b9ffe198f971c1a", - "sha256:a28f9c45b2c4a273ac9735245145b80247908c9aceab3f01550756023d1c61fd", - "sha256:a5e4bc0722c48e872f0fc5752b514fe9167ab1dce6cfdad79687f8d2a130d0c1", - "sha256:db59c3bf4fda65eb49ad5ab4933aa182c764200c8ed57d8e5474d7accbbcb609", - "sha256:e6dd41d204565bccc7126eb43a19187c6ff4a2c828bf2a79161cb4cef19cbef9", - "sha256:f7360c05368df5036d87b48a8b97c1d45970dfe4fca9f853b862a0609f2cf093", - "sha256:fa30a31d60173a83c185caf9e77c2666f3f20dd9b7c6ffd3aa49ef511199f258" - ], - "markers": "python_version >= '2.7'", - "version": "==7.0.4.99.0.0a1" + "sha256:0fa5dc4fe3762b8e1380e1e9ebcff4d21506a89c635cb49c00c0be9c65663279", + "sha256:15bdbeda1e445f13f5e94bf6665cbffebd76b461d2bfb782d453166ad93a6e4e", + "sha256:16257b172e55cdbff2b7fe8ecc48d00e782e714996c01fd3ef63abbcfec4f6fd", + "sha256:1bb8cb3a810b1c60be7c8ee060840eb9b5b6ba24337473b7ce072ea70470a8a9", + "sha256:2497532712a608dba29a0da07e094d425e3eda3e3a29c5ee3f3931b1dd78277b", + "sha256:2bedc3915709119c6bf8cdd40bf30682570ee24308556ce5d7b8658804c4da38", + "sha256:31c7e298498a78ea4d1ac5eb3d1d794df3fb62c54e44ba5d489a1a6771f416a1", + "sha256:350f540864f642be73ea0a12c52520b2c050e65027e21ce3c5293f5cbfea3b20", + "sha256:3748d0f7e04190ea3910d552a8915f3572f4f50fa9e231f8e03a14b01362ae79", + "sha256:4190673b94c3021bb0493484f7128b45c055af47ecb34433a913c5dd496f56e7", + "sha256:51bf0e1118125ba7d8c485b515579cd1210024a876eaa65d1084f51278414476", + "sha256:5f6e2d9ffb3e89e48115dae36c532aded656c047840b6788d55afbd009399a30", + "sha256:5fade32ea523f2ace0021d973046460c4baaa4ca8ea4d4523299e14c6f42cf4e", + "sha256:88a4695f807207f19e9d38084df5c799c8998c3ba7f63b81de5f2d7370a64d8b", + "sha256:9e8ffda8298698b83fd6a3f728ec1574de4f442145462951a2a96ea3e3a165e5", + "sha256:a84b5a7a17963d5bba02c7cdc536cf1dd7d66fa600aaca09a0d6c2425c069db3", + "sha256:ac5296922499c8a5090a1b1f5d1ab36b6472d99bb93c0fdcd7885eefb798f031", + "sha256:b031ef91321daa3eb30120d8a2e55aa7545480038e76655ac0fe386966260f0d", + "sha256:b86592c6777d877af1168c71b07476d30514334837523e0b34c73a0e99f594d7", + "sha256:e004f31e74397bc1c6a8b313570620ac14272a9867a11dcd8be5a4a9a10bec85", + "sha256:e18ba8f3979a7f69b22069445eb76733829f40706153bdc2d62e6cbc4d4634f4", + "sha256:e2435e8774669f623dd70688b6b3f884cd150a744186fba544b0d867468a6a7e", + "sha256:e33f3fc053238b751e33de97d3d163c6dabfcf4202b477e353542300d69f6c1f", + "sha256:e8cc04309f8b885c61bc0e6562a2af80c097f295d3dbb6e375e5bc80717d6e53", + "sha256:f2c8a85042aae0bf5b949d0aedfdf4ed08ea8498c1d25019c7f5c671708d87d2" + ], + "index": "pypi", + "version": "==7.0.4.99.1.2" }, "epicsdbbuilder": { "hashes": [ @@ -54,40 +61,74 @@ }, "numpy": { "hashes": [ - "sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29", - "sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab", - "sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04", - "sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d", - "sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590", - "sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f", - "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a", - "sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845", - "sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090", - "sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b", - "sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e", - "sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc", - "sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257", - "sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e", - "sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff", - "sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9", - "sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b", - "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e", - "sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0", - "sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb", - "sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f", - "sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2", - "sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14", - "sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9" + "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727", + "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6", + "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98", + "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7", + "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d", + "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2", + "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9", + "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935", + "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff", + "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee", + "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb", + "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042", + "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3", + "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5", + "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6", + "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f", + "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4", + "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737", + "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931", + "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6", + "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677", + "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576", + "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935", + "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd" ], "markers": "python_version >= '3.7'", - "version": "==1.20.1" + "version": "==1.20.2" + }, + "scipy": { + "hashes": [ + "sha256:01b38dec7e9f897d4db04f8de4e20f0f5be3feac98468188a0f47a991b796055", + "sha256:10dbcc7de03b8d635a1031cb18fd3eaa997969b64fdf78f99f19ac163a825445", + "sha256:19aeac1ad3e57338723f4657ac8520f41714804568f2e30bd547d684d72c392e", + "sha256:1b21c6e0dc97b1762590b70dee0daddb291271be0580384d39f02c480b78290a", + "sha256:1caade0ede6967cc675e235c41451f9fb89ae34319ddf4740194094ab736b88d", + "sha256:23995dfcf269ec3735e5a8c80cfceaf384369a47699df111a6246b83a55da582", + "sha256:2a799714bf1f791fb2650d73222b248d18d53fd40d6af2df2c898db048189606", + "sha256:3274ce145b5dc416c49c0cf8b6119f787f0965cd35e22058fe1932c09fe15d77", + "sha256:33d1677d46111cfa1c84b87472a0274dde9ef4a7ef2e1f155f012f5f1e995d8f", + "sha256:44d452850f77e65e25b1eb1ac01e25770323a782bfe3a1a3e43847ad4266d93d", + "sha256:9e3302149a369697c6aaea18b430b216e3c88f9a61b62869f6104881e5f9ef85", + "sha256:a75b014d3294fce26852a9d04ea27b5671d86736beb34acdfc05859246260707", + "sha256:ad7269254de06743fb4768f658753de47d8b54e4672c5ebe8612a007a088bd48", + "sha256:b30280fbc1fd8082ac822994a98632111810311a9ece71a0e48f739df3c555a2", + "sha256:b79104878003487e2b4639a20b9092b02e1bad07fc4cf924b495cf413748a777", + "sha256:d449d40e830366b4c612692ad19fbebb722b6b847f78a7b701b1e0d6cda3cc13", + "sha256:d647757373985207af3343301d89fe738d5a294435a4f2aafb04c13b4388c896", + "sha256:f68eb46b86b2c246af99fcaa6f6e37c7a7a413e1084a794990b877f2ff71f7b6", + "sha256:fdf606341cd798530b05705c87779606fcdfaf768a8129c348ea94441da15b04" + ], + "index": "pypi", + "version": "==1.6.3" }, "softioc": { "editable": true, + "extras": [], "path": "." } }, "develop": { + "aioca": { + "hashes": [ + "sha256:5caa44ca048e6b6a409255be1da21ca3b6b7df33117cb16f48102e1cb2979311", + "sha256:a1805c81c3e3f3cbc672b567ea82729dcb1345faf80c1bb07c3cf22f687503b6" + ], + "index": "pypi", + "version": "==1.0" + }, "alabaster": { "hashes": [ "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359", @@ -97,19 +138,19 @@ }, "attrs": { "hashes": [ - "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", - "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.3.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" }, "babel": { "hashes": [ - "sha256:9d35c22fcc79893c3ecc85ac4a56cde1ecf3f19c540bba0922308a6c06ca6fa5", - "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05" + "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9", + "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.0" + "version": "==2.9.1" }, "certifi": { "hashes": [ @@ -128,58 +169,61 @@ }, "coverage": { "hashes": [ - "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7", - "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5", - "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f", - "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde", - "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f", - "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f", - "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c", - "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66", - "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90", - "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337", - "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d", - "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4", - "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409", - "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37", - "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1", - "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247", - "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39", - "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c", - "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994", - "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c", - "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb", - "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc", - "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f", - "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca", - "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135", - "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3", - "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339", - "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9", - "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9", - "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af", - "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370", - "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19", - "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3", - "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44", - "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3", - "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a", - "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c", - "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b", - "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9", - "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8", - "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22", - "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f", - "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345", - "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880", - "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0", - "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b", - "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec", - "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3", - "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786" + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==5.4" + "version": "==5.5" }, "docutils": { "hashes": [ @@ -189,13 +233,44 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.16" }, + "epicscorelibs": { + "hashes": [ + "sha256:0fa5dc4fe3762b8e1380e1e9ebcff4d21506a89c635cb49c00c0be9c65663279", + "sha256:15bdbeda1e445f13f5e94bf6665cbffebd76b461d2bfb782d453166ad93a6e4e", + "sha256:16257b172e55cdbff2b7fe8ecc48d00e782e714996c01fd3ef63abbcfec4f6fd", + "sha256:1bb8cb3a810b1c60be7c8ee060840eb9b5b6ba24337473b7ce072ea70470a8a9", + "sha256:2497532712a608dba29a0da07e094d425e3eda3e3a29c5ee3f3931b1dd78277b", + "sha256:2bedc3915709119c6bf8cdd40bf30682570ee24308556ce5d7b8658804c4da38", + "sha256:31c7e298498a78ea4d1ac5eb3d1d794df3fb62c54e44ba5d489a1a6771f416a1", + "sha256:350f540864f642be73ea0a12c52520b2c050e65027e21ce3c5293f5cbfea3b20", + "sha256:3748d0f7e04190ea3910d552a8915f3572f4f50fa9e231f8e03a14b01362ae79", + "sha256:4190673b94c3021bb0493484f7128b45c055af47ecb34433a913c5dd496f56e7", + "sha256:51bf0e1118125ba7d8c485b515579cd1210024a876eaa65d1084f51278414476", + "sha256:5f6e2d9ffb3e89e48115dae36c532aded656c047840b6788d55afbd009399a30", + "sha256:5fade32ea523f2ace0021d973046460c4baaa4ca8ea4d4523299e14c6f42cf4e", + "sha256:88a4695f807207f19e9d38084df5c799c8998c3ba7f63b81de5f2d7370a64d8b", + "sha256:9e8ffda8298698b83fd6a3f728ec1574de4f442145462951a2a96ea3e3a165e5", + "sha256:a84b5a7a17963d5bba02c7cdc536cf1dd7d66fa600aaca09a0d6c2425c069db3", + "sha256:ac5296922499c8a5090a1b1f5d1ab36b6472d99bb93c0fdcd7885eefb798f031", + "sha256:b031ef91321daa3eb30120d8a2e55aa7545480038e76655ac0fe386966260f0d", + "sha256:b86592c6777d877af1168c71b07476d30514334837523e0b34c73a0e99f594d7", + "sha256:e004f31e74397bc1c6a8b313570620ac14272a9867a11dcd8be5a4a9a10bec85", + "sha256:e18ba8f3979a7f69b22069445eb76733829f40706153bdc2d62e6cbc4d4634f4", + "sha256:e2435e8774669f623dd70688b6b3f884cd150a744186fba544b0d867468a6a7e", + "sha256:e33f3fc053238b751e33de97d3d163c6dabfcf4202b477e353542300d69f6c1f", + "sha256:e8cc04309f8b885c61bc0e6562a2af80c097f295d3dbb6e375e5bc80717d6e53", + "sha256:f2c8a85042aae0bf5b949d0aedfdf4ed08ea8498c1d25019c7f5c671708d87d2" + ], + "index": "pypi", + "version": "==7.0.4.99.1.2" + }, "flake8": { "hashes": [ - "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", - "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" + "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", + "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==3.8.4" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.9.2" }, "idna": { "hashes": [ @@ -293,6 +368,36 @@ ], "version": "==0.6.1" }, + "numpy": { + "hashes": [ + "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727", + "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6", + "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98", + "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7", + "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d", + "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2", + "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9", + "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935", + "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff", + "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee", + "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb", + "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042", + "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3", + "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5", + "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6", + "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f", + "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4", + "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737", + "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931", + "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6", + "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677", + "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576", + "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935", + "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd" + ], + "markers": "python_version >= '3.7'", + "version": "==1.20.2" + }, "packaging": { "hashes": [ "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", @@ -319,43 +424,43 @@ }, "pycodestyle": { "hashes": [ - "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", - "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" + "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", + "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.6.0" + "version": "==2.7.0" }, "pyflakes": { "hashes": [ - "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", - "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", + "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.2.0" + "version": "==2.3.1" }, "pygments": { "hashes": [ - "sha256:37a13ba168a02ac54cc5891a42b1caec333e59b66addb7fa633ea8a6d73445c0", - "sha256:b21b072d0ccdf29297a82a2363359d99623597b8a265b8081760e4d0f7153c88" + "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", + "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" ], "markers": "python_version >= '3.5'", - "version": "==2.8.0" + "version": "==2.9.0" }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pytest": { "hashes": [ - "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", - "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" + "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", + "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" ], "markers": "python_version >= '3.6'", - "version": "==6.2.2" + "version": "==6.2.4" }, "pytest-cov": { "hashes": [ @@ -404,11 +509,11 @@ }, "sphinx": { "hashes": [ - "sha256:11d521e787d9372c289472513d807277caafb1684b33eb4f08f7574c405893a9", - "sha256:e90161222e4d80ce5fc811ace7c6787a226b4f5951545f7f42acf97277bfc35c" + "sha256:904e02cd0f84bed5d3748358c228ae3df3ad725b9e6cdc2166e17b309ed2e1fa", + "sha256:b246ebd74f5fb966d7e90086bbda5ed74ee4d30b4c3cbefddc1fb5210aa317c7" ], - "markers": "python_version >= '3.5'", - "version": "==3.5.1" + "markers": "python_version >= '3.6'", + "version": "==4.0.0" }, "sphinx-multiversion": { "editable": true, @@ -417,11 +522,11 @@ }, "sphinx-rtd-theme": { "hashes": [ - "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5", - "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113" + "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a", + "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f" ], "index": "pypi", - "version": "==0.5.1" + "version": "==0.5.2" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -476,16 +581,24 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "version": "==3.10.0.0" + }, "urllib3": { "hashes": [ - "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", - "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" + "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", + "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.3" + "version": "==1.26.4" } } } diff --git a/README.rst b/README.rst index cebb953a..b2e808e5 100644 --- a/README.rst +++ b/README.rst @@ -1,89 +1,73 @@ -.. default-role:: literal +PythonIOC +========= -Building `pythonSoftIoc` -======================== +|code_ci| |docs_ci| |coverage| |pypi_version| |license| -To build `pythonSoftIoc` follow these steps: -1. Edit the following files: +This module allows an EPICS IOC with Python Device Support to be run from within +the Python interpreter. Records can be programmatically created and arbitrary +Python code run to updated them and respond to caputs. It supports cothread and +asyncio for concurrency. - `configure/RELEASE` - Set `EPICS_BASE` to point to your local installation of EPICS. +============== ============================================================== +PyPI ``pip install softioc`` +Source code https://github.com/dls-controls/pythonIoc +Documentation https://dls-controls.github.io/pythonIoc +============== ============================================================== - `configure/CONFIG_SITE` - Set `PYTHON` to the executable name of your Python interpreter. +A simple example of the use of this library is the following: -2. Build `pythonIoc` without documentation by running:: +.. code:: python - make BUILD_DOCS= + # Import the basic framework components. + from softioc import softioc, builder + import cothread - At this point you should be able to run `./pythonIoc` which will give you an - interative Python interpreter. + # Set the record prefix + builder.SetDeviceName("MY-DEVICE-PREFIX") -3. Now ensure that you have numpy_, cothread_, and epicsdbbuilder_ installed. These can - be installed unversioned, or versioned (in which case - `pkg_resources.require` will need to be used), or just localling built and - added to your `PYTHONPATH`. + # Create some records + ai = builder.aIn('AI', initial_value=5) + ao = builder.aOut('AO', initial_value=12.45, on_update=lambda v: ai.set(v)) -4. Now check that `pythonIoc` works by running `example/runtest`. You may need - to edit `example/version.py` if you're using a versioned install of - `cothread` and `epicsdbbuilder`. + # Boilerplate get the IOC started + builder.LoadDatabase() + softioc.iocInit() -5. Finally build the documentation by running:: + # Start processes required to be run after iocInit + def update(): + while True: + ai.set(ai.get() + 1) + cothread.Sleep(1) - make docs + cothread.Spawn(update) - Again, if your installation of components is versioned you may need to edit - `docs/conf.py` as appropriate. + # Finally leave the IOC running with an interactive shell. + softioc.interactive_ioc(globals()) -Using `pythonSoftIoc` -===================== +.. |code_ci| image:: https://github.com/dls-controls/pythonIoc/workflows/Code%20CI/badge.svg?branch=master + :target: https://github.com/dls-controls/pythonIoc/actions?query=workflow%3A%22Code+CI%22 + :alt: Code CI -Probably the best way to use `pythonSoftIoc` is to start by copying fragments -of a simple example such as `CS-DI-IOC-02`. This consists of the following -elements: +.. |docs_ci| image:: https://github.com/dls-controls/pythonIoc/workflows/Docs%20CI/badge.svg?branch=master + :target: https://github.com/dls-controls/pythonIoc/actions?query=workflow%3A%22Docs+CI%22 + :alt: Docs CI -1. A startup shell script `start-ioc` which launches the soft IOC using a - production build of `pythonSoftIoc`. This script typically looks like - this:: +.. |coverage| image:: https://codecov.io/gh/dls-controls/pythonIoc/branch/master/graph/badge.svg + :target: https://codecov.io/gh/dls-controls/pythonIoc + :alt: Test Coverage - #!/bin/sh +.. |pypi_version| image:: https://img.shields.io/pypi/v/pythonIoc.svg + :target: https://pypi.org/project/pythonIoc + :alt: Latest PyPI version - PYIOC=/path/to/pythonSoftIoc/pythonIoc +.. |license| image:: https://img.shields.io/badge/License-Apache%202.0-blue.svg + :target: https://opensource.org/licenses/Apache-2.0 + :alt: Apache License - cd "$(dirname "$0")" - exec $PYIOC start_ioc.py "$@" +.. + Anything below this line is used when viewing README.rst and will be replaced + when included in index.rst -2. The startup Python script. This establishes the essential component - versions (apart from the `pythonSoftIoc` version), performs the appropriate - initialisation and starts the IOC running. The following template is a - useful starting point:: - - from pkg_resources import require - require('cothread==2.12') - require('epicsdbbuilder==1.0') - - # Import the basic framework components. - from softioc import softioc, builder - import cothread - - # Import any modules required to run the IOC - import ... - - # Boilerplate get the IOC started - builder.LoadDatabase() - softioc.iocInit() - - # Start processes required to be run after iocInit - ... - - # Finally leave the IOC running with an interactive shell. - softioc.interactive_ioc(globals()) - - Note that the use of `require` is specific to DLS, and you may have a - different way of managing your installations. - -.. _numpy: http://www.numpy.org/ -.. _cothread: https://github.com/dls-controls/cothread -.. _epicsdbbuilder: https://github.com/Araneidae/epicsdbbuilder +See https://dls-controls.github.io/pythonIoc for more detailed documentation. \ No newline at end of file diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css new file mode 100644 index 00000000..95ed8f87 --- /dev/null +++ b/docs/_static/theme_overrides.css @@ -0,0 +1,4 @@ +/* allow us to stop margin after list so we can add to it */ +.no-margin-after-ul ul { + margin-bottom: 0 !important; +} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 00000000..58cc9b88 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,23 @@ +{% extends "!layout.html" %} +{% block menu %} + + {{ super() }} + + + Index + + + {% if versions %} +

Versions

+
    + {%- for item in versions|reverse %} +
  • {{ item.name }}
  • + {%- endfor %} +
+ {% endif %} +{% endblock %} diff --git a/docs/conf.py b/docs/conf.py index df1c61c7..bdedd0ef 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,197 +10,121 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) +repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, repo_root) -from pkg_resources import require -require('numpy') -require('cothread') -require('epicsdbbuilder') +import softioc # noqa +# General information about the project. +project = u'Python Soft IOC' +copyright = u'2011, Michael Abbott' + +# The full version, including alpha/beta/rc tags. +release = softioc.__version__ + +# The short X.Y version. +if '+' in release: + # Not on a tag + version = 'master' +else: + version = release # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ -# 'sphinx.ext.intersphinx', # Cross project documentation? -# 'sphinx.ext.todo', # Support todo entries in code - 'sphinx.ext.viewcode', # Adds links to source code + # For linking to external sphinx documentation + 'sphinx.ext.intersphinx', + # Add links to source code in API docs + 'sphinx.ext.viewcode', + # Add multiple versions of documentation on CI + 'sphinx_multiversion', ] viewcode_import = True +# If true, Sphinx will warn about all references where the target cannot +# be found. +nitpicky = True + +# A list of (type, target) tuples (by default empty) that should be ignored when +# generating warnings in "nitpicky mode". Note that type should include the +# domain name if present. Example entries would be ('py:func', 'int') or +# ('envvar', 'LD_LIBRARY_PATH'). +nitpick_ignore = [('py:func', 'int')] # Add any paths that contain templates here, relative to this directory. -# templates_path = ['_templates'] +templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' -# The encoding of source files. -#source_encoding = 'utf-8' - # The master toctree document. master_doc = 'index' -# General information about the project. -project = u'Python Soft IOC' -copyright = u'2011, Michael Abbott' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -# version = 'v1-17' -# # The full version, including alpha/beta/rc tags. -# release = 'v1-17' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of documents that shouldn't be included in the build. -#unused_docs = [] - # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - +# This means you can link things like `str` and `asyncio` to the relevant +# docs in the python documentation. +intersphinx_mapping = dict( + python=('https://docs.python.org/3/', None), + cothread=("https://cothread.readthedocs.org/en/stable/", None), + epicsdbbuilder=( + "https://dls-controls.github.io/epicsdbbuilder/master/", None) +) # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None +try: + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' +except ImportError: + html_theme = 'default' -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None +# Options for the sphinx rtd theme, use DLS blue +html_theme_options = dict(style_nav_header_background='rgb(7, 43, 93)') # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_use_modindex = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' - -# Output file base name for HTML help builder. -# htmlhelp_basename = 'Cothreaddoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +html_static_path = ['_static'] -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -# latex_documents = [ -# ('index', 'Cothread.tex', u'Cothread Documentation', -# u'Michael Abbott', 'manual'), -# ] +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = True -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False +# Add some CSS classes for columns and other tweaks in a custom css file +html_css_files = ['theme_overrides.css'] -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# Logo +html_logo = 'images/softioc-logo.svg' +html_favicon = 'images/softioc-favicon.ico' -# Documents to append as an appendix to all manuals. -#latex_appendices = [] +# sphinx-multiversion config +smv_rebuild_tags = False +smv_tag_whitelist = r'^\d+\.\d+.*$' # only document tags with form 0.9* +smv_branch_whitelist = r'^master$' # only branch to document is master +smv_outputdir_format = '{ref.name}' +smv_prefer_remote_refs = False +smv_remote_whitelist = 'origin|github' -# If false, no module index is generated. -#latex_use_modindex = True +# Common links that should be available on every page +rst_epilog = """ +.. _epicscorelibs: https://github.com/mdavidsaver/epicscorelibs +""" diff --git a/docs/images/softioc-favicon.ico b/docs/images/softioc-favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b44d890a32feb46fb43a98f5f708000f9fa60641 GIT binary patch literal 16958 zcmeHOdr(wW7(c``Ic80a=`NOum4%_OFEbU5afYd2Eh3FTEAzo)(J31hP5;=5${e4B z$S%7pBU6U&oW_aD5hjIQ#=;PYvB(H#vaunA1YvjBukYOTZujo4%iY(Ejps7op8MdO zbAP|@JihO{=MWNwe<>*hf8$8}d_u+(LgG>I7SW^7`|My0Fa{U{i~%tNX`1n=>1ugV zz@mzMM_5vsE@4c$PO;ROulT@Fp!_&gDc2>vZ_p)WeX{!TdxZ6H=Vi5Hh^Zl)n62wk zih`DkhUd~%@>0YOK}awEUZz!vlMMw)U4{Y$gjrMc~HAu3w1j}m)h;=P`x3U;}hG|RyTf& zt%l;BSknawXL}%_{#w8?<@8k-cLMx7-_cT1jq1lx=38vjm+z1O@-9FAza%e16f+1gV znZv)oaV*vS*kc!i#!h)-_?iB>u`>+)pS6G1|JnG*#(y^d{lDk`W%DP&%G5YcN>(KL zv<1uZlDOv?Gbgz1KPlt?1xq$m7tQf0htXyh+B=@-f!aSae{{fu$A9)KF-O|H>_4IZ zvuoLOpZH6269s;ftcTiUQt&sM(T=@^Z)9Gr4l905(~xw(2ItLMG_q`pe_h5D zC|#LE?MJZ`ac{0*!a)Sb-q$I z|GDfxHvcpGhqtGg{Wo&_OW^-7`S~Eo=3Tmj=yzZ4%qf&`jxZ0U*fn4760rOC#ZiI^ec4D(Czm9|9r)( zVZkqDUC`~g{)ORZ`VYtW$J+nM>jDGm{}qL5Fczvr^OkV^qm=p2^P4ukZmW7zG|xq{ z|EIKxIgx~4r-bHHRp77>f_<wOMHB}JD1}i6#U)o---$yw%-8CNhI*s_lc^4t9F37 z64!rH-FqIk<0T>EANZ@ovsk!vQXdig-43s+AOqWk6#ai=TIZGd#Lnd*?@?xv1$YGYk#<1B2JELBr7<;pzW~wYReooWl+n>iZKqzupqD z{!ev%?mB=22Y`+n0Ly>%?*Ce~teE;fA1z66wVu_Ec#zt*~?r#2R9~*?u-1tAlJVqd#sgr+y9n^En4OHSrq@G*$+84 zEgWGFM9x#NI_)u+|A+fV-1zeqUr>$?zc&~6Kcok||Kaqubog}bEDrzC&GSV0jg)_W z{X4DDz5n5IQ2QU^zw3s3KfmDee=N3ST4b6gb`5gipgc$s-22h;f>J(?=B3A;Lgggf zr}B%iorLvVh@b9%u~@Hg{#z(}U@X=W*W0^((`Jm5BM*6q|CW29wCdP*G4AoAT21a2 z=B4MAOH)P}bjpR;KRyg~?~`|a#YRzmbpMmNAqP1KV(*jfZBkJ`LsTBSU<@z@7z1}8 z10rM8&jpF9BV+>-6s0Fb;gQDCYm^A4`e<@LiHm~UHH|cW@ACCG`NkCWp?B#esxG>Y NkY`A=A61GT{R<*=R}cUI literal 0 HcmV?d00001 diff --git a/docs/images/softioc-logo.svg b/docs/images/softioc-logo.svg new file mode 100644 index 00000000..2ad9ccd3 --- /dev/null +++ b/docs/images/softioc-logo.svg @@ -0,0 +1,315 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/docs/index.rst b/docs/index.rst index 52efac8a..f156b34c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,9 +1,9 @@ .. _index: -Documentation for Python Soft IOC -================================= +.. include:: ../README.rst + :end-before: when included in index.rst -Contents: +.. rst-class:: no-margin-after-ul .. toctree:: :numbered: @@ -13,10 +13,6 @@ Contents: records softioc - -Indices and tables -================== - * :ref:`genindex` * :ref:`modindex` * :ref:`search` diff --git a/docs/pythonsoftioc.rst b/docs/pythonsoftioc.rst index baa7743c..7fae50a3 100644 --- a/docs/pythonsoftioc.rst +++ b/docs/pythonsoftioc.rst @@ -6,6 +6,95 @@ Python Soft IOC The ``pythonIoc`` command is a tool for creating and running an EPICS IOC entirely within Python. +Building `pythonSoftIoc` +======================== + +To build `pythonSoftIoc` follow these steps: + +1. Edit the following files: + + `configure/RELEASE` + Set `EPICS_BASE` to point to your local installation of EPICS. + + `configure/CONFIG_SITE` + Set `PYTHON` to the executable name of your Python interpreter. + +2. Build `pythonIoc` without documentation by running:: + + make BUILD_DOCS= + + At this point you should be able to run `./pythonIoc` which will give you an + interative Python interpreter. + +3. Now ensure that you have numpy_, cothread_, and epicsdbbuilder_ installed. These can + be installed unversioned, or versioned (in which case + `pkg_resources.require` will need to be used), or just localling built and + added to your `PYTHONPATH`. + +4. Now check that `pythonIoc` works by running `example/runtest`. You may need + to edit `example/version.py` if you're using a versioned install of + `cothread` and `epicsdbbuilder`. + +5. Finally build the documentation by running:: + + make docs + + Again, if your installation of components is versioned you may need to edit + `docs/conf.py` as appropriate. + + +Using `pythonSoftIoc` +===================== + +Probably the best way to use `pythonSoftIoc` is to start by copying fragments +of a simple example such as `CS-DI-IOC-02`. This consists of the following +elements: + +1. A startup shell script `start-ioc` which launches the soft IOC using a + production build of `pythonSoftIoc`. This script typically looks like + this:: + + #!/bin/sh + + PYIOC=/path/to/pythonSoftIoc/pythonIoc + + cd "$(dirname "$0")" + exec $PYIOC start_ioc.py "$@" + +2. The startup Python script. This establishes the essential component + versions (apart from the `pythonSoftIoc` version), performs the appropriate + initialisation and starts the IOC running. The following template is a + useful starting point:: + + from pkg_resources import require + require('cothread==2.12') + require('epicsdbbuilder==1.0') + + # Import the basic framework components. + from softioc import softioc, builder + import cothread + + # Import any modules required to run the IOC + import ... + + # Boilerplate get the IOC started + builder.LoadDatabase() + softioc.iocInit() + + # Start processes required to be run after iocInit + ... + + # Finally leave the IOC running with an interactive shell. + softioc.interactive_ioc(globals()) + + Note that the use of `require` is specific to DLS, and you may have a + different way of managing your installations. + +.. _numpy: http://www.numpy.org/ +.. _cothread: https://github.com/dls-controls/cothread +.. _epicsdbbuilder: https://github.com/Araneidae/epicsdbbuilder + + Introduction ------------ @@ -48,7 +137,7 @@ with one PV:: This example script illustrates the following points. -- The use of :func:`pkg_resources.require` is standard across all use of the +- The use of ``pkg_resources.require`` is standard across all use of the ``dls-python`` Python interpreter at Diamond, and in this example we are using both :mod:`cothread` and :mod:`epicsdbbuilder`. Of course, in an officially published IOC specific versions must be specified, in this example I'm using @@ -107,7 +196,7 @@ bit more structure is needed. I recommend at least four files as shown: ``ioc_entry.py`` I recommend that the top level Python script used to launch the IOC contain - only :func:`pkg_resources.require` statements, simple code to start the body + only ``pkg_resources.require`` statements, simple code to start the body of the IOC, and it should end with standard code to start the IOC. The following structure can be followed (here I've assumed that the rest of the IOC is in a single file called ``ioc_body.py``:: @@ -172,9 +261,9 @@ following types: Occasionally it may be desirable to create a soft record without ``Python`` device support, particularly if any other record type is required. This can be done using the corresponding record creation -functions provided as methods of :attr:`records`. For example, if a ``calc`` +functions provided as methods of :attr:`softioc.builder.records`. For example, if a ``calc`` record is required then this can be created by calling -:func:`softioc.builder.records.calc`. +``softioc.builder.records.calc``. For all records created by these methods both :meth:`~softioc.device.ProcessDeviceSupportIn.get` and @@ -194,7 +283,7 @@ which must be called in this order. After calling It is sensible to start any server background activity after the IOC has been initialised by calling :func:`~softioc.softioc.iocInit`. After this has been -done (:func:`cothread.Spawn` is recommended for initiating persistent background +done (:class:`cothread.Spawn` is recommended for initiating persistent background activity) the top level script must pause, as as soon as it exits the IOC will exit. Calling :func:`~softioc.softioc.interactive_ioc` is recommended for this as the last statement in the top level script. diff --git a/docs/records.rst b/docs/records.rst index 02b60104..b347db71 100644 --- a/docs/records.rst +++ b/docs/records.rst @@ -12,10 +12,10 @@ This is used to invoke Python processing in response to record processing, making it easy to integrate Python into the EPICS IOC layer. Records are created dynamically during IOC startup before calling -:func:`iocInit` and with the help of the :mod:`softioc.builder` module can be +:func:`~softioc.softioc.iocInit` and with the help of the :mod:`softioc.builder` module can be loaded with :func:`~softioc.builder.LoadDatabase`. -All records are created internally using methods of the :class:`PythonDevice` +All records are created internally using methods of the ``PythonDevice`` class, one method for each of the supported record types, however the corresponding wrapping functions published by :mod:`softioc.builder` should be used as they configure sensible defaults and are generally easier to use. @@ -111,14 +111,14 @@ specified: If used this should be set to a callable taking two arguments. The first argument will be the record object, and the second will be the new value being written. The `validiate` function can reject the update by returning - :const:`False` or accept it by returning :const:`True`. + `False` or accept it by returning `True`. Note that this function is called asynchronously on a thread determined by EPICS and it is not safe to perform any cothread actions within this callback. `always_update` - This flag defaults to :const:`False`, in which case updates to the record + This flag defaults to `False`, in which case updates to the record which don't change its value will be discarded. In particular this means that such updates don't call `validate` or `on_update`. @@ -132,7 +132,7 @@ specified: Updates the value associated with the record. By default this will trigger record processing, and so will cause any associated `on_update` - and `validate` methods to be called. If `process` is :const:`False` + and `validate` methods to be called. If `process` is `False` then neither of these methods will be called, but the value will still be updated. diff --git a/docs/softioc.rst b/docs/softioc.rst index 3d8801c4..7d655a84 100644 --- a/docs/softioc.rst +++ b/docs/softioc.rst @@ -31,21 +31,21 @@ following submodules can be loaded: The following submodules implement internals and should not normally be looked at directly: -:mod:`softioc.imports` +``softioc.imports`` Imports and wraps C functions from EPICS IOC support. -:mod:`softioc.fields` +``softioc.fields`` Used internally as part of record support to implement access to EPICS record fields. -:mod:`softioc.device_core` +``softioc.device_core`` Implements the basics of ``Python`` EPICS device support. :mod:`softioc.device` Implements ``Python`` device support for all the record types supported. -:mod:`softioc.pythonSoftIoc` - Implements :mod:`iocbuilder` interface for all of the ``Python`` records. +``softioc.pythonSoftIoc`` + Implements ``iocbuilder`` interface for all of the ``Python`` records. Top Level IOC Interface: :mod:`softioc.softioc` @@ -84,14 +84,14 @@ importing ``*``): This is the normal way to run an interactive shell after starting the IOC. The `context` argument is a dictionary of values that will be made available to the interactive Python shell together with a number of EPICS test - functions. By default, if `call_exit` is :const:`True`, the IOC will be - terminated by calling :func:`epicsExit` when the interpreter exits, which + functions. By default, if `call_exit` is `True`, the IOC will be + terminated by calling ``epicsExit()`` when the interpreter exits, which means that :func:`interactive_ioc` will not return. While the interactive shell is running a number of EPICS test functions are made available for use together with the constant value :const:`exit` with special behaviour: typing :const:`exit` at the interpreter prompt will - immediately call :func:`epicsExit` causing the Python interpreter and IOC to + immediately call ``epicsExit()`` causing the Python interpreter and IOC to terminate. This module provides Python wrappers for the following EPICS test functions and @@ -186,7 +186,7 @@ EPICS documentation for more details of each function. .. attribute:: exit - Displaying this value will invoke :func:`epicsExit` causing the IOC to + Displaying this value will invoke ``epicsExit()`` causing the IOC to terminate immediately. @@ -351,7 +351,7 @@ The following attributes allow more direct access to record creation. .. attribute:: records - This is the :mod:`iocbuilder` records object, and is populated with + This is the ``iocbuilder`` records object, and is populated with functions named after each available record type. Records created with these calls are created with soft device support and Python is not involved in their processing. @@ -383,8 +383,8 @@ Alarm Value Definitions .. module:: softioc.alarm :synopsis: Constant definitions for EPICS severity and alarm values -The following values can be passed to IN record :meth:`set` and -:meth:`set_alarm` methods. +The following values can be passed to IN record :meth:`~softioc.device.ProcessDeviceSupportIn.set` and +:meth:`~softioc.device.ProcessDeviceSupportIn.set_alarm` methods. .. attribute:: NO_ALARM = 0 @@ -392,7 +392,7 @@ The following values can be passed to IN record :meth:`set` and MAJOR_ALARM = 2 INVALID_ALARM = 3 - These are severity values. The default severity is :attr:`NO_ALARM`. + These are severity values. The default severity is ``NO_ALARM``. .. attribute:: READ_ALARM diff --git a/iocStats b/iocStats new file mode 160000 index 00000000..4df9e878 --- /dev/null +++ b/iocStats @@ -0,0 +1 @@ +Subproject commit 4df9e87815f6a9432955a3ddb45fafa9fe4a4d40 diff --git a/pyproject.toml b/pyproject.toml index ed3ef262..03c32fc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools_dso", "epicscorelibs>=7.0.4.99.0.0a1"] +requires = ["setuptools", "wheel", "setuptools_dso", "epicscorelibs>=7.0.4.99.1"] build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index 676a93a7..9a7b883e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = softioc -description = EPICS IOC with Python Device Support +description = Embed an EPICS IOC in a Python process url = https://github.com/dls-controls/pythonIoc author = Michael Abbott author_email = Michael.Abbott@diamond.ac.uk @@ -11,12 +11,13 @@ classifiers = Development Status :: 5 - Production/Stable License :: OSI Approved :: Apache Software License Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] packages = softioc +python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.* [options.entry_points] # Include a command line script @@ -39,7 +40,7 @@ extend-ignore = [tool:pytest] # Run pytest with all our checkers, and don't spam us with massive tracebacks on error -addopts = --tb=native -vv --flake8 --doctest-modules --ignore=iocStats --ignore=epicscorelibs +addopts = --tb=native -vv --doctest-modules --ignore=iocStats --ignore=epicscorelibs [coverage:run] # This is covered in the versiongit test suite so exclude it here diff --git a/setup.py b/setup.py index 43ea801e..4955d321 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ def _add_file(f): ], dsos = ['epicscorelibs.lib.dbCore', 'epicscorelibs.lib.Com'], define_macros = get_config_var('CPPFLAGS'), - extra_compile_args = get_config_var('CXXFLAGS'), + extra_compile_args = get_config_var('CFLAGS'), extra_link_args = get_config_var('LDFLAGS'), ) @@ -94,9 +94,9 @@ def install_for_development(self): version=__version__, ext_modules = [ext], install_requires = [ - # Dependency declared in pyproject.toml + # Dependency version declared in pyproject.toml epicscorelibs.version.abi_requires(), - "numpy>=1.18", + "numpy", "epicsdbbuilder>=1.4" ], zip_safe = False, # setuptools_dso is not compatible with eggs! diff --git a/softioc/__init__.py b/softioc/__init__.py index bb8d9bf2..bf39f622 100644 --- a/softioc/__init__.py +++ b/softioc/__init__.py @@ -1,3 +1,2 @@ '''Python soft IOC module.''' - from ._version_git import __version__ diff --git a/softioc/imports.py b/softioc/imports.py index dd62934f..69d13d30 100644 --- a/softioc/imports.py +++ b/softioc/imports.py @@ -8,7 +8,7 @@ from epicscorelibs import path from epicscorelibs.ioc import \ - iocshRegisterCommon, registerRecordDeviceDriver, pdbbase + iocshRegisterCommon, registerRecordDeviceDriver, pdbbase, dbCore, Com from . import _extension @@ -60,30 +60,11 @@ def auto_decode(result, func, args): return result.decode() -def EpicsDll(dll): - return CDLL(path.get_lib(dll)) - - -# A bit tricky: in more recent versions of EPICS all the entry points we want -# have been gathered into a single .so, but previously they were split among -# four different ones. Just try both options. -try: - libdbCore = EpicsDll('dbCore') - libregistryIoc = libdbCore - libdbIoc = libdbCore - libmiscIoc = libdbCore -except OSError: - # Ok, no dbCore, then we should find everything in these four instead. - libregistryIoc = EpicsDll('registryIoc') - libdbIoc = EpicsDll('dbIoc') - libmiscIoc = EpicsDll('miscIoc') - - # int registryDeviceSupportAdd( # const char *name,const struct dset *pdset); # # Registers device support. -registryDeviceSupportAdd = libregistryIoc.registryDeviceSupportAdd +registryDeviceSupportAdd = dbCore.registryDeviceSupportAdd registryDeviceSupportAdd.argtypes = (c_char_p, c_void_p) registryDeviceSupportAdd.errcheck = expect_true @@ -94,15 +75,15 @@ def EpicsDll(dll): # Initialise and trigger I/O Intr processing structure. IOSCANPVT = c_void_p -scanIoInit = libdbIoc.scanIoInit +scanIoInit = dbCore.scanIoInit scanIoInit.argtypes = (IOSCANPVT,) scanIoInit.restype = None -scanIoRequest = libdbIoc.scanIoRequest +scanIoRequest = dbCore.scanIoRequest scanIoRequest.argtypes = (IOSCANPVT,) scanIoRequest.restype = None -dbLoadDatabase = libdbIoc.dbLoadDatabase +dbLoadDatabase = dbCore.dbLoadDatabase dbLoadDatabase.argtypes = (auto_encode, auto_encode, auto_encode) dbLoadDatabase.errcheck = expect_success @@ -111,15 +92,15 @@ def EpicsDll(dll): # # Raises event processing if any alarm status has changed, and resets NSTA # and NSEV fields for further processing. -recGblResetAlarms = libdbIoc.recGblResetAlarms +recGblResetAlarms = dbCore.recGblResetAlarms recGblResetAlarms.argtypes = (c_void_p,) recGblResetAlarms.restype = c_short -iocInit = libmiscIoc.iocInit +iocInit = dbCore.iocInit iocInit.argtypes = () -epicsExit = libmiscIoc.epicsExit +epicsExit = Com.epicsExit epicsExit.argtypes = () iocshRegisterCommon() diff --git a/softioc/softioc.py b/softioc/softioc.py index ee01f72a..137805d1 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -32,8 +32,9 @@ def safeEpicsExit(): # IOC Test facilities -def ExportTest(name, argtypes, defaults=(), description='no description yet'): - f = getattr(imports.libdbIoc, name) +def ExportTest(name, argtypes, defaults=(), description='no description yet', + lib=imports.dbCore): + f = getattr(lib, name) f.argtypes = argtypes f.restype = None @@ -136,10 +137,10 @@ def call_f(*args): Prints all records in the I/O event scan lists.''') ExportTest('generalTimeReport', (c_int,), (0,), '''\ -Displays time providers and their status''') +Displays time providers and their status''', lib=imports.Com) ExportTest('eltc', (c_int,), (), '''\ -Turn EPICS logging on or off.''') +Turn EPICS logging on or off.''', lib=imports.Com) # Hacked up exit object so that when soft IOC framework sends us an exit command diff --git a/tests/test_softioc.py b/tests/test_softioc.py index 031c2b78..3129da2c 100644 --- a/tests/test_softioc.py +++ b/tests/test_softioc.py @@ -1,4 +1,3 @@ -from importlib import reload import random import string import os @@ -9,11 +8,16 @@ import sim_records + +if sys.version_info > (3,): + from importlib import reload + + PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) def test_records(tmp_path): - path = tmp_path / "records.db" + path = str(tmp_path / "records.db") builder.WriteRecords(path) expected = os.path.join(os.path.dirname(__file__), "expected_records.db") assert open(path).readlines()[4:] == open(expected).readlines()[4:] From 4fb500e0022e7253ce48a22e7882ea73d718af6d Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Fri, 14 May 2021 14:30:42 +0100 Subject: [PATCH 12/19] Add asyncio dispatcher_callback --- .github/workflows/code.yml | 46 +++++++----- Pipfile | 2 + Pipfile.lock | 142 +++++++++++++++++++++--------------- softioc/asyncio_callback.py | 19 +++++ softioc/device.py | 28 +++++-- softioc/softioc.py | 10 ++- tests/conftest.py | 5 ++ tests/sim_asyncio_ioc.py | 36 +++++++++ tests/sim_cothread_ioc.py | 20 +++++ tests/sim_records.py | 11 +-- tests/test_asyncio.py | 62 ++++++++++++++++ tests/test_cothread.py | 67 +++++++++++++++++ tests/test_records.py | 12 +++ tests/test_softioc.py | 58 --------------- 14 files changed, 366 insertions(+), 152 deletions(-) create mode 100644 softioc/asyncio_callback.py create mode 100644 tests/conftest.py create mode 100644 tests/sim_asyncio_ioc.py create mode 100644 tests/sim_cothread_ioc.py create mode 100644 tests/test_asyncio.py create mode 100644 tests/test_cothread.py create mode 100644 tests/test_records.py delete mode 100644 tests/test_softioc.py diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 9dd39fb9..1ba87a78 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -32,22 +32,36 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] python: [cp27, cp37, cp38, cp39] - include: - - os: macos-latest - TEST_REQUIRES: pytest-cov cothread - # Put coverage straight in the dist dir - COV_FILE: '{project}/dist/coverage.xml' + exclude: + - os: windows-latest + # No cothread or asyncio so doesn't work + python: cp27 + include: - os: ubuntu-latest - TEST_REQUIRES: pytest-cov cothread # Put coverage in the output dir mounted in docker - COV_FILE: '/output/coverage.xml' + cov_file: /output/coverage.xml + test_requires: cothread pytest-asyncio aioca - os: windows-latest + cov_file: '{project}/dist/coverage.xml' # cothread doesn't work on windows - TEST_REQUIRES: pytest-cov - # Put coverage straight in the dist dir - COV_FILE: '{project}/dist/coverage.xml' + test_requires: pytest-asyncio aioca + + - os: macos-latest + cov_file: '{project}/dist/coverage.xml' + test_requires: cothread pytest-asyncio aioca + + - os: ubuntu-latest + python: cp27 + # asyncio doesn't work on Python2.7 + test_requires: cothread + + - os: macos-latest + python: cp27 + # asyncio doesn't work on Python2.7 + test_requires: cothread + steps: - name: Checkout Source @@ -70,8 +84,8 @@ jobs: run: cibuildwheel --output-dir dist env: CIBW_BUILD: ${{ matrix.python }}*64 - CIBW_TEST_REQUIRES: ${{ matrix.TEST_REQUIRES }} - CIBW_TEST_COMMAND: pytest --cov=softioc {project}/tests --cov-report xml:${{ matrix.COV_FILE }} + CIBW_TEST_REQUIRES: pytest-cov ${{ matrix.test_requires }} + CIBW_TEST_COMMAND: pytest --cov=softioc {project}/tests --cov-report xml:${{ matrix.cov_file }} # Disable auditwheel as it isn't compatible with setuptools_dso approach # https://github.com/mdavidsaver/setuptools_dso/issues/17 CIBW_REPAIR_WHEEL_COMMAND: '' @@ -79,6 +93,7 @@ jobs: - name: Upload Wheel and Sdist uses: actions/upload-artifact@v2 with: + name: dist path: dist/softioc* - name: Upload coverage to Codecov @@ -91,17 +106,14 @@ jobs: needs: [build] runs-on: ubuntu-latest # upload to PyPI on every tag - #if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') steps: - uses: actions/download-artifact@v2 with: + name: dist path: dist - - name: Display structure of downloaded files - run: ls -R - - name: Publish to PyPI - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.pypi_token }} diff --git a/Pipfile b/Pipfile index ee099401..4a56d948 100644 --- a/Pipfile +++ b/Pipfile @@ -11,6 +11,7 @@ sphinx-rtd-theme = "*" sphinx-multiversion = {editable = true,git = "https://github.com/dls-controls/sphinx-multiversion.git",ref = "only-arg"} setuptools-dso = "*" aioca = "*" +pytest-asyncio = "*" [packages] # All other package requirements from setup.py @@ -20,6 +21,7 @@ epicscorelibs = "*" # Add some other useful extras cothread = "*" scipy = "*" +aioca = "*" [scripts] # Put coverage here so we don't interfere with debugging in the IDE diff --git a/Pipfile.lock b/Pipfile.lock index e139a117..b11db770 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7232f8aab61630fe29934f2891416df7f518e8aa75e0562764772f98027b6d60" + "sha256": "291eabbed33886532f92f18262967ff5032159f5a100a759b10ba6f935f53108" }, "pipfile-spec": 6, "requires": {}, @@ -14,6 +14,14 @@ ] }, "default": { + "aioca": { + "hashes": [ + "sha256:5caa44ca048e6b6a409255be1da21ca3b6b7df33117cb16f48102e1cb2979311", + "sha256:a1805c81c3e3f3cbc672b567ea82729dcb1345faf80c1bb07c3cf22f687503b6" + ], + "index": "pypi", + "version": "==1.0" + }, "cothread": { "hashes": [ "sha256:50cf5b5a65e7fcfd113f06c2d6b1f363e37b0caee412c75ac7608377aa0bd10a" @@ -61,33 +69,33 @@ }, "numpy": { "hashes": [ - "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727", - "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6", - "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98", - "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7", - "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d", - "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2", - "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9", - "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935", - "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff", - "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee", - "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb", - "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042", - "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3", - "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5", - "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6", - "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f", - "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4", - "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737", - "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931", - "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6", - "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677", - "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576", - "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935", - "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd" + "sha256:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010", + "sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd", + "sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43", + "sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9", + "sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df", + "sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400", + "sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2", + "sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4", + "sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a", + "sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6", + "sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8", + "sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b", + "sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8", + "sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb", + "sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2", + "sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f", + "sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4", + "sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a", + "sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16", + "sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f", + "sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69", + "sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65", + "sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17", + "sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48" ], "markers": "python_version >= '3.7'", - "version": "==1.20.2" + "version": "==1.20.3" }, "scipy": { "hashes": [ @@ -116,8 +124,15 @@ }, "softioc": { "editable": true, - "extras": [], "path": "." + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "version": "==3.10.0.0" } }, "develop": { @@ -168,6 +183,9 @@ "version": "==4.0.0" }, "coverage": { + "extras": [ + "toml" + ], "hashes": [ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", @@ -370,33 +388,33 @@ }, "numpy": { "hashes": [ - "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727", - "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6", - "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98", - "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7", - "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d", - "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2", - "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9", - "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935", - "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff", - "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee", - "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb", - "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042", - "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3", - "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5", - "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6", - "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f", - "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4", - "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737", - "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931", - "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6", - "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677", - "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576", - "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935", - "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd" + "sha256:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010", + "sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd", + "sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43", + "sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9", + "sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df", + "sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400", + "sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2", + "sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4", + "sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a", + "sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6", + "sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8", + "sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b", + "sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8", + "sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb", + "sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2", + "sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f", + "sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4", + "sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a", + "sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16", + "sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f", + "sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69", + "sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65", + "sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17", + "sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48" ], "markers": "python_version >= '3.7'", - "version": "==1.20.2" + "version": "==1.20.3" }, "packaging": { "hashes": [ @@ -462,13 +480,21 @@ "markers": "python_version >= '3.6'", "version": "==6.2.4" }, + "pytest-asyncio": { + "hashes": [ + "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f", + "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea" + ], + "index": "pypi", + "version": "==0.15.1" + }, "pytest-cov": { "hashes": [ - "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", - "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" + "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e", + "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e" ], "index": "pypi", - "version": "==2.11.1" + "version": "==2.12.0" }, "pytest-flake8": { "hashes": [ @@ -509,11 +535,11 @@ }, "sphinx": { "hashes": [ - "sha256:904e02cd0f84bed5d3748358c228ae3df3ad725b9e6cdc2166e17b309ed2e1fa", - "sha256:b246ebd74f5fb966d7e90086bbda5ed74ee4d30b4c3cbefddc1fb5210aa317c7" + "sha256:b2566f5f339737a6ef37198c47d56de1f4a746c722bebdb2fe045c34bfd8b9d0", + "sha256:cf5104777571b2b7f06fa88ee08fade24563f4a0594cf4bd17d31c47b8740b4c" ], "markers": "python_version >= '3.6'", - "version": "==4.0.0" + "version": "==4.0.1" }, "sphinx-multiversion": { "editable": true, diff --git a/softioc/asyncio_callback.py b/softioc/asyncio_callback.py new file mode 100644 index 00000000..41ff05e9 --- /dev/null +++ b/softioc/asyncio_callback.py @@ -0,0 +1,19 @@ +import asyncio +import inspect +import threading + + +class AsyncioCallback(threading.Thread): + def __init__(self): + super().__init__() + self.loop = asyncio.new_event_loop() + + def run(self): + self.loop.run_forever() + + def __call__(self, func, *args): + async def async_wrapper(): + ret = func(*args) + if inspect.isawaitable(ret): + await ret + asyncio.run_coroutine_threadsafe(async_wrapper(), self.loop) diff --git a/softioc/device.py b/softioc/device.py index 710d0791..0e4cb700 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -1,17 +1,33 @@ import os import time -import traceback +import inspect from ctypes import * import numpy -import cothread - from . import alarm from .fields import DbfCodeToNumpy, DbrToDbfCode from .imports import dbLoadDatabase, recGblResetAlarms, db_put_field from .device_core import DeviceSupportCore, RecordLookup +dispatcher_callback = None + + +def use_cothread(): + import cothread + # Create our own cothread callback queue so that our callbacks + # processing doesn't interfere with other callback processing. + global dispatcher_callback + dispatcher_callback = cothread.cothread._Callback() + + +def use_asyncio(): + from .asyncio_callback import AsyncioCallback + global dispatcher_callback + dispatcher_callback = AsyncioCallback() + dispatcher_callback.start() + + class ProcessDeviceSupportCore(DeviceSupportCore, RecordLookup): '''Implements canonical default processing for records with a _process method. Processing typically either copies a locally set value into the @@ -79,10 +95,6 @@ def get(self): class ProcessDeviceSupportOut(ProcessDeviceSupportCore): _link_ = 'OUT' - # Create our own cothread callback queue so that our callbacks processing - # doesn't interfere with other callback processing. - __Callback = cothread.cothread._Callback() - def __init__(self, name, **kargs): on_update = kargs.pop('on_update', None) on_update_name = kargs.pop('on_update_name', None) @@ -133,7 +145,7 @@ def _process(self, record): self._value = value if self.__on_update and self.__enable_write: - self.__Callback(self.__on_update, value) + dispatcher_callback(self.__on_update, value) return 0 diff --git a/softioc/softioc.py b/softioc/softioc.py index 137805d1..4a9724b9 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -6,14 +6,19 @@ from epicsdbbuilder.recordset import recordset -from . import imports +from . import imports, device __all__ = ['dbLoadDatabase', 'iocInit', 'interactive_ioc'] -iocInit = imports.iocInit epicsExit = imports.epicsExit +def iocInit(): + if device.dispatcher_callback is None: + # Fallback to cothread + device.use_cothread() + imports.iocInit() + def safeEpicsExit(): '''Calls epicsExit() after ensuring Python exit handlers called.''' if hasattr(sys, 'exitfunc'): @@ -26,7 +31,6 @@ def safeEpicsExit(): del sys.exitfunc epicsExit() - # The following identifiers will be exported to interactive shell. command_names = [] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..382e0951 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,5 @@ +import sys + +if sys.version_info < (3,): + # Python2 has no asyncio, so ignore these tests + collect_ignore = ["test_asyncio.py", "sim_asyncio_ioc.py"] diff --git a/tests/sim_asyncio_ioc.py b/tests/sim_asyncio_ioc.py new file mode 100644 index 00000000..e2f269be --- /dev/null +++ b/tests/sim_asyncio_ioc.py @@ -0,0 +1,36 @@ +from argparse import ArgumentParser + +import asyncio +import time +import sys + +from softioc import softioc, builder, device, pvlog + + +if __name__ == "__main__": + # Being run as an IOC, so parse args and set prefix + parser = ArgumentParser() + parser.add_argument('prefix', help="The PV prefix for the records") + parsed_args = parser.parse_args() + builder.SetDeviceName(parsed_args.prefix) + + import sim_records + + async def callback(value): + await asyncio.sleep(0.5) + print("async update %s" % value) + sim_records.t_ai.set(value) + + t_ao = builder.aOut('AO2', initial_value=12.45, on_update=callback) + + device.use_asyncio() + + # Run the IOC + builder.LoadDatabase() + softioc.iocInit() + # Wait for some prints to have happened + time.sleep(1) + # Make sure coverage is written on epicsExit + from pytest_cov.embed import cleanup + sys.exitfunc = cleanup + softioc.interactive_ioc() diff --git a/tests/sim_cothread_ioc.py b/tests/sim_cothread_ioc.py new file mode 100644 index 00000000..b9f57603 --- /dev/null +++ b/tests/sim_cothread_ioc.py @@ -0,0 +1,20 @@ +from argparse import ArgumentParser + +from softioc import softioc, builder, pvlog + + +if __name__ == "__main__": + import cothread + + # Being run as an IOC, so parse args and set prefix + parser = ArgumentParser() + parser.add_argument('prefix', help="The PV prefix for the records") + parsed_args = parser.parse_args() + builder.SetDeviceName(parsed_args.prefix) + + import sim_records + + # Run the IOC + builder.LoadDatabase() + softioc.iocInit() + cothread.WaitForQuit() diff --git a/tests/sim_records.py b/tests/sim_records.py index c8062f0e..a6d8c396 100755 --- a/tests/sim_records.py +++ b/tests/sim_records.py @@ -2,10 +2,11 @@ from __future__ import print_function -from epicsdbbuilder.recordnames import GetRecordNames +from epicsdbbuilder import GetRecordNames, WriteRecords from softioc import softioc from softioc.builder import * + import numpy names = GetRecordNames() @@ -79,11 +80,5 @@ def UpdateOut(): 'Update', 'UpdateOut' ] - if __name__ == "__main__": - # Simple example script for building an example soft IOC. - from softioc import pvlog, builder - builder.WriteRecords("expected_records.db") - LoadDatabase() - softioc.iocInit() - softioc.interactive_ioc(globals()) + WriteRecords("expected_records.db") diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py new file mode 100644 index 00000000..ddf42315 --- /dev/null +++ b/tests/test_asyncio.py @@ -0,0 +1,62 @@ +# Will be ignored on Python2 by conftest.py settings + +import random +import string +import subprocess +import sys +import os +import atexit +import pytest + +PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + + +@pytest.fixture +def asyncio_ioc(): + sim_ioc = os.path.join(os.path.dirname(__file__), "sim_asyncio_ioc.py") + cmd = [sys.executable, sim_ioc, PV_PREFIX] + proc = subprocess.Popen( + cmd, env=dict(os.environ, PYTHONUNBUFFERED="1"), + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + yield proc + proc.kill() + out, err = proc.communicate() + print(out.decode()) + print(err.decode(), file=sys.stderr) + + +@pytest.mark.asyncio +async def test_asyncio_ioc(asyncio_ioc): + import asyncio + from aioca import caget, caput, camonitor, CANothing, _catools + # Unregister the atexit handler as it conflicts with cothread + atexit.unregister(_catools._catools_atexit) + + # Start + assert await caget(PV_PREFIX + ":UPTIME") in ["00:00:00", "00:00:01"] + # WAVEFORM + await caput(PV_PREFIX + ":SINN", 4, wait=True) + q = asyncio.Queue() + m = camonitor(PV_PREFIX + ":SIN", q.put, notify_disconnect=True) + assert len(await asyncio.wait_for(q.get(), 1)) == 4 + # AO + assert await caget(PV_PREFIX + ":AO2") == 12.45 + await caput(PV_PREFIX + ":AO2", 3.56, wait=True) + assert await caget(PV_PREFIX + ":AI") == 12.34 + await asyncio.sleep(0.6) + assert await caget(PV_PREFIX + ":AI") == 3.56 + # Stop + out, err = asyncio_ioc.communicate(b"exit\n", timeout=1) + out = out.decode() + err = err.decode() + # Disconnect + assert isinstance(await asyncio.wait_for(q.get(), 10), CANothing) + m.close() + # check closed and output + assert "%s:SINN.VAL 1024 -> 4" % PV_PREFIX in out + assert 'update_sin_wf 4' in out + assert "%s:AO2.VAL 12.45 -> 3.56" % PV_PREFIX in out + assert 'async update 3.56' in out + assert 'Starting iocInit' in err + assert 'iocRun: All initialization complete' in err + assert '(InteractiveConsole)' in err diff --git a/tests/test_cothread.py b/tests/test_cothread.py new file mode 100644 index 00000000..88c8bf6f --- /dev/null +++ b/tests/test_cothread.py @@ -0,0 +1,67 @@ +import random +import string +import subprocess +import sys +import os +import signal +import pytest + +PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) + + +if sys.platform.startswith("win"): + pytest.skip("Cothread doesn't work on windows", allow_module_level=True) + + +@pytest.fixture +def cothread_ioc(): + sim_ioc = os.path.join(os.path.dirname(__file__), "sim_cothread_ioc.py") + cmd = [sys.executable, sim_ioc, PV_PREFIX] + proc = subprocess.Popen( + cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + yield proc + # This has to work on Python2 and 3 so can't rely on communicate to + # produce stdout and stderr if we already closed the process + try: + proc.kill() + out, err = proc.communicate() + print(out.decode()) + print(err.decode()) + except OSError: + # Doesn't matter if its already stopped + pass + + +def test_cothread_ioc(cothread_ioc): + import epicscorelibs.path.cothread + import cothread + from cothread.catools import ca_nothing, caget, caput, camonitor + + # Start + assert caget(PV_PREFIX + ":UPTIME") in ["00:00:00", "00:00:01"] + # WAVEFORM + caput(PV_PREFIX + ":SINN", 4, wait=True) + q = cothread.EventQueue() + m = camonitor(PV_PREFIX + ":SIN", q.Signal, notify_disconnect=True) + assert len(q.Wait(1)) == 4 + # STRINGOUT + assert caget(PV_PREFIX + ":STRINGOUT") == "watevah" + caput(PV_PREFIX + ":STRINGOUT", "something", wait=True) + assert caget(PV_PREFIX + ":STRINGOUT") == "something" + # Stop + cothread_ioc.send_signal(signal.SIGINT) + # Disconnect + assert isinstance(q.Wait(10), ca_nothing) + m.close() + # check closed and output + out, err = cothread_ioc.communicate() + out = out.decode() + err = err.decode() + # check closed and output + assert "%s:SINN.VAL 1024 -> 4" % PV_PREFIX in out + assert 'update_sin_wf 4' in out + assert "%s:STRINGOUT.VAL watevah -> something" % PV_PREFIX in out + assert 'on_update \'something\'' in out + assert 'Starting iocInit' in err + assert 'iocRun: All initialization complete' in err diff --git a/tests/test_records.py b/tests/test_records.py new file mode 100644 index 00000000..a3778a5b --- /dev/null +++ b/tests/test_records.py @@ -0,0 +1,12 @@ +import os + +from softioc import builder + +import sim_records + + +def test_records(tmp_path): + path = str(tmp_path / "records.db") + builder.WriteRecords(path) + expected = os.path.join(os.path.dirname(__file__), "expected_records.db") + assert open(path).readlines()[4:] == open(expected).readlines()[4:] diff --git a/tests/test_softioc.py b/tests/test_softioc.py deleted file mode 100644 index 3129da2c..00000000 --- a/tests/test_softioc.py +++ /dev/null @@ -1,58 +0,0 @@ -import random -import string -import os -import sys - -from softioc import softioc, builder -from cothread.catools import caget, caput - -import sim_records - - -if sys.version_info > (3,): - from importlib import reload - - -PV_PREFIX = "".join(random.choice(string.ascii_uppercase) for _ in range(12)) - - -def test_records(tmp_path): - path = str(tmp_path / "records.db") - builder.WriteRecords(path) - expected = os.path.join(os.path.dirname(__file__), "expected_records.db") - assert open(path).readlines()[4:] == open(expected).readlines()[4:] - - -def assert_record_equals(record, value): - assert record.get() == caget(record.name) == value - -def test_ioc(capsys): - builder.ResetRecords() - builder.SetDeviceName(PV_PREFIX) - reload(sim_records) - builder.LoadDatabase() - softioc.iocInit() - out, err = capsys.readouterr() - assert not out - assert not err - assert caget(PV_PREFIX + ":UPTIME") in ["00:00:00", "00:00:01"] - # AI - assert_record_equals(sim_records.t_ai, 12.34) - sim_records.t_ai.set(34) - assert_record_equals(sim_records.t_ai, 34) - # STRINGOUT - assert_record_equals(sim_records.t_stringout, "watevah") - caput(PV_PREFIX + ":STRINGOUT", "something", wait=True) - out, err = capsys.readouterr() - assert out == "on_update 'something'\n" - assert not err - assert_record_equals(sim_records.t_stringout, "something") - sim_records.t_stringout.set(b"something else") - assert_record_equals(sim_records.t_stringout, "something else") - - - - - -if __name__ == "__main__": - softioc.interactive_ioc(globals()) From 48908ea92c5ee6d61554481f33501e890104b58b Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Mon, 17 May 2021 11:02:01 +0100 Subject: [PATCH 13/19] Move dispatcher to iocInit arg and imports to __init__ --- softioc/__init__.py | 19 +++++++++++++++++ ...ncio_callback.py => asyncio_dispatcher.py} | 3 ++- softioc/device.py | 21 ++++--------------- softioc/imports.py | 13 ++---------- softioc/softioc.py | 13 +++++++++--- tests/sim_asyncio_ioc.py | 6 ++---- 6 files changed, 39 insertions(+), 36 deletions(-) rename softioc/{asyncio_callback.py => asyncio_dispatcher.py} (87%) diff --git a/softioc/__init__.py b/softioc/__init__.py index bf39f622..35c9b808 100644 --- a/softioc/__init__.py +++ b/softioc/__init__.py @@ -1,2 +1,21 @@ '''Python soft IOC module.''' +import os + +from epicscorelibs import path +from epicscorelibs.ioc import \ + iocshRegisterCommon, registerRecordDeviceDriver, pdbbase + +# This import will also pull in the extension, which is needed +# before we call iocshRegisterCommon +from .imports import dbLoadDatabase from ._version_git import __version__ + +# Need to do this before calling anything in device.py +iocshRegisterCommon() +dbLoadDatabase("base.dbd", os.path.join(path.base_path, "dbd"), None) +dbLoadDatabase("devIocStats.dbd", os.path.dirname(__file__), None) + +if registerRecordDeviceDriver(pdbbase): + raise RuntimeError('Error registering') + +__all__ = ["__version__"] diff --git a/softioc/asyncio_callback.py b/softioc/asyncio_dispatcher.py similarity index 87% rename from softioc/asyncio_callback.py rename to softioc/asyncio_dispatcher.py index 41ff05e9..19904ab0 100644 --- a/softioc/asyncio_callback.py +++ b/softioc/asyncio_dispatcher.py @@ -3,10 +3,11 @@ import threading -class AsyncioCallback(threading.Thread): +class AsyncioDispatcher(threading.Thread): def __init__(self): super().__init__() self.loop = asyncio.new_event_loop() + self.start() def run(self): self.loop.run_forever() diff --git a/softioc/device.py b/softioc/device.py index 0e4cb700..c91d8545 100644 --- a/softioc/device.py +++ b/softioc/device.py @@ -10,22 +10,9 @@ from .device_core import DeviceSupportCore, RecordLookup -dispatcher_callback = None - - -def use_cothread(): - import cothread - # Create our own cothread callback queue so that our callbacks - # processing doesn't interfere with other callback processing. - global dispatcher_callback - dispatcher_callback = cothread.cothread._Callback() - - -def use_asyncio(): - from .asyncio_callback import AsyncioCallback - global dispatcher_callback - dispatcher_callback = AsyncioCallback() - dispatcher_callback.start() +# This is set from softioc.iocInit +# dispatcher(func, *args) will queue a callback to happen +dispatcher = None class ProcessDeviceSupportCore(DeviceSupportCore, RecordLookup): @@ -145,7 +132,7 @@ def _process(self, record): self._value = value if self.__on_update and self.__enable_write: - dispatcher_callback(self.__on_update, value) + dispatcher(self.__on_update, value) return 0 diff --git a/softioc/imports.py b/softioc/imports.py index 69d13d30..fc4e0e3a 100644 --- a/softioc/imports.py +++ b/softioc/imports.py @@ -6,9 +6,8 @@ import sys from ctypes import * -from epicscorelibs import path -from epicscorelibs.ioc import \ - iocshRegisterCommon, registerRecordDeviceDriver, pdbbase, dbCore, Com +# Use the libs with the right windows flags +from epicscorelibs.ioc import dbCore, Com from . import _extension @@ -96,20 +95,12 @@ def auto_decode(result, func, args): recGblResetAlarms.argtypes = (c_void_p,) recGblResetAlarms.restype = c_short - iocInit = dbCore.iocInit iocInit.argtypes = () epicsExit = Com.epicsExit epicsExit.argtypes = () -iocshRegisterCommon() -dbLoadDatabase("base.dbd", os.path.join(path.base_path, "dbd"), None) -dbLoadDatabase("devIocStats.dbd", os.path.dirname(__file__), None) - -if registerRecordDeviceDriver(pdbbase): - raise RuntimeError('Error registering') - __all__ = [ 'get_field_offsets', diff --git a/softioc/softioc.py b/softioc/softioc.py index 4a9724b9..cb2fe386 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -13,12 +13,19 @@ epicsExit = imports.epicsExit -def iocInit(): - if device.dispatcher_callback is None: + +def iocInit(dispatcher=None): + if dispatcher is None: # Fallback to cothread - device.use_cothread() + import cothread + # Create our own cothread callback queue so that our callbacks + # processing doesn't interfere with other callback processing. + dispatcher = cothread.cothread._Callback() + # Set the dispatcher for record processing callbacks + device.dispatcher = dispatcher imports.iocInit() + def safeEpicsExit(): '''Calls epicsExit() after ensuring Python exit handlers called.''' if hasattr(sys, 'exitfunc'): diff --git a/tests/sim_asyncio_ioc.py b/tests/sim_asyncio_ioc.py index e2f269be..9f2b25b9 100644 --- a/tests/sim_asyncio_ioc.py +++ b/tests/sim_asyncio_ioc.py @@ -4,7 +4,7 @@ import time import sys -from softioc import softioc, builder, device, pvlog +from softioc import softioc, builder, asyncio_dispatcher, pvlog if __name__ == "__main__": @@ -23,11 +23,9 @@ async def callback(value): t_ao = builder.aOut('AO2', initial_value=12.45, on_update=callback) - device.use_asyncio() - # Run the IOC builder.LoadDatabase() - softioc.iocInit() + softioc.iocInit(asyncio_dispatcher.AsyncioDispatcher()) # Wait for some prints to have happened time.sleep(1) # Make sure coverage is written on epicsExit From 0b45e327d6138939ea923ebdf9b053b63d7c59f7 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Mon, 17 May 2021 12:32:58 +0100 Subject: [PATCH 14/19] Fix tests and docs on CI --- .github/workflows/docs.yml | 1 + tests/sim_asyncio_ioc.py | 2 ++ tests/test_asyncio.py | 15 ++++++++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7f454cdb..6ec3fc50 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,6 +15,7 @@ jobs: with: # require all of history to see all tagged versions' docs fetch-depth: 0 + submodules: true - name: Set up Python uses: actions/setup-python@v2 diff --git a/tests/sim_asyncio_ioc.py b/tests/sim_asyncio_ioc.py index 9f2b25b9..62eaac28 100644 --- a/tests/sim_asyncio_ioc.py +++ b/tests/sim_asyncio_ioc.py @@ -19,6 +19,8 @@ async def callback(value): await asyncio.sleep(0.5) print("async update %s" % value) + # Make sure it goes as epicsExit will not flush this for us + sys.stdout.flush() sim_records.t_ai.set(value) t_ao = builder.aOut('AO2', initial_value=12.45, on_update=callback) diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index ddf42315..12e1d530 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -16,8 +16,8 @@ def asyncio_ioc(): sim_ioc = os.path.join(os.path.dirname(__file__), "sim_asyncio_ioc.py") cmd = [sys.executable, sim_ioc, PV_PREFIX] proc = subprocess.Popen( - cmd, env=dict(os.environ, PYTHONUNBUFFERED="1"), - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + cmd, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) yield proc proc.kill() out, err = proc.communicate() @@ -33,7 +33,9 @@ async def test_asyncio_ioc(asyncio_ioc): atexit.unregister(_catools._catools_atexit) # Start - assert await caget(PV_PREFIX + ":UPTIME") in ["00:00:00", "00:00:01"] + assert await caget(PV_PREFIX + ":UPTIME") in [ + "00:00:00", "00:00:01", "00:00:02", "00:00:03" + ] # WAVEFORM await caput(PV_PREFIX + ":SINN", 4, wait=True) q = asyncio.Queue() @@ -42,11 +44,14 @@ async def test_asyncio_ioc(asyncio_ioc): # AO assert await caget(PV_PREFIX + ":AO2") == 12.45 await caput(PV_PREFIX + ":AO2", 3.56, wait=True) + await asyncio.sleep(0.3) assert await caget(PV_PREFIX + ":AI") == 12.34 - await asyncio.sleep(0.6) + await asyncio.sleep(0.5) assert await caget(PV_PREFIX + ":AI") == 3.56 + # Wait for a bit longer for the print output to flush + await asyncio.sleep(2) # Stop - out, err = asyncio_ioc.communicate(b"exit\n", timeout=1) + out, err = asyncio_ioc.communicate(b"exit\n", timeout=5) out = out.decode() err = err.decode() # Disconnect From ad39462c56504483a753220001cb2131de116ae0 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Wed, 19 May 2021 16:53:25 +0100 Subject: [PATCH 15/19] Update to DLS versions of deps --- Pipfile.lock | 225 +++++++++++++++++++++------------------------------ 1 file changed, 94 insertions(+), 131 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index b11db770..2f21cf15 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -16,7 +16,7 @@ "default": { "aioca": { "hashes": [ - "sha256:5caa44ca048e6b6a409255be1da21ca3b6b7df33117cb16f48102e1cb2979311", + "sha256:0add8e6f1294b8b392482fdebcbdd46465237c89d47580f6a6825f2dbbb042dc", "sha256:a1805c81c3e3f3cbc672b567ea82729dcb1345faf80c1bb07c3cf22f687503b6" ], "index": "pypi", @@ -62,40 +62,39 @@ }, "epicsdbbuilder": { "hashes": [ - "sha256:0c3bb0ae55d618474880c1004218a3d3ac640235d04426df6622b75229c563fc", - "sha256:be4c37aead8f2bb5500ef3e7693eee97ecd4fb3c1090324689062d0505de3325" + "sha256:71610b0a22555d8285a67f0a0da38c7e0e40c702db163d47d5860d9c48f9d308", + "sha256:9e2dc365479215e9bb83e8fe3c643a682233da350c282c464925a15b079be77c" ], - "version": "==1.4" + "version": "==1.4.3" }, "numpy": { "hashes": [ - "sha256:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010", - "sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd", - "sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43", - "sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9", - "sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df", - "sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400", - "sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2", - "sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4", - "sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a", - "sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6", - "sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8", - "sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b", - "sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8", - "sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb", - "sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2", - "sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f", - "sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4", - "sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a", - "sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16", - "sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f", - "sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69", - "sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65", - "sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17", - "sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48" - ], - "markers": "python_version >= '3.7'", - "version": "==1.20.3" + "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727", + "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6", + "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98", + "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7", + "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d", + "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2", + "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9", + "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935", + "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff", + "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee", + "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb", + "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042", + "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3", + "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5", + "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6", + "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f", + "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4", + "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737", + "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931", + "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6", + "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677", + "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576", + "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935", + "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd" + ], + "version": "==1.20.2" }, "scipy": { "hashes": [ @@ -138,7 +137,7 @@ "develop": { "aioca": { "hashes": [ - "sha256:5caa44ca048e6b6a409255be1da21ca3b6b7df33117cb16f48102e1cb2979311", + "sha256:0add8e6f1294b8b392482fdebcbdd46465237c89d47580f6a6825f2dbbb042dc", "sha256:a1805c81c3e3f3cbc672b567ea82729dcb1345faf80c1bb07c3cf22f687503b6" ], "index": "pypi", @@ -153,18 +152,16 @@ }, "attrs": { "hashes": [ - "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", - "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.2.0" + "version": "==20.3.0" }, "babel": { "hashes": [ "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9", "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.9.1" }, "certifi": { @@ -179,13 +176,9 @@ "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==4.0.0" }, "coverage": { - "extras": [ - "toml" - ], "hashes": [ "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", @@ -240,7 +233,6 @@ "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==5.5" }, "docutils": { @@ -248,7 +240,6 @@ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.16" }, "epicscorelibs": { @@ -284,18 +275,16 @@ }, "flake8": { "hashes": [ - "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", - "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" + "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378", + "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.9.2" + "version": "==3.9.1" }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "imagesize": { @@ -303,9 +292,16 @@ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.2.0" }, + "importlib-metadata": { + "hashes": [ + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + ], + "markers": "python_version < '3.8' and python_version < '3.8'", + "version": "==4.0.1" + }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -318,7 +314,6 @@ "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.11.3" }, "markupsafe": { @@ -328,12 +323,8 @@ "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", - "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", - "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", - "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", @@ -342,41 +333,25 @@ "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", - "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", - "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", - "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", - "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", - "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", - "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", - "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "mccabe": { @@ -388,40 +363,38 @@ }, "numpy": { "hashes": [ - "sha256:1676b0a292dd3c99e49305a16d7a9f42a4ab60ec522eac0d3dd20cdf362ac010", - "sha256:16f221035e8bd19b9dc9a57159e38d2dd060b48e93e1d843c49cb370b0f415fd", - "sha256:43909c8bb289c382170e0282158a38cf306a8ad2ff6dfadc447e90f9961bef43", - "sha256:4e465afc3b96dbc80cf4a5273e5e2b1e3451286361b4af70ce1adb2984d392f9", - "sha256:55b745fca0a5ab738647d0e4db099bd0a23279c32b31a783ad2ccea729e632df", - "sha256:5d050e1e4bc9ddb8656d7b4f414557720ddcca23a5b88dd7cff65e847864c400", - "sha256:637d827248f447e63585ca3f4a7d2dfaa882e094df6cfa177cc9cf9cd6cdf6d2", - "sha256:6690080810f77485667bfbff4f69d717c3be25e5b11bb2073e76bb3f578d99b4", - "sha256:66fbc6fed94a13b9801fb70b96ff30605ab0a123e775a5e7a26938b717c5d71a", - "sha256:67d44acb72c31a97a3d5d33d103ab06d8ac20770e1c5ad81bdb3f0c086a56cf6", - "sha256:6ca2b85a5997dabc38301a22ee43c82adcb53ff660b89ee88dded6b33687e1d8", - "sha256:6e51534e78d14b4a009a062641f465cfaba4fdcb046c3ac0b1f61dd97c861b1b", - "sha256:70eb5808127284c4e5c9e836208e09d685a7978b6a216db85960b1a112eeace8", - "sha256:830b044f4e64a76ba71448fce6e604c0fc47a0e54d8f6467be23749ac2cbd2fb", - "sha256:8b7bb4b9280da3b2856cb1fc425932f46fba609819ee1c62256f61799e6a51d2", - "sha256:a9c65473ebc342715cb2d7926ff1e202c26376c0dcaaee85a1fd4b8d8c1d3b2f", - "sha256:c1c09247ccea742525bdb5f4b5ceeacb34f95731647fe55774aa36557dbb5fa4", - "sha256:c5bf0e132acf7557fc9bb8ded8b53bbbbea8892f3c9a1738205878ca9434206a", - "sha256:db250fd3e90117e0312b611574cd1b3f78bec046783195075cbd7ba9c3d73f16", - "sha256:e515c9a93aebe27166ec9593411c58494fa98e5fcc219e47260d9ab8a1cc7f9f", - "sha256:e55185e51b18d788e49fe8305fd73ef4470596b33fc2c1ceb304566b99c71a69", - "sha256:ea9cff01e75a956dbee133fa8e5b68f2f92175233de2f88de3a682dd94deda65", - "sha256:f1452578d0516283c87608a5a5548b0cdde15b99650efdfd85182102ef7a7c17", - "sha256:f39a995e47cb8649673cfa0579fbdd1cdd33ea497d1728a6cb194d6252268e48" - ], - "markers": "python_version >= '3.7'", - "version": "==1.20.3" + "sha256:2428b109306075d89d21135bdd6b785f132a1f5a3260c371cee1fae427e12727", + "sha256:377751954da04d4a6950191b20539066b4e19e3b559d4695399c5e8e3e683bf6", + "sha256:4703b9e937df83f5b6b7447ca5912b5f5f297aba45f91dbbbc63ff9278c7aa98", + "sha256:471c0571d0895c68da309dacee4e95a0811d0a9f9f532a48dc1bea5f3b7ad2b7", + "sha256:61d5b4cf73622e4d0c6b83408a16631b670fc045afd6540679aa35591a17fe6d", + "sha256:6c915ee7dba1071554e70a3664a839fbc033e1d6528199d4621eeaaa5487ccd2", + "sha256:6e51e417d9ae2e7848314994e6fc3832c9d426abce9328cf7571eefceb43e6c9", + "sha256:719656636c48be22c23641859ff2419b27b6bdf844b36a2447cb39caceb00935", + "sha256:780ae5284cb770ade51d4b4a7dce4faa554eb1d88a56d0e8b9f35fca9b0270ff", + "sha256:878922bf5ad7550aa044aa9301d417e2d3ae50f0f577de92051d739ac6096cee", + "sha256:924dc3f83de20437de95a73516f36e09918e9c9c18d5eac520062c49191025fb", + "sha256:97ce8b8ace7d3b9288d88177e66ee75480fb79b9cf745e91ecfe65d91a856042", + "sha256:9c0fab855ae790ca74b27e55240fe4f2a36a364a3f1ebcfd1fb5ac4088f1cec3", + "sha256:9cab23439eb1ebfed1aaec9cd42b7dc50fc96d5cd3147da348d9161f0501ada5", + "sha256:a8e6859913ec8eeef3dbe9aed3bf475347642d1cdd6217c30f28dee8903528e6", + "sha256:aa046527c04688af680217fffac61eec2350ef3f3d7320c07fd33f5c6e7b4d5f", + "sha256:abc81829c4039e7e4c30f7897938fa5d4916a09c2c7eb9b244b7a35ddc9656f4", + "sha256:bad70051de2c50b1a6259a6df1daaafe8c480ca98132da98976d8591c412e737", + "sha256:c73a7975d77f15f7f68dacfb2bca3d3f479f158313642e8ea9058eea06637931", + "sha256:d15007f857d6995db15195217afdbddfcd203dfaa0ba6878a2f580eaf810ecd6", + "sha256:d76061ae5cab49b83a8cf3feacefc2053fac672728802ac137dd8c4123397677", + "sha256:e8e4fbbb7e7634f263c5b0150a629342cc19b47c5eba8d1cd4363ab3455ab576", + "sha256:e9459f40244bb02b2f14f6af0cd0732791d72232bbb0dc4bab57ef88e75f6935", + "sha256:edb1f041a9146dcf02cd7df7187db46ab524b9af2515f392f337c7cbbf5b52cd" + ], + "version": "==1.20.2" }, "packaging": { "hashes": [ "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==20.9" }, "pluggy": { @@ -429,7 +402,6 @@ "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.13.1" }, "py": { @@ -437,7 +409,6 @@ "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.10.0" }, "pycodestyle": { @@ -445,7 +416,6 @@ "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.7.0" }, "pyflakes": { @@ -453,7 +423,6 @@ "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.3.1" }, "pygments": { @@ -461,7 +430,6 @@ "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" ], - "markers": "python_version >= '3.5'", "version": "==2.9.0" }, "pyparsing": { @@ -469,32 +437,30 @@ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pytest": { "hashes": [ - "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", - "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" + "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634", + "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc" ], - "markers": "python_version >= '3.6'", - "version": "==6.2.4" + "version": "==6.2.3" }, "pytest-asyncio": { "hashes": [ - "sha256:2564ceb9612bbd560d19ca4b41347b54e7835c2f792c504f698e05395ed63f6f", - "sha256:3042bcdf1c5d978f6b74d96a151c4cfb9dcece65006198389ccd7e6c60eb1eea" + "sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d", + "sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700" ], "index": "pypi", - "version": "==0.15.1" + "version": "==0.14.0" }, "pytest-cov": { "hashes": [ - "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e", - "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e" + "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", + "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" ], "index": "pypi", - "version": "==2.12.0" + "version": "==2.11.1" }, "pytest-flake8": { "hashes": [ @@ -516,7 +482,6 @@ "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.25.1" }, "setuptools-dso": { @@ -535,11 +500,10 @@ }, "sphinx": { "hashes": [ - "sha256:b2566f5f339737a6ef37198c47d56de1f4a746c722bebdb2fe045c34bfd8b9d0", - "sha256:cf5104777571b2b7f06fa88ee08fade24563f4a0594cf4bd17d31c47b8740b4c" + "sha256:19010b7b9fa0dc7756a6e105b2aacd3a80f798af3c25c273be64d7beeb482cb1", + "sha256:2320d4e994a191f4b4be27da514e46b3d6b420f2ff895d064f52415d342461e8" ], - "markers": "python_version >= '3.6'", - "version": "==4.0.1" + "version": "==3.5.4" }, "sphinx-multiversion": { "editable": true, @@ -559,7 +523,6 @@ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a", "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-devhelp": { @@ -567,7 +530,6 @@ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e", "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4" ], - "markers": "python_version >= '3.5'", "version": "==1.0.2" }, "sphinxcontrib-htmlhelp": { @@ -575,7 +537,6 @@ "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f", "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-jsmath": { @@ -583,7 +544,6 @@ "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8" ], - "markers": "python_version >= '3.5'", "version": "==1.0.1" }, "sphinxcontrib-qthelp": { @@ -591,7 +551,6 @@ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72", "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6" ], - "markers": "python_version >= '3.5'", "version": "==1.0.3" }, "sphinxcontrib-serializinghtml": { @@ -599,7 +558,6 @@ "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc", "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a" ], - "markers": "python_version >= '3.5'", "version": "==1.1.4" }, "toml": { @@ -607,7 +565,6 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "typing-extensions": { @@ -620,11 +577,17 @@ }, "urllib3": { "hashes": [ - "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df", - "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937" + "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", + "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" + ], + "version": "==1.26.5" + }, + "zipp": { + "hashes": [ + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.4" + "version": "==3.4.1" } } } From d347ce07de2cd2dd70903a04c42e0366970d8fac Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Thu, 10 Jun 2021 16:23:55 +0100 Subject: [PATCH 16/19] Tweaks to make it work at DLS --- pyproject.toml | 2 +- setup.py | 2 +- tests/test_asyncio.py | 14 ++++++++------ tests/test_cothread.py | 9 +++------ 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 03c32fc5..788e2c44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] requires = ["setuptools", "wheel", "setuptools_dso", "epicscorelibs>=7.0.4.99.1"] -build-backend = "setuptools.build_meta" +build-backend = "setuptools.build_meta:__legacy__" diff --git a/setup.py b/setup.py index 4955d321..17be87f0 100644 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ def _add_file(f): ], dsos = ['epicscorelibs.lib.dbCore', 'epicscorelibs.lib.Com'], define_macros = get_config_var('CPPFLAGS'), - extra_compile_args = get_config_var('CFLAGS'), + extra_compile_args = get_config_var('CFLAGS') + ["-std=c99"], extra_link_args = get_config_var('LDFLAGS'), ) diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 12e1d530..10e3e443 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -19,10 +19,12 @@ def asyncio_ioc(): cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) yield proc - proc.kill() - out, err = proc.communicate() - print(out.decode()) - print(err.decode(), file=sys.stderr) + if proc.returncode is None: + # still running, kill it and print the output + proc.kill() + out, err = proc.communicate() + print(out.decode()) + print(err.decode(), file=sys.stderr) @pytest.mark.asyncio @@ -44,9 +46,9 @@ async def test_asyncio_ioc(asyncio_ioc): # AO assert await caget(PV_PREFIX + ":AO2") == 12.45 await caput(PV_PREFIX + ":AO2", 3.56, wait=True) - await asyncio.sleep(0.3) + await asyncio.sleep(0.1) assert await caget(PV_PREFIX + ":AI") == 12.34 - await asyncio.sleep(0.5) + await asyncio.sleep(0.6) assert await caget(PV_PREFIX + ":AI") == 3.56 # Wait for a bit longer for the print output to flush await asyncio.sleep(2) diff --git a/tests/test_cothread.py b/tests/test_cothread.py index 88c8bf6f..41b2f7c1 100644 --- a/tests/test_cothread.py +++ b/tests/test_cothread.py @@ -21,16 +21,13 @@ def cothread_ioc(): cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) yield proc - # This has to work on Python2 and 3 so can't rely on communicate to - # produce stdout and stderr if we already closed the process - try: + if proc.returncode is None: + # still running, kill it and print the output proc.kill() out, err = proc.communicate() print(out.decode()) print(err.decode()) - except OSError: - # Doesn't matter if its already stopped - pass + def test_cothread_ioc(cothread_ioc): From 7534530acf87897d13567379687507d550c9ea80 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Wed, 16 Jun 2021 13:28:28 +0100 Subject: [PATCH 17/19] Make extension.c more readable --- softioc/extension.c | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/softioc/extension.c b/softioc/extension.c index d7bb3a3e..c04ff630 100644 --- a/softioc/extension.c +++ b/softioc/extension.c @@ -245,15 +245,8 @@ static struct PyModuleDef softioc_module = { PyMOD(_extension) { #if PY_MAJOR_VERSION >= 3 - PyObject *mod = PyModule_Create(&softioc_module); + return PyModule_Create(&softioc_module); #else - PyObject *mod = Py_InitModule("softioc._extension", softioc_methods); -#endif - if(mod) { - } -#if PY_MAJOR_VERSION >= 3 - return mod; -#else - (void)mod; + Py_InitModule("softioc._extension", softioc_methods); #endif } From 7f14ed055786d98e7c4e8d4c6c1ed5eb8042aa91 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Mon, 21 Jun 2021 16:59:19 +0100 Subject: [PATCH 18/19] First pass at docs restructure --- CONTRIBUTING.rst | 72 +++ docs/_static/theme_overrides.css | 29 + docs/conf.py | 25 +- docs/explanations/use-cases.rst | 4 + docs/how-to/use-asyncio-in-an-ioc.rst | 4 + docs/index.rst | 77 ++- docs/records.rst | 141 ----- docs/reference/api.rst | 520 ++++++++++++++++++ docs/reference/contributing.rst | 3 + docs/softioc.rst | 433 --------------- .../creating-an-ioc.rst} | 60 +- docs/tutorials/installation.rst | 46 ++ softioc/asyncio_dispatcher.py | 5 + softioc/builder.py | 2 - softioc/softioc.py | 153 ++++-- 15 files changed, 898 insertions(+), 676 deletions(-) create mode 100644 CONTRIBUTING.rst create mode 100644 docs/explanations/use-cases.rst create mode 100644 docs/how-to/use-asyncio-in-an-ioc.rst delete mode 100644 docs/records.rst create mode 100644 docs/reference/api.rst create mode 100644 docs/reference/contributing.rst delete mode 100644 docs/softioc.rst rename docs/{pythonsoftioc.rst => tutorials/creating-an-ioc.rst} (83%) create mode 100644 docs/tutorials/installation.rst diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..67b5db65 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,72 @@ +Contributing +============ + +Contributions and issues are most welcome! All issues and pull requests are +handled through github on the `dls_controls repository`_. Also, please check for +any existing issues before filing a new one. If you have a great idea but it +involves big changes, please file a ticket before making a pull request! We +want to make sure you don't spend your time coding something that might not fit +the scope of the project. + +.. _dls_controls repository: https://github.com/dls-controls/pythonIoc/issues + +Running the tests +----------------- + +To get the source source code and run the unit tests, run:: + + $ git clone git://github.com/dls-controls/pythonIoc.git + $ cd pythonIoc + $ pipenv install --dev + $ pipenv run tests + +While 100% code coverage does not make a library bug-free, it significantly +reduces the number of easily caught bugs! Please make sure coverage remains the +same or is improved by a pull request! + +Code Styling +------------ + +The code in this repository conforms to standards set by the following tools: + +- flake8_ for style checks + +.. _flake8: http://flake8.pycqa.org/en/latest/ + +These tests will be run on code when running ``pipenv run tests`` and also +automatically at check in. Please read the tool documentation for details +on how to fix the errors it reports. + +Documentation +------------- + +Documentation is contained in the ``docs`` directory and extracted from +docstrings of the API. + +Docs follow the underlining convention:: + + Headling 1 (page title) + ======================= + + Heading 2 + --------- + + Heading 3 + ~~~~~~~~~ + + +You can build the docs from the project directory by running:: + + $ pipenv run docs + $ firefox build/html/index.html + + +Release Checklist +----------------- + +Before a new release, please go through the following checklist: + +- Choose a new PEP440 compliant release number +- Git tag the version with a message summarizing the changes +- Push to github and the actions will make a release on pypi +- Push to internal gitlab and do a dls-release.py of the tag diff --git a/docs/_static/theme_overrides.css b/docs/_static/theme_overrides.css index 95ed8f87..df732c7e 100644 --- a/docs/_static/theme_overrides.css +++ b/docs/_static/theme_overrides.css @@ -1,4 +1,33 @@ +/* override table width restrictions */ +@media screen and (min-width: 639px) { + .wy-table-responsive table td { + /* !important prevents the common CSS stylesheets from + overriding this as on RTD they are loaded after this stylesheet */ + white-space: normal !important; + } +} + +/* override table padding */ +.rst-content table.docutils th, .rst-content table.docutils td { + padding: 4px 6px; +} + /* allow us to stop margin after list so we can add to it */ .no-margin-after-ul ul { margin-bottom: 0 !important; } + +/* Add two-column option */ +@media only screen and (min-width: 1000px), + only screen and (min-width: 500px) and (max-width: 768px){ + .columns { + padding-left: 5px; + padding-right: 5px; + float: left; + width: 50%; + } +} + +.endcolumns { + clear: both +} diff --git a/docs/conf.py b/docs/conf.py index bdedd0ef..d8cedf34 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,8 +22,9 @@ import softioc # noqa # General information about the project. -project = u'Python Soft IOC' -copyright = u'2011, Michael Abbott' +project = u'pythonIoc' +copyright = u'2008, Diamond Light Source' +author = 'Michael Abbott' # The full version, including alpha/beta/rc tags. release = softioc.__version__ @@ -40,6 +41,10 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ + # Use this for generating API docs + 'sphinx.ext.autodoc', + # This can parse google style docstrings + 'sphinx.ext.napoleon', # For linking to external sphinx documentation 'sphinx.ext.intersphinx', # Add links to source code in API docs @@ -48,8 +53,6 @@ 'sphinx_multiversion', ] -viewcode_import = True - # If true, Sphinx will warn about all references where the target cannot # be found. nitpicky = True @@ -60,6 +63,20 @@ # ('envvar', 'LD_LIBRARY_PATH'). nitpick_ignore = [('py:func', 'int')] +# Both the class’ and the __init__ method’s docstring are concatenated and +# inserted into the main body of the autoclass directive +autoclass_content = 'both' + +# Order the members by the order they appear in the source code +autodoc_member_order = 'bysource' + +# Don't inherit docstrings from baseclasses +autodoc_inherit_docstrings = False + +# The name of a reST role (builtin or Sphinx extension) to use as the default +# role, that is, for text marked up `like this` +default_role = 'any' + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/explanations/use-cases.rst b/docs/explanations/use-cases.rst new file mode 100644 index 00000000..e66c3431 --- /dev/null +++ b/docs/explanations/use-cases.rst @@ -0,0 +1,4 @@ +Use Cases +========= + +Write about what it is good for diff --git a/docs/how-to/use-asyncio-in-an-ioc.rst b/docs/how-to/use-asyncio-in-an-ioc.rst new file mode 100644 index 00000000..016d0462 --- /dev/null +++ b/docs/how-to/use-asyncio-in-an-ioc.rst @@ -0,0 +1,4 @@ +Use `asyncio` in an IOC +======================= + +Write about the differences creating an IOC using `AsyncioDispatcher` diff --git a/docs/index.rst b/docs/index.rst index f156b34c..bf6780be 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,19 +1,74 @@ -.. _index: - .. include:: ../README.rst :end-before: when included in index.rst +How the documentation is structured +----------------------------------- + +.. rst-class:: columns + +:ref:`tutorials` +~~~~~~~~~~~~~~~~ + +Tutorials for installation, library and commandline usage. New users start here. + +.. rst-class:: columns + +:ref:`how-to` +~~~~~~~~~~~~~ + +Practical step-by-step guides for the more experienced user. + +.. rst-class:: columns + +:ref:`explanations` +~~~~~~~~~~~~~~~~~~~ + +Explanation of how the library works and why it works that way. + +.. rst-class:: columns + +:ref:`reference` +~~~~~~~~~~~~~~~~ + +Technical reference material, for classes, methods, APIs, commands, and contributing to the project. + +.. rst-class:: endcolumns + +About the documentation +~~~~~~~~~~~~~~~~~~~~~~~ + +`Why is the documentation structured this way? `_ + +.. toctree:: + :caption: Tutorials + :name: tutorials + :maxdepth: 1 + + tutorials/installation + tutorials/creating-an-ioc + +.. toctree:: + :caption: How-to Guides + :name: how-to + :maxdepth: 1 + + how-to/use-asyncio-in-an-ioc + +.. toctree:: + :caption: Explanations + :name: explanations + :maxdepth: 1 + + explanations/use-cases + .. rst-class:: no-margin-after-ul -.. toctree:: - :numbered: - :maxdepth: 2 +.. toctree:: + :caption: Reference + :name: reference + :maxdepth: 1 - pythonsoftioc - records - softioc + reference/api + reference/contributing * :ref:`genindex` -* :ref:`modindex` -* :ref:`search` -* `Selected source code <_modules/index.html>`_ diff --git a/docs/records.rst b/docs/records.rst deleted file mode 100644 index b347db71..00000000 --- a/docs/records.rst +++ /dev/null @@ -1,141 +0,0 @@ -.. _records: - -Record Support in the Python Soft IOC -===================================== - -.. module:: softioc.device - :synopsis: Implementation of Python Device support - - -The Python soft IOC implements EPICS device support (almost) entirely in Python. -This is used to invoke Python processing in response to record processing, -making it easy to integrate Python into the EPICS IOC layer. - -Records are created dynamically during IOC startup before calling -:func:`~softioc.softioc.iocInit` and with the help of the :mod:`softioc.builder` module can be -loaded with :func:`~softioc.builder.LoadDatabase`. - -All records are created internally using methods of the ``PythonDevice`` -class, one method for each of the supported record types, however the -corresponding wrapping functions published by :mod:`softioc.builder` should be -used as they configure sensible defaults and are generally easier to use. - -Create IN records (used for publishing data *from* the IOC, the naming of the -direction is confusing) using the following :mod:`softioc.builder` methods: - - :func:`~softioc.builder.aIn`, :func:`~softioc.builder.boolIn`, - :func:`~softioc.builder.longIn`, :func:`~softioc.builder.stringIn`, - :func:`~softioc.builder.mbbIn`, :func:`~softioc.builder.Waveform`. - -Create OUT records for receiving control information into the IOC using the -following methods: - - :func:`~softioc.builder.aOut`, :func:`~softioc.builder.boolOut`, - :func:`~softioc.builder.longOut`, :func:`~softioc.builder.stringOut`, - :func:`~softioc.builder.mbbOut`, :func:`~softioc.builder.WaveformOut`. - -For all records the `initial_value` keyword argument can be used to specify the -records value on startup. - - -Working with IN records ------------------------ - -EPICS IN records are implemented as subclasses of the -:class:`ProcessDeviceSupportIn` class which provides the methods documented -below. - -.. class:: ProcessDeviceSupportIn - - This class is used to implement Python device support for the record types - ``ai``, ``bi``, ``longin``, ``mbbi`` and IN ``waveform`` records. - - .. method:: set(value, severity=NO_ALARM, alarm=UDF_ALARM, timestamp=None) - - Updates the stored value and severity status and triggers an update. If - ``SCAN`` has been set to ``'I/O Intr'`` (which is the default if the - :mod:`~softioc.builder` methods have been used) then the record will be - processed by EPICS and the given value will be published to all users. - - Optionally an explicit timestamp can be set. This is a value in seconds - in the Unix epoch, as returned by :func:`time.time`. This argument only - has any effect if ``TSE = -2`` was set when the record was created. - - Note that when calling :func:`set` for a waveform record the value is - always copied immediately -- this avoids accidents with mutable values. - - .. method:: set_alarm(severity, alarm, timestamp=None) - - This is exactly equivalent to calling:: - - rec.set(rec.get(), severity, alarm, timestamp) - - and triggers an alarm status change without changing the value. - - .. method:: get() - - This returns the value last written to this record with :func:`set`. - - Note that channel access puts to a Python soft IOC input record are - completely ineffective, and this includes waveform records. - - -Working with OUT records ------------------------- - -When creating OUT records three further optional keyword arguments can be -specified: - -`on_update` - If used this should be set to a callable taking exactly one argument. After - successful record processing this function will be called with the new value - just written to the record. - - Note that this callback occurs at an unpredictable time after record - processing and if repeated high speed channel access puts are in progress it - is possible that callbacks may be delayed. Each callback will be passed the - value at the time the record was processed. - - Note also that `on_update` callbacks occur as part of cothread processing - and normal cothread operations can occur during the callback. However only - one callback is dispatched at a time, so if a callback blocks it will delay - `on_update` callbacks for other records. - -`on_update_name` - This is an alternative form of `on_update` with the same behaviour: note - that at most one of `on_update` and `on_update_name` may be passed. The - difference is that `on_update_name` is called with the record name as its - second argument after the value as the first argument. - -`validate` - If used this should be set to a callable taking two arguments. The first - argument will be the record object, and the second will be the new value - being written. The `validiate` function can reject the update by returning - `False` or accept it by returning `True`. - - Note that this function is called asynchronously on a thread determined by - EPICS and it is not safe to perform any cothread actions within this - callback. - -`always_update` - This flag defaults to `False`, in which case updates to the record - which don't change its value will be discarded. In particular this means - that such updates don't call `validate` or `on_update`. - -.. class:: ProcessDeviceSupportOut - - This class is used to implement Python device support for the record types - ``ao``, ``bo``, ``longout``, ``mbbo`` and OUT ``waveform`` records. All OUT - records support the following methods. - - .. method:: set(value, process=True) - - Updates the value associated with the record. By default this will - trigger record processing, and so will cause any associated `on_update` - and `validate` methods to be called. If `process` is `False` - then neither of these methods will be called, but the value will still - be updated. - - .. method:: get() - - Returns the value associated with the record. diff --git a/docs/reference/api.rst b/docs/reference/api.rst new file mode 100644 index 00000000..a4bf0e61 --- /dev/null +++ b/docs/reference/api.rst @@ -0,0 +1,520 @@ +.. _API: + +API +=== + +.. automodule:: softioc + + ``softioc`` + ----------- + + The top level softioc module contains a number of packages that can be used + in the creation of IOCs: + + `softioc.softioc` + This module wraps the basic interface to the EPICS IOC. A large number of + interactive EPICS commands are wrapped and can be made available through the + interpreter by invoking the interpreter through this module. + + `softioc.asyncio_dispatcher` + A dispatcher for `asyncio` based applications instead of the default + `cothread` one + + `softioc.alarm` + This module simply contains definitions for severity and alarm values taken + from the EPICS ``alarm.h`` header file. + + `softioc.builder` + This module provides facilities for creating PVs. + + `softioc.pvlog` + The act of importing this module configures the IOC to log every external + put to the database. + + The following submodules implement internals and should not normally be looked + at directly: + + ``softioc.imports`` + Imports and wraps C functions from EPICS IOC support. + + ``softioc.fields`` + Used internally as part of record support to implement access to EPICS + record fields. + + ``softioc.device_core`` + Implements the basics of ``Python`` EPICS device support. + + `softioc.device` + Implements ``Python`` device support for all the record types supported. + + ``softioc.pythonSoftIoc`` + Implements `epicsdbbuilder` interface for all of the ``Python`` records. + +.. data:: softioc.__version__ + :type: str + + Version number as calculated by https://github.com/dls-controls/versiongit + +.. automodule:: softioc.softioc + + .. + NOTE: can't use :members: here as it calls repr on the exit object which + makes the process exit! + + Top Level IOC Interface: `softioc.softioc` + ------------------------------------------ + + This module provides the following functions for general use (available by + importing ``*``): + +.. autofunction:: iocInit + +.. autofunction:: dbLoadDatabase + + .. note:: + + This function is not normally called directly, instead + `softioc.builder.LoadDatabase` is normally used to create and load the + EPICS database on the fly. + + However, if required, an existing EPICS database can be loaded + explicitly using this method. Note that `dbLoadDatabase` cannot be + called after `iocInit`. + +.. autofunction:: devIocStats + +.. autofunction:: interactive_ioc + +While the interactive shell is running a number of EPICS test functions are made +available for use together with the constant value `exit` with special +behaviour: typing `exit` at the interpreter prompt will immediately call +``epicsExit`` causing the Python interpreter and IOC to terminate. + +This module provides Python wrappers for the following EPICS test functions and +makes them available to the `interactive_ioc` interpreter shell. See the `IOC +Test Facilities`_ documentation for more details of each function. + +.. _IOC Test Facilities: + https://docs.epics-controls.org/en/latest/appdevguide/IOCTestFacilities.html + +.. autofunction:: dba +.. autofunction:: dbl +.. autofunction:: dbnr +.. autofunction:: dbgrep +.. autofunction:: dbgf +.. autofunction:: dbpf +.. autofunction:: dbpr +.. autofunction:: dbtr +.. autofunction:: dbtgf +.. autofunction:: dbtpf +.. autofunction:: dbior +.. autofunction:: dbhcr +.. autofunction:: gft +.. autofunction:: pft +.. autofunction:: tpn +.. autofunction:: dblsr +.. autofunction:: dbLockShowLocked +.. autofunction:: scanppl +.. autofunction:: scanpel +.. autofunction:: scanpiol +.. autofunction:: generalTimeReport +.. autofunction:: eltc + +.. attribute:: exit + + Displaying this value will invoke ``epicsExit()`` causing the IOC to + terminate immediately. + +.. automodule:: softioc.asyncio_dispatcher + :members: + + Asyncio Dispatcher: `softioc.asyncio_dispatcher` + ------------------------------------------------ + + If your application uses `asyncio` then this module gives an alternative + dispatcher for caput requests. + +.. automodule:: softioc.builder + + Creating Records: `softioc.builder` + ----------------------------------- + + This module publishes functions for creating records. All of the other methods + in this module must be called before calling :func:`LoadDatabase`, after which + no function in this module is usable. + + See `softioc.device` for a detailed explanation of record support and creation, + but note that only the following records types have direct support from this + module: + + ai, ao, bi, bo, longin, longout, mbbi, mbbo, stringin, stringout, waveform + + The following methods create records of the corresponding type. For all records + the `initial_value` parameter can be used to specify an initial value for the + record. + + The following optional keyword arguments are available for all of these + functions: + + .. _initial_value: + + `initial_value` + ~~~~~~~~~~~~~~~ + + This is used to specify an initial value for each record. + + .. _on_update: + + `on_update` + ~~~~~~~~~~~ + + This is only available on OUT records (including those created by + :func:`WaveformOut`). This specifies a function that will be called after + record processing has completed. + + If used this should be set to a callable taking exactly one argument. After + successful record processing this function will be called with the new value + just written to the record. + + Note that this callback occurs at an unpredictable time after record + processing and if repeated high speed channel access puts are in progress it + is possible that callbacks may be delayed. Each callback will be passed the + value at the time the record was processed. + + Note also that `on_update` callbacks occur as part of cothread processing + and normal cothread operations can occur during the callback. However only + one callback is dispatched at a time, so if a callback blocks it will delay + `on_update` callbacks for other records. + + .. _on_update_name: + + `on_update_name` + ~~~~~~~~~~~~~~~~ + + This is an alternative form of `on_update` with the same behaviour: note + that at most one of `on_update` and `on_update_name` may be passed. The + difference is that `on_update_name` is called with the record name as its + second argument after the value as the first argument. + + .. _validate: + + `validate` + ~~~~~~~~~~ + + Also only available on OUT records, specifies a function called during + record processing. If used this should be set to a callable taking two + arguments. The first argument will be the record object, and the second + will be the new value being written. The `validate` function can reject + the update by returning `False` or accept it by returning `True`. + + .. note:: + + This function is called asynchronously on a thread determined by + EPICS and it is not safe to perform any cothread actions within this + callback. + + .. _always_update: + + `always_update` + ~~~~~~~~~~~~~~~ + + Again only on OUT records, determines whether record writes which don't + change the existing value are passed through. If this field is not set then + writing to ``.PROC`` will have no visible effect. + + This flag defaults to `False`, in which case updates to the record + which don't change its value will be discarded. In particular this means + that such updates don't call `validate` or `on_update`. + +For all of these functions any EPICS database field can be assigned a value by +passing it as a keyword argument for the corresponding field name (in upper +case) or by assigning to the corresponding field of the returned record object. +Thus the ``**fields`` argument in all of the definitions below refers to both the +optional keyword arguments listed above and record field names. + +All functions return a wrapped `ProcessDeviceSupportIn` or +`ProcessDeviceSupportOut` instance. + +.. function:: + aIn(name, LOPR=None, HOPR=None, **fields) + aOut(name, LOPR=None, HOPR=None, **fields) + + Create ``ai`` and ``ao`` records. The lower and upper limits for the + record can be specified, and if specified these will also be used to set the + ``EGUL`` and ``EGUF`` fields. + +.. function:: + boolIn(name, ZNAM=None, ONAM=None, **fields) + boolOut(name, ZNAM=None, ONAM=None, **fields) + + Create ``bi`` and ``bo`` records with the specified names for false (zero) + and true (one). + +.. function:: + longIn(name, LOPR=None, HOPR=None, EGU=None, **fields) + longOut(name, DRVL=None, DRVH=None, EGU=None, **fields) + + Create ``longin`` and ``longout`` records with specified limits and units. + +.. function:: + stringIn(name, **fields) + stringOut(name, **fields) + + Create ``stringin`` and ``stringout`` records. + +.. function:: + mbbIn(name, *option_values, **fields) + mbbOut(name, *option_values, **fields) + + Create ``mbbi`` and ``mbbo`` records. Up to 16 options can be specified as + a list of two or three field tuples. The first field of each tuple is the + option name, the second field is the option value, and the third optional + field is the option severity. For example:: + + status = mbbIn('STATUS', + ('OK', 0), + ('FAILING', 1, alarm.MINOR_ALARM), + ('FAILED', 2, alarm.MAJOR_ALARM)) + +.. function:: + Waveform(name, [value,] **fields) + WaveformOut(name, [value,] **fields) + + Create ``waveform`` records. Depending on whether `Waveform` or + `WaveformOut` is called the record is configured to behave as an IN or an + OUT record, in particular `on_update` can only be specified when calling + `WaveformOut`. + + If ``value`` is specified or if an `initial_value` is specified (only one of + these can be used) the value is used to initialise the waveform and to + determine its field type and length. If no initial value is specified then + the keyword argument ``length`` must be used to specify the length of the + waveform. + + The field type can be explicitly specified either by setting the ``datatype`` + keyword to a Python type name, or by setting ``FTVL`` to the appropriate EPICS + field type name. Otherwise the field type is taken from the initial value + if given, or defaults to ``'FLOAT'``. + + +The following function generates a specialised record. + +.. function:: Action(name, **fields) + + Creates a record (using `boolOut`) which will always call the `on_update` + method when processed. Used for action records. The `on_update` keyword + should always be passed. + + +The following functions manage record names. The record device name must be +specified before creating records, then each record will be created with a +standard two part name of the form ``device:name`` where the ``device`` part is +specified by the functions below and the ``name`` part is specified in the +record creation function. + +.. function:: SetDeviceName(device_name) + + Sets up the prefix part of the record name, referred to here as the "device" + part. This function must be called before creating any records. + Note that + only this function need be used, the three other functions below are + entirely optional. + +.. function:: UnsetDevice() + + This can optionally be called after completing the creation of records to + prevent the accidential creation of records with the currently set device + name. + + +The following helper functions are useful when constructing links between +records. + +.. function:: + PP(record) + CP(record) + NP(record) + MS(record) + + When assigned to a link field in a record these functions add the + appropriate processing attributes to the link. These are not normally used. + + +The following attributes allow more direct access to record creation. + +.. attribute:: records + + This is the ``iocbuilder`` records object, and is populated with + functions named after each available record type. Records created with + these calls are created with soft device support and Python is not involved + in their processing. + + The following example shows a calc record being used to post-process a + standard Python IOC record:: + + from softioc import builder + builder.SetDeviceName('XX-XX-XX-01') + rec = aIn('VALUE') + calc = records.calc('CALC', CALC = 'A*B', A = rec, B = 42) + rec.FLNK = PP(calc) + + +Finally, the following function is used to load record definitions before +starting the IOC. + +.. function:: LoadDatabase() + + This must be called exactly once after creating all the records required by + the IOC and before calling :func:`~softioc.softioc.iocInit`. After this + function has been called none of the functions provided by + :mod:`softioc.builder` are usable. + +.. automodule:: softioc.alarm + + Alarm Value Definitions: `softioc.alarm` + ---------------------------------------- + + +The following values can be passed to IN record :meth:`~softioc.device.ProcessDeviceSupportIn.set` and +:meth:`~softioc.device.ProcessDeviceSupportIn.set_alarm` methods. + +.. attribute:: + NO_ALARM = 0 + MINOR_ALARM = 1 + MAJOR_ALARM = 2 + INVALID_ALARM = 3 + + These are severity values. The default severity is ``NO_ALARM``. + +.. attribute:: + READ_ALARM + WRITE_ALARM + HIHI_ALARM + HIGH_ALARM + LOLO_ALARM + LOW_ALARM + STATE_ALARM + COS_ALARM + COMM_ALARM + TIMEOUT_ALARM + HW_LIMIT_ALARM + CALC_ALARM + SCAN_ALARM + LINK_ALARM + SOFT_ALARM + BAD_SUB_ALARM + UDF_ALARM + DISABLE_ALARM + SIMM_ALARM + READ_ACCESS_ALARM + WRITE_ACCESS_ALARM + + Alarm code definitions. Frankly these values aren't terribly useful, only + the severity is used for most notifications, but an alarm code needs to be + specified when specifying a non zero severity. + + +.. automodule:: softioc.pvlog + + Automatic PV logging: `softioc.pvlog` + ------------------------------------- + + Once this module has been imported all channel access writes to any PV published + by this IOC will be logged by writing a suitable message to stdout. There is + currently no control or customisation of this feature. + +.. automodule:: softioc.device + + Record Support in the Python Soft IOC: `softioc.device` + ------------------------------------------------------- + +The Python soft IOC implements EPICS device support (almost) entirely in Python. +This is used to invoke Python processing in response to record processing, +making it easy to integrate Python into the EPICS IOC layer. + +Records are created dynamically during IOC startup before calling +:func:`~softioc.softioc.iocInit` and with the help of the `softioc.builder` +module can be loaded with :func:`~softioc.builder.LoadDatabase`. + +All records are created internally using methods of the ``PythonDevice`` +class, one method for each of the supported record types, however the +corresponding wrapping functions published by `softioc.builder` should be +used as they configure sensible defaults and are generally easier to use. + +Create IN records (used for publishing data *from* the IOC, the naming of the +direction is confusing) using the following `softioc.builder` methods: + + :func:`~softioc.builder.aIn`, :func:`~softioc.builder.boolIn`, + :func:`~softioc.builder.longIn`, :func:`~softioc.builder.stringIn`, + :func:`~softioc.builder.mbbIn`, :func:`~softioc.builder.Waveform`. + +Create OUT records for receiving control information into the IOC using the +following methods: + + :func:`~softioc.builder.aOut`, :func:`~softioc.builder.boolOut`, + :func:`~softioc.builder.longOut`, :func:`~softioc.builder.stringOut`, + :func:`~softioc.builder.mbbOut`, :func:`~softioc.builder.WaveformOut`. + +For all records the `initial_value` keyword argument can be used to specify the +records value on startup. + +Working with IN records +~~~~~~~~~~~~~~~~~~~~~~~ + +EPICS IN records are implemented as subclasses of the `ProcessDeviceSupportIn` +class which provides the methods documented below. + +.. class:: ProcessDeviceSupportIn + + This class is used to implement Python device support for the record types + ``ai``, ``bi``, ``longin``, ``mbbi`` and IN ``waveform`` records. + + .. method:: set(value, severity=NO_ALARM, alarm=UDF_ALARM, timestamp=None) + + Updates the stored value and severity status and triggers an update. If + ``SCAN`` has been set to ``'I/O Intr'`` (which is the default if the + :mod:`~softioc.builder` methods have been used) then the record will be + processed by EPICS and the given value will be published to all users. + + Optionally an explicit timestamp can be set. This is a value in seconds + in the Unix epoch, as returned by :func:`time.time`. This argument only + has any effect if ``TSE = -2`` was set when the record was created. + + Note that when calling :func:`set` for a waveform record the value is + always copied immediately -- this avoids accidents with mutable values. + + .. method:: set_alarm(severity, alarm, timestamp=None) + + This is exactly equivalent to calling:: + + rec.set(rec.get(), severity, alarm, timestamp) + + and triggers an alarm status change without changing the value. + + .. method:: get() + + This returns the value last written to this record with :func:`set`. + + Note that channel access puts to a Python soft IOC input record are + completely ineffective, and this includes waveform records. + +Working with OUT records +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: ProcessDeviceSupportOut + + This class is used to implement Python device support for the record types + ``ao``, ``bo``, ``longout``, ``mbbo`` and OUT ``waveform`` records. All OUT + records support the following methods. + + .. method:: set(value, process=True) + + Updates the value associated with the record. By default this will + trigger record processing, and so will cause any associated `on_update` + and `validate` methods to be called. If ``process`` is `False` + then neither of these methods will be called, but the value will still + be updated. + + .. method:: get() + + Returns the value associated with the record. diff --git a/docs/reference/contributing.rst b/docs/reference/contributing.rst new file mode 100644 index 00000000..2b6578f6 --- /dev/null +++ b/docs/reference/contributing.rst @@ -0,0 +1,3 @@ +.. _contributing: + +.. include:: ../../CONTRIBUTING.rst diff --git a/docs/softioc.rst b/docs/softioc.rst deleted file mode 100644 index 7d655a84..00000000 --- a/docs/softioc.rst +++ /dev/null @@ -1,433 +0,0 @@ -.. _softioc: - -Soft IOC Python Module -====================== - -.. module:: softioc - :synopsis: Top level module of Python Soft IOC facilities - - -The :mod:`softioc` module is an integral part of the Python soft IOC server -``pythonIoc``. Its path is automatically available to the interpreter and the -following submodules can be loaded: - -:mod:`softioc.softioc` - This module wraps the basic interface to the EPICS IOC. A large number of - interactive EPICS commands are wrapped and can be made available through the - interpreter by invoking the interpreter through this module. - -:mod:`softioc.alarm` - This module simply contains definitions for severity and alarm values taken - from the EPICS ``alarm.h`` header file. - -:mod:`softioc.builder` - This module provides facilities for creating PVs. - -:mod:`softioc.pvlog` - The act of importing this module configures the IOC to log every external - put to the database. - - -The following submodules implement internals and should not normally be looked -at directly: - -``softioc.imports`` - Imports and wraps C functions from EPICS IOC support. - -``softioc.fields`` - Used internally as part of record support to implement access to EPICS - record fields. - -``softioc.device_core`` - Implements the basics of ``Python`` EPICS device support. - -:mod:`softioc.device` - Implements ``Python`` device support for all the record types supported. - -``softioc.pythonSoftIoc`` - Implements ``iocbuilder`` interface for all of the ``Python`` records. - - -Top Level IOC Interface: :mod:`softioc.softioc` ------------------------------------------------ - -.. module:: softioc.softioc - :synopsis: Interface to IOC creation and startup functionality - - -This module provides the following functions for general use (available by -importing ``*``): - -.. function:: iocInit() - - This must be called exactly once after loading all EPICS database files. - After this point the EPICS IOC is running and serving PVs. - -.. function:: dbLoadDatabase(database, path=None, substitutions=None) - - This loads the specified EPICS database into the IOC with any given - substitutions. Note that this function is not normally called directly, - instead :mod:`softioc.builder` and its :func:`~softioc.builder.LoadDatabase` - method is normally used to create and load the EPICS database on the fly. - - However, if required, an existing EPICS database can be loaded explicitly - using this method. Note that :func:`dbLoadDatabase` cannot be called after - :func:`iocInit`. - -.. function:: devIocStats(ioc_name) - - This will load a template for the devIocStats library with the specified IOC - name. This should be called before :func:`iocInit`. - -.. function:: interactive_ioc(context={}, call_exit=True) - - This is the normal way to run an interactive shell after starting the IOC. - The `context` argument is a dictionary of values that will be made available - to the interactive Python shell together with a number of EPICS test - functions. By default, if `call_exit` is `True`, the IOC will be - terminated by calling ``epicsExit()`` when the interpreter exits, which - means that :func:`interactive_ioc` will not return. - - While the interactive shell is running a number of EPICS test functions are - made available for use together with the constant value :const:`exit` with - special behaviour: typing :const:`exit` at the interpreter prompt will - immediately call ``epicsExit()`` causing the Python interpreter and IOC to - terminate. - -This module provides Python wrappers for the following EPICS test functions and -makes them available to the :func:`interactive_ioc` interpreter shell. See the -EPICS documentation for more details of each function. - -.. function:: dba(field) - - Prints value of each field in dbAddr structure associated with field. - -.. function:: dbl(pattern='', fields='') - - Prints the names of records in the database matching pattern. If - a (space separated) list of fields is also given then the values of - the fields are also printed. - -.. function:: dbnr(all=0) - - Print number of records of each record type. - -.. function:: dbgrep(pattern) - - Lists all record names that match the pattern. '*' matches any number of - characters in a record name. - -.. function:: dbgf(field) - - Prints field type and value. - -.. function:: dbpf(field, value) - - Writes the given value into the field. - -.. function:: dbpr(record, interest=0) - - Prints all the fields in record up to the indicated interest level: - - = =========================== - 0 Application fields which change during record processing - 1 Application fields which are fixed during processing - 2 System developer fields of major interest - 3 System developer fields of minor interest - 4 All other fields. - = =========================== - -.. function:: dbtr(record) - - Tests processing of the specified record. - -.. function:: dbior(driver='', interest=0) - - Prints driver reports for the selected driver (or all drivers if - driver is omitted) at the given interest level. - -.. function:: dbhcr() - - Prints hardware configuration report. - -.. function:: scanppl(rate=0.0) - - Prints all records with the selected scan rate (or all if rate=0). - -.. function:: scanpel(event=0) - - Prints all records with selected event number (or all if event=0). - -.. function:: scanpiol() - - Prints all records in the I/O event scan lists. - -.. function:: generalTimeReport(level=0) - - Displays time providers and their status - -.. function:: eltc(enable) - - Turn EPICS logging on or off. - -.. function:: - dbLockShowLocked() - dblsr() - dblsr() - dbtgf() - dbtpf() - dbtpn() - gft() - pft() - tpn() - - These are all wrappers around the corresponding EPICS function, see the - EPICS documentation for details of their meaning and behaviour. - -.. attribute:: exit - - Displaying this value will invoke ``epicsExit()`` causing the IOC to - terminate immediately. - - -Creating Records: :mod:`softioc.builder` ----------------------------------------- - -.. module:: softioc.builder - :synopsis: Tools for building Python bound PVs - - -This module publishes functions for creating records. All of the other methods -in this module must be called before calling :func:`LoadDatabase`, after which -no function in this module is usable. - -See :ref:`records` for a detailed explanation of record support and creation, -but note that only the following records types have direct support from this -module: - - ai, ao, bi, bo, longin, longout, mbbi, mbbo, stringin, stringout, waveform - -The following methods create records of the corresponding type. For all records -the `initial_value` parameter can be used to specify an initial value for the -record. - -The following optional keyword arguments are available for all of these -functions: - -`initial_value` - This is used to specify an initial value for each record. - -`on_update` - This is only available on OUT records (including those created by - :func:`WaveformOut`). This specifies a function that will be called after - record processing has completed. - -`on_update_name` - This is an alternative callback function to use instead of `on_update`. - This function will be passed the record name as well as updated value. - -`validate` - Also only available on OUT records, specifies a function called during - record processing. Note that this function is not cothread safe, that is to - say, it is not called on the cothread thread. - -`always_update` - Again only on OUT records, determines whether record writes which don't - change the existing value are passed through. If this field is not set then - writing to ``.PROC`` will have no visible effect. - -For all of these functions any EPICS database field can be assigned a value by -passing it as a keyword argument for the corresponding field name (in upper -case) or by assigning to the corresponding field of the returned record object. -Thus the `**fields` argument in all of the definitions below refers to both the -optional keyword arguments listed above and record field names. - -.. function:: - aIn(name, LOPR=None, HOPR=None, **fields) - aOut(name, LOPR=None, HOPR=None, **fields) - - Create ``ai`` and ``ao`` records. The lower and upper limits for the - record can be specified, and if specified these will also be used to set the - ``EGUL`` and ``EGUF`` fields. - -.. function:: - boolIn(name, ZNAM=None, ONAM=None, **fields) - boolOut(name, ZNAM=None, ONAM=None, **fields) - - Create ``bi`` and ``bo`` records with the specified names for false (zero) - and true (one). - -.. function:: - longIn(name, LOPR=None, HOPR=None, EGU=None, **fields) - longOut(name, DRVL=None, DRVH=None, EGU=None, **fields) - - Create ``longin`` and ``longout`` records with specified limits and units. - -.. function:: - stringIn(name, **fields) - stringOut(name, **fields) - - Create ``stringin`` and ``stringout`` records. - -.. function:: - mbbIn(name, *option_values, **fields) - mbbOut(name, *option_values, **fields) - - Create ``mbbi`` and ``mbbo`` records. Up to 16 options can be specified as - a list of two or three field tuples. The first field of each tuple is the - option name, the second field is the option value, and the third optional - field is the option severity. For example:: - - status = mbbIn('STATUS', - ('OK', 0), - ('FAILING', 1, alarm.MINOR_ALARM), - ('FAILED', 2, alarm.MAJOR_ALARM)) - -.. function:: - Waveform(name, [value,] **fields) - WaveformOut(name, [value,] **fields) - - Create ``waveform`` records. Depending on whether :func:`Waveform` or - :func:`WaveformOut` is called the record is configured to behave as an IN or - an OUT record, in particular `on_update` can only be specified when calling - :func:`WaveformOut`. - - If `value` is specified or if an `initial_value` is specified (only one of - these can be used) the value is used to initialise the waveform and to - determine its field type and length. If no initial value is specified then - the keyword argument `length` must be used to specify the length of the - waveform. - - The field type can be explicitly specified either by setting the `datatype` - keyword to a Python type name, or by setting `FTVL` to the appropriate EPICS - field type name. Otherwise the field type is taken from the initial value - if given, or defaults to ``'FLOAT'``. - - -The following function generates a specialised record. - -.. function:: Action(name, **fields) - - Creates a record (using :func:`boolOut`) which will always call the - `on_update` method when processed. Used for action records. The - `on_update` keyword should always be passed. - - -The following functions manage record names. The record device name must be -specified before creating records, then each record will be created with a -standard two part name of the form ``device:name`` where the ``device`` part is -specified by the functions below and the ``name`` part is specified in the -record creation function. - -.. function:: SetDeviceName(device_name) - - Sets up the prefix part of the record name, referred to here as the "device" - part. This function must be called before creating any records. - Note that - only this function need be used, the three other functions below are - entirely optional. - -.. function:: UnsetDevice() - - This can optionally be called after completing the creation of records to - prevent the accidential creation of records with the currently set device - name. - - -The following helper functions are useful when constructing links between -records. - -.. function:: - PP(record) - CP(record) - NP(record) - MS(record) - - When assigned to a link field in a record these functions add the - appropriate processing attributes to the link. These are not normally used. - - -The following attributes allow more direct access to record creation. - -.. attribute:: records - - This is the ``iocbuilder`` records object, and is populated with - functions named after each available record type. Records created with - these calls are created with soft device support and Python is not involved - in their processing. - - The following example shows a calc record being used to post-process a - standard Python IOC record:: - - from softioc import builder - builder.SetDeviceName('XX-XX-XX-01') - rec = aIn('VALUE') - calc = records.calc('CALC', CALC = 'A*B', A = rec, B = 42) - rec.FLNK = PP(calc) - - -Finally, the following function is used to load record definitions before -starting the IOC. - -.. function:: LoadDatabase() - - This must be called exactly once after creating all the records required by - the IOC and before calling :func:`~softioc.softioc.iocInit`. After this - function has been called none of the functions provided by - :mod:`softioc.builder` are usable. - - -Alarm Value Definitions ------------------------ - -.. module:: softioc.alarm - :synopsis: Constant definitions for EPICS severity and alarm values - -The following values can be passed to IN record :meth:`~softioc.device.ProcessDeviceSupportIn.set` and -:meth:`~softioc.device.ProcessDeviceSupportIn.set_alarm` methods. - -.. attribute:: - NO_ALARM = 0 - MINOR_ALARM = 1 - MAJOR_ALARM = 2 - INVALID_ALARM = 3 - - These are severity values. The default severity is ``NO_ALARM``. - -.. attribute:: - READ_ALARM - WRITE_ALARM - HIHI_ALARM - HIGH_ALARM - LOLO_ALARM - LOW_ALARM - STATE_ALARM - COS_ALARM - COMM_ALARM - TIMEOUT_ALARM - HW_LIMIT_ALARM - CALC_ALARM - SCAN_ALARM - LINK_ALARM - SOFT_ALARM - BAD_SUB_ALARM - UDF_ALARM - DISABLE_ALARM - SIMM_ALARM - READ_ACCESS_ALARM - WRITE_ACCESS_ALARM - - Alarm code definitions. Frankly these values aren't terribly useful, only - the severity is used for most notifications, but an alarm code needs to be - specified when specifying a non zero severity. - - -Automatic PV logging --------------------- - -.. module:: softioc.pvlog - :synopsis: Enables logging of CA puts to PVs - -Once this module has been imported all channel access writes to any PV published -by this IOC will be logged by writing a suitable message to stdout. There is -currently no control or customisation of this feature. diff --git a/docs/pythonsoftioc.rst b/docs/tutorials/creating-an-ioc.rst similarity index 83% rename from docs/pythonsoftioc.rst rename to docs/tutorials/creating-an-ioc.rst index 7fae50a3..04368431 100644 --- a/docs/pythonsoftioc.rst +++ b/docs/tutorials/creating-an-ioc.rst @@ -1,57 +1,15 @@ -.. _pythonsoftioc: - -Python Soft IOC +Creating an IOC =============== -The ``pythonIoc`` command is a tool for creating and running an EPICS IOC -entirely within Python. - -Building `pythonSoftIoc` -======================== - -To build `pythonSoftIoc` follow these steps: - -1. Edit the following files: - - `configure/RELEASE` - Set `EPICS_BASE` to point to your local installation of EPICS. - - `configure/CONFIG_SITE` - Set `PYTHON` to the executable name of your Python interpreter. - -2. Build `pythonIoc` without documentation by running:: - - make BUILD_DOCS= - - At this point you should be able to run `./pythonIoc` which will give you an - interative Python interpreter. - -3. Now ensure that you have numpy_, cothread_, and epicsdbbuilder_ installed. These can - be installed unversioned, or versioned (in which case - `pkg_resources.require` will need to be used), or just localling built and - added to your `PYTHONPATH`. - -4. Now check that `pythonIoc` works by running `example/runtest`. You may need - to edit `example/version.py` if you're using a versioned install of - `cothread` and `epicsdbbuilder`. - -5. Finally build the documentation by running:: - - make docs - - Again, if your installation of components is versioned you may need to edit - `docs/conf.py` as appropriate. - - -Using `pythonSoftIoc` -===================== +Using ``pythonSoftIoc`` +----------------------- -Probably the best way to use `pythonSoftIoc` is to start by copying fragments -of a simple example such as `CS-DI-IOC-02`. This consists of the following +Probably the best way to use ``pythonSoftIoc`` is to start by copying fragments +of a simple example such as ``CS-DI-IOC-02``. This consists of the following elements: -1. A startup shell script `start-ioc` which launches the soft IOC using a - production build of `pythonSoftIoc`. This script typically looks like +1. A startup shell script ``start-ioc`` which launches the soft IOC using a + production build of ``pythonSoftIoc``. This script typically looks like this:: #!/bin/sh @@ -62,7 +20,7 @@ elements: exec $PYIOC start_ioc.py "$@" 2. The startup Python script. This establishes the essential component - versions (apart from the `pythonSoftIoc` version), performs the appropriate + versions (apart from the ``pythonSoftIoc`` version), performs the appropriate initialisation and starts the IOC running. The following template is a useful starting point:: @@ -87,7 +45,7 @@ elements: # Finally leave the IOC running with an interactive shell. softioc.interactive_ioc(globals()) - Note that the use of `require` is specific to DLS, and you may have a + Note that the use of ``require`` is specific to DLS, and you may have a different way of managing your installations. .. _numpy: http://www.numpy.org/ diff --git a/docs/tutorials/installation.rst b/docs/tutorials/installation.rst new file mode 100644 index 00000000..b8d6ade2 --- /dev/null +++ b/docs/tutorials/installation.rst @@ -0,0 +1,46 @@ +Installation Tutorial +===================== + +.. note:: + + For installation inside DLS, please see the internal documentation on + ``dls-python3`` and ``pipenv``. Although these instructions will work + inside DLS, they are intended for external use. + + If you want to contribute to the library itself, please following + the `contributing` instructions. + +Check your version of python +---------------------------- + +You will need python 3.7 or later. You can check your version of python by +typing into a terminal:: + + python3 --version + +Create a virtual environment +---------------------------- + +It is recommended that you install into a “virtual environment” so this +installation will not interfere with any existing Python software:: + + python3 -m venv /path/to/venv + source /path/to/venv/bin/activate + + +Installing the library +---------------------- + +You can now use ``pip`` to install the library:: + + python3 -m pip install softioc + +If you require a feature that is not currently released you can also install +from github:: + + python3 -m pip install git+git://github.com/dls-controls/softioc.git + +The library should now be installed and the commandline interface on your path. +You can check the version that has been installed by typing:: + + softioc --version diff --git a/softioc/asyncio_dispatcher.py b/softioc/asyncio_dispatcher.py index 19904ab0..2ee7246f 100644 --- a/softioc/asyncio_dispatcher.py +++ b/softioc/asyncio_dispatcher.py @@ -4,8 +4,13 @@ class AsyncioDispatcher(threading.Thread): + """A dispatcher for `asyncio` based IOCs. Means that `on_update` callback + functions can be async. Will run an Event Loop in a thread when + created. + """ def __init__(self): super().__init__() + #: `asyncio` event loop that the callbacks will run under. self.loop = asyncio.new_event_loop() self.start() diff --git a/softioc/builder.py b/softioc/builder.py index cfd7efd0..6b087e9d 100644 --- a/softioc/builder.py +++ b/softioc/builder.py @@ -1,5 +1,3 @@ -'''Support for using the builder in cooperation with the python soft ioc.''' - import os import numpy from .softioc import dbLoadDatabase diff --git a/softioc/softioc.py b/softioc/softioc.py index cb2fe386..cf09bc8e 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -1,5 +1,3 @@ -'''Top level import script for soft IOC support.''' - import os import sys from ctypes import * @@ -15,6 +13,17 @@ def iocInit(dispatcher=None): + """This must be called exactly once after loading all EPICS database files. + After this point the EPICS IOC is running and serving PVs. + + Args: + dispatcher: A callable with signature ``dispatcher(func, *args)``. Will + be called in response to caput on a record. If not supplied use + `cothread` as a dispatcher. + + See Also: + `softioc.asyncio_dispatcher` is a dispatcher for `asyncio` applications + """ if dispatcher is None: # Fallback to cothread import cothread @@ -70,88 +79,154 @@ def call_f(*args): ExportTest('dba', (auto_encode,), (), '''\ dba(field) - Prints value of each field in dbAddr structure associated with field.''') +Prints value of each field in dbAddr structure associated with field.''') ExportTest('dbl', (auto_encode, auto_encode,), ('', ''), '''\ dbl(pattern='', fields='') - Prints the names of records in the database matching pattern. If - a (space separated) list of fields is also given then the values of - the fields are also printed.''') +Prints the names of records in the database matching pattern. If a (space +separated) list of fields is also given then the values of the fields are also +printed.''') ExportTest('dbnr', (c_int,), (0,), '''\ dbnr(all=0) - Print number of records of each record type.''') +Print number of records of each record type.''') ExportTest('dbgrep', (auto_encode,), (), '''\ dbgrep(pattern) - Lists all record names that match the pattern. * matches any number of - characters in a record name.''') +Lists all record names that match the pattern. * matches any number of +characters in a record name.''') ExportTest('dbgf', (auto_encode,), (), '''\ dbgf(field) - Prints field type and value.''') +Prints field type and value.''') ExportTest('dbpf', (auto_encode, auto_encode,), (), '''\ dbpf(field, value) - Writes the given value into the field.''') +Writes the given value into the field.''') ExportTest('dbpr', (auto_encode, c_int,), (0,), '''\ dbpr(record, interest=0) - Prints all the fields in record up to the indicated interest level: +Prints all the fields in record up to the indicated interest level: - 0 Application fields which change during record processing - 1 Application fields which are fixed during processing - 2 System developer fields of major interest - 3 System developer fields of minor interest - 4 All other fields.''') += ======================================================== +0 Application fields which change during record processing +1 Application fields which are fixed during processing +2 System developer fields of major interest +3 System developer fields of minor interest +4 All other fields += ========================================================''') ExportTest('dbtr', (auto_encode,), (), '''\ dbtr(record) - Tests processing of the specified record.''') +Tests processing of the specified record.''') + +ExportTest('dbtgf', (auto_encode,), (), '''\ +dbtgf(field_name) + +This performs a dbNameToAddr and then calls dbGetField with all possible request +types and options. It prints the results of each call. This routine is of most +interest to system developers for testing database access.''') + +ExportTest('dbtpf', (auto_encode, auto_encode,), (), '''\ +dbtpf(field_name, value) -ExportTest('dbtgf', (auto_encode,)) -ExportTest('dbtpf', (auto_encode, auto_encode,)) +This command performs a dbNameToAddr, then calls dbPutField, followed by dbgf +for each possible request type. This routine is of interest to system developers +for testing database access.''') + +ExportTest('dbtpn', (auto_encode, auto_encode,), (), '''\ +dbtpn(field, value) + +This command performs a dbProcessNotify request. If a non-null value argument +string is provided it issues a putProcessRequest to the named record; if no +value is provided it issues a processGetRequest. This routine is mainly of +interest to system developers for testing database access.''') ExportTest('dbior', (auto_encode, c_int,), ('', 0,), '''\ dbior(driver='', interest=0) - Prints driver reports for the selected driver (or all drivers if - driver is omitted) at the given interest level.''') +Prints driver reports for the selected driver (or all drivers if driver is +omitted) at the given interest level.''') + +ExportTest('dbhcr', (), (), '''\ +dbhcr() + +Prints hardware configuration report.''') + +ExportTest('gft', (auto_encode,), (), '''\ +gft(field) + +Get Field Test for old database access''') -ExportTest('dbhcr', (), (), '''Prints hardware configuration report.''') +ExportTest('pft', (auto_encode,), (), '''\ +pft(field, value) -ExportTest('gft', (auto_encode,)) -ExportTest('pft', (auto_encode,)) -ExportTest('dbtpn', (auto_encode, auto_encode,)) -ExportTest('tpn', (auto_encode, auto_encode,)) -ExportTest('dblsr', (auto_encode, c_int,)) -ExportTest('dbLockShowLocked', (c_int,)) +Put Field Test for old database access''') + +ExportTest('tpn', (auto_encode, auto_encode,), (), '''\ +tpn(field, value) + +Test Process Notify for old database access''') + +ExportTest('dblsr', (auto_encode, c_int,), (), '''\ +dblsr(recordname, level) + +This command generates a report showing the lock set to which each record +belongs. If recordname is 0, "", or "*" all records are shown, otherwise only +records in the same lock set as recordname are shown. + +level can have the following values: + += ======================================================= +0 Show lock set information only +1 Show each record in the lock set +2 Show each record and all database links in the lock set += =======================================================''') + +ExportTest('dbLockShowLocked', (c_int,), (), '''\ +dbLockShowLocked(level) + +This command generates a report showing all locked locksets, the records they +contain, the lockset state and the thread that currently owns the lockset. The +level argument is passed to epicsMutexShow to adjust the information reported +about each locked epicsMutex.''') ExportTest('scanppl', (c_double,), (0.0,), '''\ scanppl(rate=0.0) - Prints all records with the selected scan rate (or all if rate=0).''') +Prints all records with the selected scan rate (or all if rate=0).''') ExportTest('scanpel', (c_int,), (0,), '''\ scanpel(event=0) - Prints all records with selected event number (or all if event=0).''') +Prints all records with selected event number (or all if event=0).''') ExportTest('scanpiol', (), (), '''\ +scanpiol() + Prints all records in the I/O event scan lists.''') ExportTest('generalTimeReport', (c_int,), (0,), '''\ -Displays time providers and their status''', lib=imports.Com) +generalTimeReport(int level) + +This routine displays the time providers and their priority levels that have +registered with the General Time subsystem for both current and event times. At +level 1 it also shows the current time as obtained from each provider.''', + lib=imports.Com) ExportTest('eltc', (c_int,), (), '''\ -Turn EPICS logging on or off.''', lib=imports.Com) +eltc(noYes) + +TThis determines if error messages are displayed on the IOC console. 0 means no +and any other value means yes.''', + lib=imports.Com) # Hacked up exit object so that when soft IOC framework sends us an exit command @@ -192,13 +267,23 @@ def _add_records_from_file(dir, file, macros): def devIocStats(ioc_name): + '''This will load a template for the devIocStats library with the specified + IOC name. This should be called before `iocInit`''' macros = dict(IOCNAME=ioc_name, TODFORMAT='%m/%d/%Y %H:%M:%S') iocstats_dir = os.path.join(os.path.dirname(__file__), 'iocStatsDb') _add_records_from_file(iocstats_dir, 'ioc.template', macros) def interactive_ioc(context = {}, call_exit = True): - '''Fires up the interactive IOC prompt with the given context.''' + '''Fires up the interactive IOC prompt with the given context. + + Args: + context: A dictionary of values that will be made available to the + interactive Python shell together with a number of EPICS test + functions + call_exit: If `True`, the IOC will be terminated by calling epicsExit + which means that `interactive_ioc` will not return + ''' # Add all our commands to the given context. exports = dict((key, globals()[key]) for key in command_names) import code From 0633d74ba5d44c78a903cf1346586f4baa62f3f3 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Wed, 23 Jun 2021 09:29:17 +0100 Subject: [PATCH 19/19] Add use cases --- docs/explanations/use-cases.rst | 4 -- docs/explanations/why-use-pythonIoc.rst | 55 +++++++++++++++++++++++++ docs/index.rst | 2 +- docs/tutorials/creating-an-ioc.rst | 2 + 4 files changed, 58 insertions(+), 5 deletions(-) delete mode 100644 docs/explanations/use-cases.rst create mode 100644 docs/explanations/why-use-pythonIoc.rst diff --git a/docs/explanations/use-cases.rst b/docs/explanations/use-cases.rst deleted file mode 100644 index e66c3431..00000000 --- a/docs/explanations/use-cases.rst +++ /dev/null @@ -1,4 +0,0 @@ -Use Cases -========= - -Write about what it is good for diff --git a/docs/explanations/why-use-pythonIoc.rst b/docs/explanations/why-use-pythonIoc.rst new file mode 100644 index 00000000..6a958a07 --- /dev/null +++ b/docs/explanations/why-use-pythonIoc.rst @@ -0,0 +1,55 @@ +Why use pythonIOC? +================== + +EPICS IOCs are flexible and modular, why do we need to wrap it in Python? This +page attempts to answer that question and list a few good use-cases for it. + +Calculating PVs from other values +--------------------------------- + +Some use cases require PVs to be calculated from multiple sources. This is +possible in EPICS records with ``calc`` or ``aSub`` records, but pythonIoc +allows you to write this as: + +.. code-block:: + + import numpy as np + from cothread.catools import caget, camonitor + from softioc import builder + + # The PVs we want to average and their initial values + PVs = [f"DEVICE{i}:CURRENT" for i in range(100)] + values = np.array(caget(PVs)) + + # The PV we want to serve + avg = builder.aOut("AVERAGE:CURRENT", np.mean(values)) + + # Make a monitor on the PVs to keep the value up to date + def update_avg(value: float, index: int): + values[index] = value + avg.set(np.mean(values)) + + camonitor(PVs, update_avg) + +ADD THE CONCENTRATOR USE CASE HERE + +Dynamically created PVs +----------------------- + +Other use cases will do something like: + +.. code-block:: + + connect to device + make PVs + +ADD THE PANDA USE CASE HERE + +Existing Python Support +----------------------- + +It may be that you have specific device support written in Python that you wish +to expose as PVs. + +NEED A GOOD EXAMPLE HERE + diff --git a/docs/index.rst b/docs/index.rst index bf6780be..c17499c9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -59,7 +59,7 @@ About the documentation :name: explanations :maxdepth: 1 - explanations/use-cases + explanations/why-use-pythonIoc .. rst-class:: no-margin-after-ul diff --git a/docs/tutorials/creating-an-ioc.rst b/docs/tutorials/creating-an-ioc.rst index 04368431..4de37e3d 100644 --- a/docs/tutorials/creating-an-ioc.rst +++ b/docs/tutorials/creating-an-ioc.rst @@ -1,6 +1,8 @@ Creating an IOC =============== +THIS NEEDS UPDATING + Using ``pythonSoftIoc`` -----------------------