Merge remote-tracking branch 'origin/devel'

* origin/devel:
  Python init: don't install sig handlers. Fix #5
  Fix pyIocApp's Makefile
  testing cleanup
  cleanup and inithook
  minor
  update doc
  Base 3.14
  py3
  travis-ci update
  test field access and dset
  start PDB unittest
  rework to separate out python module
  separate softIocPy build
  Makefile updates from p4p

# Conflicts:
#	devsupApp/src/dbapi.c
#	devsupApp/src/pydevsup.h
This commit is contained in:
Michael Davidsaver
2019-03-12 17:18:05 -07:00
30 changed files with 1089 additions and 735 deletions

View File

@ -14,17 +14,20 @@ install:
- ./build-deps.sh
script:
- make PYTHON=`which python` -j2
- if [ "$TEST" = "YES" ]; then make PYTHON=`which python` -j2 nose; fi
matrix:
include:
- python: "2.7"
env: BRBASE=3.16 PROF=deb8
- python: "2.7"
env: BRBASE=3.16 PROF=deb8 CMPLR=clang
env: BRBASE=7.0 PROF=deb8 TEST=YES
- python: "3.4"
env: BRBASE=3.16 PROF=deb8
env: BRBASE=7.0 PROF=deb8 TEST=YES
- python: "3.5"
env: BRBASE=7.0 PROF=deb9 TEST=YES
- python: "3.6"
env: BRBASE=3.16 PROF=deb9
env: BRBASE=7.0 PROF=deb9 TEST=YES
- python: "2.7"
env: BRBASE=3.15 PROF=deb8
env: BRBASE=3.16 PROF=deb8 TEST=YES
- python: "2.7"
env: BRBASE=3.15 PROF=deb8 TEST=YES
- python: "2.7"
env: BRBASE=3.14 PROF=deb8

View File

@ -10,14 +10,24 @@ define DIR_template
endef
$(foreach dir, $(filter-out configure,$(DIRS)),$(eval $(call DIR_template,$(dir))))
pyIocApp_DEPEND_DIRS += devsupApp
iocBoot_DEPEND_DIRS += $(filter %App,$(DIRS))
include $(TOP)/configure/RULES_TOP
UNINSTALL_DIRS += $(wildcard $(INSTALL_LOCATION)/python*)
#useful targets includ: doc-html and doc-clean
doc-%:
PYTHONPATH=$$PWD/python$(PY_VER)/$(EPICS_HOST_ARCH) $(MAKE) -C documentation $*
# jump to a sub-directory where CONFIG_PY has been included
# can't include CONFIG_PY here as it may not exist yet
nose sphinx sh ipython: all
$(MAKE) -C devsupApp/src/O.$(EPICS_HOST_ARCH) $@ PYTHON=$(PYTHON)
doc: doc-html
sphinx-clean:
$(MAKE) -C documentation clean PYTHON=$(PYTHON)
sphinx-commit: sphinx
touch documentation/_build/html/.nojekyll
./commit-gh.sh documentation/_build/html
.PHONY: nose sphinx sphinx-commit sphinx-clean

View File

@ -2,24 +2,22 @@ ifneq ($(T_A),)
PYMODULE ?= YES
ifeq ($(PY_VER),)
$(error Must set PY_VER to select a python version)
ifeq ($(PYTHON),)
$(error Must set PYTHON to select a python version)
endif
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_PY$(PY_VER).$(EPICS_HOST_ARCH).Common
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_PY$(PY_VER).Common.$(T_A)
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_PY$(PY_VER).$(EPICS_HOST_ARCH).$(T_A)
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_PY.$(EPICS_HOST_ARCH).Common
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_PY.Common.$(T_A)
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_PY.$(EPICS_HOST_ARCH).$(T_A)
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_SITE_PY$(PY_VER).$(EPICS_HOST_ARCH).Common
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_SITE_PY$(PY_VER).Common.$(T_A)
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_SITE_PY$(PY_VER).$(EPICS_HOST_ARCH).$(T_A)
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_SITE_PY.$(EPICS_HOST_ARCH).Common
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_SITE_PY.Common.$(T_A)
-include $(dir $(lastword $(MAKEFILE_LIST)))/os/CONFIG_SITE_PY.$(EPICS_HOST_ARCH).$(T_A)
ifneq ($(PY_OK),YES)
$(error No usable configuration for python$(PY_VER))
$(error No usable configuration for $(PYTHON))
endif
PYTHON ?= python$(PY_VER)
SHRLIB_DEPLIB_DIRS += $(PY_LIBDIRS)
PROD_DEPLIB_DIRS += $(PY_LIBDIRS)
@ -27,18 +25,29 @@ INCLUDES += $(PY_INCDIRS:%=-I%)
ifeq ($(HAVE_NUMPY),YES)
TARGET_CPPFLAGS += -DHAVE_NUMPY
else
$(error numpy required)
endif
LIB_SYS_LIBS += python$(PY_LD_VER)
PROD_SYS_LIBS += python$(PY_LD_VER)
#LIB_SYS_LIBS += python$(PY_LD_VER)
#PROD_SYS_LIBS += python$(PY_LD_VER)
PY_INSTALL_DIR = $(INSTALL_LOCATION)/python$(PY_VER)/$(T_A)
PY_INSTALL_DIR = $(INSTALL_LOCATION)/python$(PY_LD_VER)/$(T_A)
ifneq ($(PYMODULE),NO)
# Python loadables have no prefix (eg 'pymod.so')
# and are installed alongsize .py files
LOADABLE_SHRLIB_PREFIX =
ifneq ($(PYMODULE),NO)
INSTALL_SHRLIB = $(PY_INSTALL_DIR)
ifeq ($(OS_CLASS),Darwin)
# need -undefined dynamic_lookup
LOADABLE_SHRLIB_LDFLAGS = -bundle -flat_namespace -undefined dynamic_lookup
LOADABLE_SHRLIB_SUFFIX = .so
endif
endif
endif

View File

@ -37,7 +37,9 @@ PY_VER=2.7
# Module will be build against this version of the
# Python interpreter
#PYTHON = python$(PY_VER)
PYTHON ?= python$(PY_VER)
USR_CPPFLAGS += -DUSE_TYPED_RSET
-include $(TOP)/configure/CONFIG_SITE.local
-include $(TOP)/../CONFIG_SITE.local

View File

@ -2,20 +2,19 @@ TOP=..
include $(TOP)/configure/CONFIG
ifeq ($(PY_VER),)
$(error Must set PY_VER to select a python version)
endif
PYTHON ?= python$(PY_VER)
TARGETS = $(CONFIG_TARGETS)
ifdef T_A
CONFIGS = CONFIG_PY RULES_PY os/CONFIG_PY$(PY_VER).Common.$(T_A)
CONFIGS = CONFIG_PY RULES_PY os/CONFIG_PY.Common.$(T_A)
endif
CONFIGS += $(subst ../,,$(wildcard $(CONFIG_INSTALLS)))
include $(TOP)/configure/RULES
os/CONFIG_PY$(PY_VER).Common.$(T_A): $(TOP)/makehelper.py
os/CONFIG_PY.Common.$(T_A): $(TOP)/makehelper.py
[ -d $(dir $@) ] || $(MKDIR) $(dir $@)
$(PYTHON) $< $@

View File

@ -1,80 +1,36 @@
TOP=../..
include $(TOP)/configure/CONFIG
PYMODULE = NO
include $(TOP)/configure/CONFIG_PY
#----------------------------------------
# ADD MACRO DEFINITIONS AFTER THIS LINE
#=============================
#=============================
# Build the IOC application
INSTALL_SHRLIB = $(PY_INSTALL_DIR)/devsup
LIBRARY = pyDevSup$(PY_LD_VER)
LOADABLE_LIBRARY_HOST += _dbapi
SHRLIB_VERSION = 0
TARGETS += $(COMMON_DIR)/pyDevSupCommon.dbd
DBDDEPENDS_FILES += pyDevSupCommon.dbd$(DEP)
DBD += pyDevSup.dbd
pyDevSupCommon_DBD += base.dbd
pyDevSup$(PY_LD_VER)_SYS_LIBS += python$(PY_LD_VER)
dbapi_CPPFLAGS += -DXEPICS_ARCH=\"$(T_A)\"
dbapi_CPPFLAGS += -DXPYDEV_BASE=\"$(abspath $(INSTALL_LOCATION))\"
dbapi_CPPFLAGS += -DXEPICS_BASE=\"$(EPICS_BASE)\"
dbapi_CPPFLAGS += -DPYDIR=\"python$(PY_VER)\"
setup_CPPFLAGS += -DXEPICS_ARCH=\"$(T_A)\"
setup_CPPFLAGS += -DXPYDEV_BASE=\"$(abspath $(INSTALL_LOCATION))\"
setup_CPPFLAGS += -DXEPICS_BASE=\"$(EPICS_BASE)\"
setup_CPPFLAGS += -DPYDIR=\"python$(PY_VER)\"
_dbapi_SRCS += dbapi.c
_dbapi_SRCS += dbrec.c
_dbapi_SRCS += dbfield.c
_dbapi_SRCS += dbdset.c
_dbapi_SRCS += utest.c
devsupMain_CPPFLAGS += -DXPYDEV_BASE=\"$(abspath $(INSTALL_LOCATION))\"
pyDevSup$(PY_LD_VER)_SRCS += setup.c
pyDevSup$(PY_LD_VER)_SRCS += dbbase.c
pyDevSup$(PY_LD_VER)_SRCS += dbrec.c
pyDevSup$(PY_LD_VER)_SRCS += dbfield.c
pyDevSup$(PY_LD_VER)_SRCS += dbdset.c
pyDevSup$(PY_LD_VER)_LIBS += $(EPICS_BASE_IOC_LIBS)
PROD_IOC = softIocPy$(PY_VER)
PRODNAME = $(addsuffix $(EXE),$(PROD))
# softIocPy.dbd will be created and installed
DBD += softIocPy.dbd
# softIocPy.dbd will be made up from these files:
softIocPy_DBD += base.dbd
softIocPy_DBD += pyDevSup.dbd
softIocPy_DBD += system.dbd
softIocPy$(PY_VER)_LIBS += pyDevSup$(PY_LD_VER)
# softIocPy_registerRecordDeviceDriver.cpp derives from softIocPy.dbd
softIocPy$(PY_VER)_SRCS += softIocPy_registerRecordDeviceDriver.cpp
# Build the main IOC entry point on workstation OSs.
softIocPy$(PY_VER)_SRCS_DEFAULT += devsupMain.cpp
ifneq ($(DEVIOCSTATS),)
softIocPy_DBD += devIocStats.dbd
softIocPy$(PY_VER)_LIBS += devIocStats
endif
ifneq ($(AUTOSAVE),)
softIocPy_DBD += asSupport.dbd
softIocPy$(PY_VER)_LIBS += autosave
endif
ifneq ($(CAPUTLOG),)
softIocPy_DBD += caPutLog.dbd
softIocPy$(PY_VER)_LIBS += caPutLog
endif
# Finally link to the EPICS Base libraries
softIocPy$(PY_VER)_LIBS += $(EPICS_BASE_IOC_LIBS)
_dbapi_SRCS += pyDevSupCommon_registerRecordDeviceDriver.cpp
_dbapi_LIBS += $(EPICS_BASE_IOC_LIBS)
PY += devsup/__init__.py
PY += devsup/_nullapi.py
PY += devsup/db.py
PY += devsup/dset.py
PY += devsup/hooks.py
@ -83,6 +39,10 @@ PY += devsup/util.py
PY += devsup/disect.py
PY += devsup/ptable.py
PY += devsup/test/__init__.py
PY += devsup/test/util.py
PY += devsup/test/test_db.py
#===========================
include $(TOP)/configure/RULES
@ -98,3 +58,20 @@ pyconfig:
@echo "Library path: $(PY_LIBDIRS)"
@echo "USR_CPPFLAGS: $(USR_CPPFLAGS)"
@echo "USR_LDFLAGS: $(USR_LDFLAGS)"
ifneq (,$(T_A))
nose:
PYTHONPATH="${PYTHONPATH}:$(abspath $(TOP))/python$(PY_LD_VER)/$(EPICS_HOST_ARCH)" $(PYTHON) -m nose -P devsup $(NOSEFLAGS)
# bounce back down to the sphinx generated Makefile
# aren't Makefiles fun...
sphinx:
PYTHONPATH="${PYTHONPATH}:$(abspath $(TOP))/python$(PY_LD_VER)/$(EPICS_HOST_ARCH)" $(MAKE) -C $(TOP)/documentation html
sh:
echo "export PYTHONPATH=\$${PYTHONPATH}:$(abspath $(TOP))/python$(PY_LD_VER)/$(EPICS_HOST_ARCH)" > $(OUTPUT)
ipython:
PYTHONPATH="${PYTHONPATH}:$(abspath $(TOP))/python$(PY_LD_VER)/$(EPICS_HOST_ARCH)" $(PYTHON) -c "import sys; sys.argv[0] = '$(PYTHON)'; from IPython.terminal.ipapp import launch_new_instance; launch_new_instance()"
endif

View File

@ -1,6 +1,3 @@
/* Global interpreter setup
*/
/* python has its own ideas about which version to support */
#undef _POSIX_C_SOURCE
#undef _XOPEN_SOURCE
@ -21,11 +18,15 @@
#include <epicsThread.h>
#include <epicsExit.h>
#include <alarm.h>
#include <iocsh.h>
#include <iocInit.h>
#include "pydevsup.h"
initHookState pyInitLastState;
extern int pyDevSupCommon_registerRecordDeviceDriver(DBBASE *pbase);
typedef struct {
const initHookState state;
const char * const name;
@ -104,6 +105,20 @@ void pyfile(const char* file)
PyGILState_Release(state);
}
static const iocshArg argCode = {"python code", iocshArgString};
static const iocshArg argFile = {"file", iocshArgString};
static const iocshArg* const codeArgs[] = {&argCode};
static const iocshArg* const fileArgs[] = {&argFile};
static const iocshFuncDef codeDef = {"py", 1, codeArgs};
static const iocshFuncDef fileDef = {"pyfile", 1, fileArgs};
static void codeRun(const iocshArgBuf *args){py(args[0].sval);}
static void fileRun(const iocshArgBuf *args){pyfile(args[0].sval);}
initHookState pyInitLastState = (initHookState)-1;
static void pyhook(initHookState state)
@ -139,28 +154,160 @@ fail:
PyGILState_Release(gilstate);
}
static
PyObject* py_announce(PyObject *unused, PyObject *args, PyObject *kws)
{
static char* names[] = {"state", NULL};
int state;
if(!PyArg_ParseTupleAndKeywords(args, kws, "i", names, &state))
return NULL;
Py_BEGIN_ALLOW_THREADS {
initHookAnnounce((initHookState)state);
} Py_END_ALLOW_THREADS
Py_RETURN_NONE;
}
static
PyObject *py_iocsh(PyObject *unused, PyObject *args, PyObject *kws)
{
int ret;
static char* names[] = {"script", "cmd", NULL};
char *script=NULL, *cmd=NULL;
if(!PyArg_ParseTupleAndKeywords(args, kws, "|ss", names, &script, &cmd))
return NULL;
if(!(!script ^ !cmd)) {
PyErr_SetString(PyExc_ValueError, "iocsh requires a script file name or command string");
return NULL;
}
Py_BEGIN_ALLOW_THREADS {
if(script)
ret = iocsh(script);
else
ret = iocshCmd(cmd);
} Py_END_ALLOW_THREADS
return PyInt_FromLong(ret);
}
static
PyObject *py_dbReadDatabase(PyObject *unused, PyObject *args, PyObject *kws)
{
long status;
static char* names[] = {"name", "fp", "path", "sub", NULL};
char *fname=NULL, *path=NULL, *sub=NULL;
int fd=-1;
if(!PyArg_ParseTupleAndKeywords(args, kws, "|siss", names, &fname, &fd, &path, &sub))
return NULL;
if(!((!fname) ^ (fd<0))) {
PyErr_SetString(PyExc_ValueError, "dbReadDatabase requires a file name or descriptor");
return NULL;
}
Py_BEGIN_ALLOW_THREADS {
if(fname) {
status = dbReadDatabase(&pdbbase, fname, path, sub);
} else {
FILE *ff = fdopen(fd, "r");
status = dbReadDatabaseFP(&pdbbase, ff, path, sub);
// dbReadDatabaseFP() has called fclose()
}
} Py_END_ALLOW_THREADS
if(status) {
char buf[30];
errSymLookup(status, buf, sizeof(buf));
PyErr_SetString(PyExc_RuntimeError, buf);
return NULL;
}
Py_RETURN_NONE;
}
static
PyObject *py_iocInit(PyObject *unused, PyObject *args, PyObject *kws)
{
static char* names[] = {"isolate", NULL};
PyObject *pyisolate = Py_True;
int isolate, ret;
if(!PyArg_ParseTupleAndKeywords(args, kws, "|O", names, &pyisolate))
return NULL;
isolate = PyObject_IsTrue(pyisolate);
Py_BEGIN_ALLOW_THREADS {
ret = isolate ? iocBuildIsolated() : iocBuild();
if(!ret)
ret = iocRun();
} Py_END_ALLOW_THREADS
if(ret)
return PyErr_Format(PyExc_RuntimeError, "Error %d", ret);
Py_RETURN_NONE;
}
static
PyObject *py_pyDevSupCommon(PyObject *unused)
{
Py_BEGIN_ALLOW_THREADS {
pyDevSupCommon_registerRecordDeviceDriver(pdbbase);
} Py_END_ALLOW_THREADS
Py_RETURN_NONE;
}
static struct PyMethodDef dbapimethod[] = {
{"initHookAnnounce", (PyCFunction)py_announce, METH_VARARGS|METH_KEYWORDS,
"initHookAnnounce(state)\n"},
{"iocsh", (PyCFunction)py_iocsh, METH_VARARGS|METH_KEYWORDS,
"Execute IOC shell script or command"},
{"dbReadDatabase", (PyCFunction)py_dbReadDatabase, METH_VARARGS|METH_KEYWORDS,
"Load EPICS database file"},
{"iocInit", (PyCFunction)py_iocInit, METH_NOARGS,
"Initialize IOC"},
{"_dbd_setup", (PyCFunction)pyDBD_setup, METH_NOARGS, ""},
{"_dbd_rrd_base", (PyCFunction)py_pyDevSupCommon, METH_NOARGS, ""},
{"_dbd_cleanup", (PyCFunction)pyDBD_cleanup, METH_NOARGS, ""},
{NULL}
};
#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef dbapimodule = {
PyModuleDef_HEAD_INIT,
"_dbapi",
"devsup._dbapi",
NULL,
-1,
NULL
&dbapimethod
};
#endif
/* initialize "magic" builtin module */
#if PY_MAJOR_VERSION >= 3
PyMODINIT_FUNC PyInit__dbapi(void)
#else
PyMODINIT_FUNC init_dbapi(void)
#endif
{
PyObject *mod = NULL, *hookdict;
PyObject *mod = NULL, *hookdict, *vertup;
pystate *st;
pyDevReasonID = epicsThreadPrivateCreate();
iocshRegister(&codeDef, &codeRun);
iocshRegister(&fileDef, &fileRun);
initHookRegister(&pyhook);
import_array();
#if PY_MAJOR_VERSION >= 3
mod = PyModule_Create(&dbapimodule);
#else
mod = Py_InitModule("_dbapi", NULL);
mod = Py_InitModule("devsup._dbapi", dbapimethod);
#endif
if(!mod)
goto fail;
@ -180,43 +327,6 @@ PyMODINIT_FUNC init_dbapi(void)
}
}
if(pyField_prepare(mod))
goto fail;
if(pyRecord_prepare(mod))
goto fail;
MODINIT_RET(mod);
fail:
fprintf(stderr, "Failed to initialize builtin _dbapi module!\n");
Py_XDECREF(mod);
MODINIT_RET(NULL);
}
#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef constantsmodule = {
PyModuleDef_HEAD_INIT,
"_dbconstants",
NULL,
-1,
NULL
};
#endif
/* initialize "magic" builtin module */
PyMODINIT_FUNC init_dbconstants(void)
{
PyObject *mod = NULL, *vertup;
#if PY_MAJOR_VERSION >= 3
mod = PyModule_Create(&constantsmodule);
#else
mod = Py_InitModule("_dbconstants", NULL);
#endif
if(!mod)
MODINIT_RET(NULL);
PyModule_AddIntMacro(mod, NO_ALARM);
PyModule_AddIntMacro(mod, MINOR_ALARM);
PyModule_AddIntMacro(mod, MAJOR_ALARM);
@ -279,150 +389,17 @@ PyMODINIT_FUNC init_dbconstants(void)
if(vertup)
PyModule_AddObject(mod, "pydevver", vertup);
if(pyField_prepare(mod))
goto fail;
if(pyRecord_prepare(mod))
goto fail;
if(pyUTest_prepare(mod))
goto fail;
MODINIT_RET(mod);
}
static void cleanupPy(void *junk)
{
PyThreadState *state = PyGILState_GetThisThreadState();
PyEval_RestoreThread(state);
/* special "fake" hook for shutdown */
pyhook((initHookState)9999);
pyDBD_cleanup();
pyField_cleanup();
Py_Finalize();
epicsThreadPrivateDelete(pyDevReasonID);
}
/* Initialize the interpreter environment
*/
static void setupPyInit(void)
{
PyImport_AppendInittab("_dbapi", init_dbapi);
PyImport_AppendInittab("_dbconstants", init_dbconstants);
PyImport_AppendInittab("_dbbase", init_dbbase);
Py_Initialize();
PyEval_InitThreads();
(void)PyEval_SaveThread();
epicsAtExit(&cleanupPy, NULL);
}
static void extendPath(PyObject *list,
const char *base,
const char *archdir)
{
PyObject *mod, *ret;
mod = PyImport_ImportModule("os.path");
if(!mod)
return;
ret = PyObject_CallMethod(mod, "join", "sss", base, PYDIR, archdir);
if(ret && !PySequence_Contains(list, ret)) {
PyList_Insert(list, 0, ret);
}
Py_XDECREF(ret);
Py_DECREF(mod);
if(PyErr_Occurred()) {
PyErr_Print();
PyErr_Clear();
}
}
static void insertDefaultPath(PyObject *list)
{
const char *basedir, *pydevdir, *top, *arch;
basedir = getenv("EPICS_BASE");
if(!basedir)
basedir = XEPICS_BASE;
pydevdir = getenv("PYDEV_BASE");
if(!pydevdir)
pydevdir = XPYDEV_BASE;
top = getenv("TOP");
arch = getenv("ARCH");
if(!arch)
arch = XEPICS_ARCH;
assert(PyList_Check(list));
assert(PySequence_Check(list));
extendPath(list, basedir, arch);
extendPath(list, pydevdir, arch);
if(top)
extendPath(list, top, arch);
}
static void setupPyPath(void)
{
PyObject *mod, *path = NULL;
mod = PyImport_ImportModule("sys");
if(mod)
path = PyObject_GetAttrString(mod, "path");
fail:
fprintf(stderr, "Failed to initialize builtin _dbapi module!\n");
Py_XDECREF(mod);
if(path) {
PyObject *cur;
char cwd[PATH_MAX];
insertDefaultPath(path);
/* prepend current directory */
if(getcwd(cwd, sizeof(cwd)-1)) {
cwd[sizeof(cwd)-1] = '\0';
cur = PyString_FromString(cwd);
if(cur)
PyList_Insert(path, 0, cur);
Py_XDECREF(cur);
}
}
Py_XDECREF(path);
MODINIT_RET(NULL);
}
#include <iocsh.h>
static const iocshArg argCode = {"python code", iocshArgString};
static const iocshArg argFile = {"file", iocshArgString};
static const iocshArg* const codeArgs[] = {&argCode};
static const iocshArg* const fileArgs[] = {&argFile};
static const iocshFuncDef codeDef = {"py", 1, codeArgs};
static const iocshFuncDef fileDef = {"pyfile", 1, fileArgs};
static void codeRun(const iocshArgBuf *args){py(args[0].sval);}
static void fileRun(const iocshArgBuf *args){pyfile(args[0].sval);}
static void pySetupReg(void)
{
PyGILState_STATE state;
pyDevReasonID = epicsThreadPrivateCreate();
setupPyInit();
iocshRegister(&codeDef, &codeRun);
iocshRegister(&fileDef, &fileRun);
initHookRegister(&pyhook);
state = PyGILState_Ensure();
init_dbapi();
setupPyPath();
if(PyErr_Occurred()) {
PyErr_Print();
PyErr_Clear();
}
PyGILState_Release(state);
}
#include <epicsExport.h>
epicsExportRegistrar(pySetupReg);

View File

@ -1,136 +0,0 @@
/* python has its own ideas about which version to support */
#undef _POSIX_C_SOURCE
#undef _XOPEN_SOURCE
#include <Python.h>
#include <epicsVersion.h>
#include <dbCommon.h>
#include <dbStaticLib.h>
#include <dbAccess.h>
#include <initHooks.h>
#include <iocsh.h>
#include <iocInit.h>
#include "pydevsup.h"
static
PyObject *py_iocsh(PyObject *unused, PyObject *args, PyObject *kws)
{
int ret;
static char* names[] = {"script", "cmd", NULL};
char *script=NULL, *cmd=NULL;
if(!PyArg_ParseTupleAndKeywords(args, kws, "|ss", names, &script, &cmd))
return NULL;
if(!(!script ^ !cmd)) {
PyErr_SetString(PyExc_ValueError, "iocsh requires a script file name or command string");
return NULL;
}
Py_BEGIN_ALLOW_THREADS {
if(script)
ret = iocsh(script);
else
ret = iocshCmd(cmd);
} Py_END_ALLOW_THREADS
return PyInt_FromLong(ret);
}
static
PyObject *py_dbReadDatabase(PyObject *unused, PyObject *args, PyObject *kws)
{
long status;
static char* names[] = {"name", "fp", "path", "sub", NULL};
char *fname=NULL, *path=NULL, *sub=NULL;
int fd=-1;
if(!PyArg_ParseTupleAndKeywords(args, kws, "|siss", names, &fname, &fd, &path, &sub))
return NULL;
if(!((!fname) ^ (fd<0))) {
PyErr_SetString(PyExc_ValueError, "dbReadDatabase requires a file name or descriptor");
return NULL;
}
Py_BEGIN_ALLOW_THREADS {
if(fname)
status = dbReadDatabase(&pdbbase, fname, path, sub);
else {
FILE *ff = fdopen(fd, "r");
status = dbReadDatabaseFP(&pdbbase, ff, path, sub);
fclose(ff);
}
} Py_END_ALLOW_THREADS
if(status) {
char buf[30];
errSymLookup(status, buf, sizeof(buf));
PyErr_SetString(PyExc_RuntimeError, buf);
return NULL;
}
Py_RETURN_NONE;
}
static
PyObject *py_iocInit(PyObject *unused)
{
Py_BEGIN_ALLOW_THREADS {
iocInit();
} Py_END_ALLOW_THREADS
Py_RETURN_NONE;
}
static struct PyMethodDef dbbasemethods[] = {
{"iocsh", (PyCFunction)py_iocsh, METH_VARARGS|METH_KEYWORDS,
"Execute IOC shell script or command"},
{"dbReadDatabase", (PyCFunction)py_dbReadDatabase, METH_VARARGS|METH_KEYWORDS,
"Load EPICS database file"},
{"iocInit", (PyCFunction)py_iocInit, METH_NOARGS,
"Initialize IOC"},
{NULL}
};
#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef dbbasemodule = {
PyModuleDef_HEAD_INIT,
"_dbbase",
NULL,
-1,
&dbbasemethods
};
#endif
/* initialize "magic" builtin module */
PyMODINIT_FUNC init_dbbase(void)
{
PyObject *mod = NULL, *obj = NULL;
#if PY_MAJOR_VERSION >= 3
mod = PyModule_Create(&dbbasemodule);
#else
mod = Py_InitModule("_dbbase", dbbasemethods);
#endif
if(!mod)
goto fail;
#if PY_MAJOR_VERSION >= 3 || (PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION>=7)
obj = PyCapsule_New(pdbbase, "pdbbase", NULL);
#else
obj = PyCObject_FromVoidPtrAndDesc(pdbbase, "pdbbase", NULL);
#endif
if(!obj)
goto fail;
PyModule_AddObject(mod, "pdbbase", obj);
MODINIT_RET(mod);
fail:
Py_XDECREF(obj);
Py_XDECREF(mod);
fprintf(stderr, "Failed to initialize builtin _dbbase module!\n");
MODINIT_RET(NULL);
}

View File

@ -19,6 +19,8 @@
#include <dbScan.h>
#include <cantProceed.h>
#include <registryFunction.h>
#include <iocshRegisterCommon.h>
#include <registryCommon.h>
#include <aSubRecord.h>
#include "pydevsup.h"
@ -471,8 +473,28 @@ int canIOScanRecord(dbCommon *prec)
return !!priv->scanobj;
}
static
const dset* pydsets[] = {
&pydevsupComSpec.com,
&pydevsupComIn.com,
&pydevsupComOut.com,
};
static const char* pydsetnames[] = {
"pydevsupComSpec",
"pydevsupComIn",
"pydevsupComOut",
};
PyObject* pyDBD_setup(PyObject *unused)
{
registerDevices(pdbbase, NELEMENTS(pydsets), pydsetnames, pydsets);
registryFunctionAdd("python_asub", (REGISTRYFUNCTION)&python_asub);
Py_RETURN_NONE;
}
/* Called with GIL locked */
void pyDBD_cleanup(void)
PyObject* pyDBD_cleanup(PyObject *unused)
{
ELLNODE *cur;
inshutdown = 1;
@ -497,12 +519,5 @@ void pyDBD_cleanup(void)
free(priv);
}
Py_RETURN_NONE;
}
#include <epicsExport.h>
epicsExportAddress(dset, pydevsupComSpec);
epicsExportAddress(dset, pydevsupComIn);
epicsExportAddress(dset, pydevsupComOut);
epicsRegisterFunction(python_asub);

View File

@ -192,10 +192,6 @@ static PyObject* pyField_getval(pyField *self)
return PyErr_Format(PyExc_ValueError, "Error fetching array info for %s.%s",
self->addr.precord->name,
self->addr.pfldDes->name);
else if(noe<1) {
PyErr_SetString(PyExc_IndexError, "zero length array");
return NULL;
}
rawfield = self->addr.pfield;
/* get_array_info can modify pfield in >3.15.0.1 */
@ -388,8 +384,8 @@ static PyObject *pyField_setlen(pyField *self, PyObject *args)
return NULL;
}
if(len<1 || len > self->addr.no_elements) {
PyErr_Format(PyExc_ValueError, "Requested length %ld out of range [1,%lu)",
if(len > self->addr.no_elements) {
PyErr_Format(PyExc_ValueError, "Requested length %ld out of range [0,%lu)",
(long)len, (unsigned long)self->addr.no_elements);
return NULL;
}
@ -449,25 +445,28 @@ static PyObject *pyField_len(pyField *self)
static PyMethodDef pyField_methods[] = {
{"name", (PyCFunction)pyField_name, METH_NOARGS,
"Return Names (\"record\",\"field\")"},
"name() -> (recname, fldname)\n"},
{"fieldinfo", (PyCFunction)pyField_fldinfo, METH_NOARGS,
"Field type info\nReturn (type, size, #elements"},
"fieldinfo() -> (dbf, elem_size, elem_count"},
{"getval", (PyCFunction)pyField_getval, METH_NOARGS,
"Returns scalar version of field value"},
"getval() -> object\n"},
{"putval", (PyCFunction)pyField_putval, METH_VARARGS,
"Sets field value from a scalar"},
"putval(object)\n"},
{"getarray", (PyCFunction)pyField_getarray, METH_NOARGS,
"getarray() -> numpy.ndarray\n"
"Return a numpy ndarray refering to this field for in-place operations."},
{"getarraylen", (PyCFunction)pyField_getlen, METH_NOARGS,
"getarraylen() -> int\n"
"Return current number of valid elements for array fields."},
{"putarraylen", (PyCFunction)pyField_setlen, METH_VARARGS,
"putarraylen(int)\n"
"Set number of valid elements for array fields."},
{"getTime", (PyCFunction)pyField_getTime, METH_NOARGS,
"Return link target timestamp as a tuple (sec, nsec)."},
"getTime() -> (sec, nsec)."},
{"getAlarm", (PyCFunction)pyField_getAlarm, METH_NOARGS,
"Return link target alarm condtions as a tuple (severity, status)."},
"getAlarm() -> (severity, status)."},
{"__len__", (PyCFunction)pyField_len, METH_NOARGS,
"Maximum number of elements storable in this field"},
"Maximum number of elements storable in this field."},
{NULL, NULL, 0, NULL}
};
@ -569,12 +568,3 @@ int pyField_prepare(PyObject *module)
return 0;
}
void pyField_cleanup(void)
{
size_t i;
for(i=0; i<=DBF_MENU; i++) {
Py_XDECREF(dbf2np[i]);
dbf2np[i] = NULL;
}
}

View File

@ -293,23 +293,32 @@ static PyObject *pyRecord_exit(pyRecord *self, PyObject *args)
static PyMethodDef pyRecord_methods[] = {
{"name", (PyCFunction)pyRecord_name, METH_NOARGS,
"Return record name string"},
"name() -> str\n\n"
"Record name. ::\n"
"\n"
" R = getRecord(\"my:record:name\")\n"
" assert R.name()==\"my:record:name\"\n"},
{"rtype", (PyCFunction)pyRecord_rtype, METH_NOARGS,
"rtype() -> str\n"
"Return record type name string"},
{"isPyRecord", (PyCFunction)pyRecord_ispyrec, METH_NOARGS,
"isPyRecord() -> bool\n"
"Is this record using Python Device."},
{"info", (PyCFunction)pyRecord_info, METH_VARARGS,
"Lookup info name\ninfo(name, def=None)"},
"info(key [,default]) -> str\n"
"Lookup info by name\n"
":rtype: str\n"
":throws: KeyError\n"},
{"infos", (PyCFunction)pyRecord_infos, METH_NOARGS,
"infos() -> {'name':'value'}\n"
"Return a dictionary of all infos for this record."},
{"setSevr", (PyCFunction)pyRecord_setSevr, METH_VARARGS|METH_KEYWORDS,
"setSevr(sevr=INVALID_ALARM, stat=COMM_ALARM)\n"
"Set alarm new alarm severity/status. Record must be locked!"},
{"setTime", (PyCFunction)pyRecord_setTime, METH_VARARGS,
"Set record timestamp if TSE==-2. Record must be locked!"},
{"scan", (PyCFunction)pyRecord_scan, METH_VARARGS|METH_KEYWORDS,
"scan(sync=False)\nScan this record. If sync is False then"
"a scan request is queued. If sync is True then the record"
"is scannined immidately on the current thread."},
"scan(sync=False, reason=None, force=0)\n"},
{"asyncStart", (PyCFunction)pyRecord_asyncStart, METH_NOARGS,
"Begin an asynchronous action. Record must be locked!"},
{"asyncFinish", (PyCFunction)pyRecord_asyncFinish, METH_VARARGS|METH_KEYWORDS,

View File

@ -1,28 +1,86 @@
try:
import _dbapi
HAVE_DBAPI = True
except ImportError:
import devsup._nullapi as _dbapi
HAVE_DBAPI = False
import os
import atexit
import tempfile
try:
from _dbconstants import *
except ImportError:
EPICS_VERSION_STRING = "EPICS 0.0.0.0-0"
EPICS_DEV_SNAPSHOT = ""
EPICS_SITE_VERSION = "0"
EPICS_VERSION = 0
EPICS_REVISION = 0
EPICS_MODIFICATION = 0
EPICS_PATCH_LEVEL = 0
from . import _dbapi
XEPICS_ARCH = "nullos-nullarch"
XPYDEV_BASE = "invaliddir"
XEPICS_BASE = "invaliddir"
epicsver = (0,0,0,0,"0","")
pydevver = (0,0)
INVALID_ALARM = UDF_ALARM = 0
from ._dbapi import (EPICS_VERSION_STRING,
EPICS_DEV_SNAPSHOT,
EPICS_SITE_VERSION,
EPICS_VERSION,
EPICS_REVISION,
EPICS_MODIFICATION,
EPICS_PATCH_LEVEL,
XEPICS_ARCH,
XPYDEV_BASE,
XEPICS_BASE,
epicsver,
pydevver,
NO_ALARM,
MINOR_ALARM,
MAJOR_ALARM,
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,
INVALID_ALARM,
)
__all__ = []
def _init(iocMain=False):
if not iocMain:
# we haven't read/register base.dbd
_dbapi.dbReadDatabase(os.path.join(XEPICS_BASE, "dbd", "base.dbd"),
path=os.path.join(XEPICS_BASE, "dbd"))
_dbapi._dbd_rrd_base()
with tempfile.NamedTemporaryFile() as F:
F.write("""
device(longin, INST_IO, pydevsupComIn, "Python Device")
device(longout, INST_IO, pydevsupComOut, "Python Device")
device(ai, INST_IO, pydevsupComIn, "Python Device")
device(ao, INST_IO, pydevsupComOut, "Python Device")
device(stringin, INST_IO, pydevsupComIn, "Python Device")
device(stringout, INST_IO, pydevsupComOut, "Python Device")
device(bi, INST_IO, pydevsupComIn, "Python Device")
device(bo, INST_IO, pydevsupComOut, "Python Device")
device(mbbi, INST_IO, pydevsupComIn, "Python Device")
device(mbbo, INST_IO, pydevsupComOut, "Python Device")
device(mbbiDirect, INST_IO, pydevsupComIn, "Python Device")
device(mbboDirect, INST_IO, pydevsupComOut, "Python Device")
device(waveform, INST_IO, pydevsupComIn, "Python Device")
device(aai, INST_IO, pydevsupComIn, "Python Device")
device(aao, INST_IO, pydevsupComOut, "Python Device")
""".encode('ascii'))
F.flush()
_dbapi.dbReadDatabase(F.name)
_dbapi._dbd_setup()
def _fini(iocMain=False):
if iocMain:
_dbapi.initHookAnnounce(9999) # our magic/fake AtExit hook
_dbapi._dbd_cleanup()

View File

@ -1,205 +0,0 @@
class _Record(object):
"""Handle for record operations
r = _Record("rec:name")
"""
def __init__(self, rec):
pass
def name(self):
"""Record name string.
>>> R = getRecord("my:record:name")
>>> R.name()
"my:record:name"
"""
def rtype(self):
"""Record type name string.
>>> R = getRecord("my:record:name")
>>> R.type()
"longin"
"""
def isPyRecord(self):
"""Is this record using Python device support.
:rtype: bool
"""
def info(self, key):
"""info(key [,default])
:rtype: str
:throws: KeyError
Lookup record info tag. If no default
is provided then an exception is raised
if the info key does not exist.
"""
def infos(self):
"""Return a dictionary of all info tags
for this record
"""
def setSevr(self, sevr=3, stat=15):
"""setSevr(sevr=INVALID_ALARM, stat=COMM_ALARM)
Signal a new alarm condition. The effect of this
call depends on the current alarm condition.
See :c:func:`recGblSetSevr` in EPICS Base.
"""
def scan(self, sync=False, reason=None, force=0):
"""Scan this record.
:param sync: scan in current thread (``True``), or queue to a worker (``False``).
:param reason: Reason object passed to :meth:`process <DeviceSupport.process>` (sync=True only)
:param force: Record processing condtion (0=Passive, 1=Force, 2=I/O Intr)
:throws: ``RuntimeError`` when ``sync=True``, but ``force`` prevents scanning.
If ``sync`` is False then a scan request is queued to run in another thread..
If ``sync`` is True then the record is scanned immediately on the current thread.
For ``reason`` argument must be used in conjunction with ``sync=True``
on records with Python device support. This provides a means
of providing extra contextual information to the record's
:meth:`process <DeviceSupport.process>` method.
``force`` is used to decide if the record will actually be processed,
``force=0`` will only process records with SCAN=Passive.
``force=1`` will process any record if at all possible.
``force=2`` will only process records with Python device support and
SCAN=I/O Intr.
.. important::
It is **never** safe to use ``sync=True`` while holding record locks,
including from within a *process* method.
"""
def asyncStart(self):
"""Start asynchronous processing
This method may be called from a device support
:meth:`process <DeviceSupport.process>` method
to indicate that processing will continue
later.
.. important::
This method is **only** safe to call within a *process* method.
"""
def asyncFinish(self, reason=None):
"""Indicate that asynchronous processing can complete
Similar to :meth:`scan`. Used to conclude asynchronous
process started with :meth:`asyncStart`.
Processing is completed on the current thread.
.. important::
This method should **never** be called within
a :meth:`process <DeviceSupport.process>` method,
or any other context where a Record lock is held.
Doing so will result in a deadlock.
Typically a *reason* will be passed to *process* as a way
of indicating that this is the completion of an async action. ::
AsyncDone = object()
class MySup(object):
def process(record, reason):
if reason is AsyncDone:
record.VAL = ... # store result
else:
threading.Timer(1.0, record.asyncFinish, kwargs={'reason':AsyncDone})
record.asyncStart()
"""
class _Field(object):
"""Handle for field operations
f = Field("rec:name.HOPR")
Field objects implement the buffer protocol.
"""
def __init__(self, fld):
pass
def name(self):
"""Fetch the record and field names.
>>> FLD = getRecord("rec").field("FLD")
>>> FLD.name()
("rec", "FLD")
"""
def fieldinfo(self):
"""(type, size, #elements) = fieldinfo()
Type is DBF type code
size is number of bytes to start a single element
#elements is the maximum number of elements the field can hold
"""
def getval(self):
"""Fetch the current field value as a scalar or numpy.ndarray.
:rtype: int, float, str, or ndarray
Returned type depends of field DBF type.
An ``int`` is returned for CHAR, SHORT, LONG, and ENUM.
A ``float`` is returned for FLOAT and DOUBLE.
A ``str`` is returned for STRING.
A ``numpy.ndarray`` is returned for array fields.
This array is read-only and has the size of the present valid values.
.. important::
It is only safe to read this ndarray while the record
lock is held (ie within :meth:`process <DeviceSupport.process>`).
"""
def putval(self, val):
"""Update the field value
Must be an Int, Float, str, or numpy.ndarray.
Strings will be truncated to 39 characters.
Arrays must have a size less than or equal to the max element count.
Arrays are converted as necessary to the field's native type.
"""
def getarray(self):
"""Return a numpy ndarray refering to this field for in-place operations.
The dtype of the ndarray will correspond to the field's DBF type.
Its size will be the **maximum** number of elements.
.. important::
It is only safe to read or write to this ndarray while the record
lock is held (ie within :meth:`process <DeviceSupport.process>`).
"""
def getarraylen(self):
"""Return the number of active elements for the field.
>>> F = Field(...)
>>> assert len(F)>=F.getarraylen()
"""
def putarraylen(self, len):
"""Set the number of active elements in field's array.
Requires that the underlying field be an array.
Must be greater than one and less than or equal to the maximum length of the field.
"""
def getAlarm(self):
"""Returns a tuple (severity, status) with the condition of the linked field.
Only works for fields of type DBF_INLINK.
"""
def __len__(self):
"""Returns the maximum number of elements which may be stored in the field.
This is always 1 for scalar fields.
"""
_hooks = {}

View File

@ -3,10 +3,7 @@ import threading, sys, traceback, time
from devsup.util import Worker, importmod
try:
import _dbapi
except ImportError:
import _nullapi as _dbapi
from . import _dbapi
_rec_cache = {}
_no_such_field = object()
@ -263,6 +260,76 @@ class Record(_dbapi._Record):
super(Record, self).setTime(sec, nsec)
def scan(self, *args, **kws):
"""scan(sync=False, reason=None, force=0)
Scan this record.
:param sync: scan in current thread (``True``), or queue to a worker (``False``).
:param reason: Reason object passed to :meth:`process <DeviceSupport.process>` (sync=True only)
:param force: Record processing condtion (0=Passive, 1=Force, 2=I/O Intr)
:throws: ``RuntimeError`` when ``sync=True``, but ``force`` prevents scanning.
If ``sync`` is False then a scan request is queued to run in another thread..
If ``sync`` is True then the record is scanned immediately on the current thread.
For ``reason`` argument must be used in conjunction with ``sync=True``
on records with Python device support. This provides a means
of providing extra contextual information to the record's
:meth:`process <DeviceSupport.process>` method.
``force`` is used to decide if the record will actually be processed,
``force=0`` will only process records with SCAN=Passive.
``force=1`` will process any record if at all possible.
``force=2`` will only process records with Python device support and
SCAN=I/O Intr.
.. important::
It is **never** safe to use ``sync=True`` while holding record locks,
including from within a *process* method.
"""
return _dbapi._Record.scan(self, *args, **kws)
def asyncStart(self):
"""Start asynchronous processing
This method may be called from a device support
:meth:`process <DeviceSupport.process>` method
to indicate that processing will continue
later.
.. important::
This method is **only** safe to call within a *process* method.
"""
return _dbapi._Record.asyncStart(self)
def asyncFinish(self, reason=None):
"""Indicate that asynchronous processing can complete
Similar to :meth:`scan`. Used to conclude asynchronous
process started with :meth:`asyncStart`.
Processing is completed on the current thread.
.. important::
This method should **never** be called within
a :meth:`process <DeviceSupport.process>` method,
or any other context where a Record lock is held.
Doing so will result in a deadlock.
Typically a *reason* will be passed to *process* as a way
of indicating that this is the completion of an async action. ::
AsyncDone = object()
class MySup(object):
def process(record, reason):
if reason is AsyncDone:
record.VAL = ... # store result
else:
threading.Timer(1.0, record.asyncFinish, kwargs={'reason':AsyncDone})
record.asyncStart()
"""
return _dbapi._Record.asyncFinish(self, reason=reason)
def __getattr__(self, name):
try:
F = self.field(name)
@ -341,5 +408,8 @@ def processLink(name, lstr):
modname, args = parts[0], parts[1] if len(parts)>1 else None
else:
args = lstr
modname, _sep, attr = modname.partition('|')
mod = importmod(modname)
if attr:
mod = getattr(mod, attr)
return rec, mod.build(rec, args)

View File

@ -4,10 +4,7 @@ import traceback
from functools import wraps
from collections import defaultdict
try:
import _dbapi
except ImportError:
import devsup._nullapi as _dbapi
from . import _dbapi
__all__ = [
"hooknames",
@ -45,7 +42,7 @@ def initHook(state):
@initHook("AfterIocRunning")
def myfn():
# do stuff
pass
"""
def _add(fn):
addHook(state, fn)

View File

@ -7,8 +7,8 @@ import threading, inspect
_tables = {}
from devsup.db import IOScanListThread
from devsup import INVALID_ALARM, UDF_ALARM
from .db import IOScanListThread
from . import INVALID_ALARM, UDF_ALARM
__all__ = [
'Parameter',

View File

View File

@ -0,0 +1,165 @@
import os
import unittest
import tempfile
import numpy
from numpy.testing import assert_array_almost_equal, assert_array_equal
from ..db import getRecord
from .. import _dbapi
from .. import _init
from .util import IOCHelper
# short-circuit warning from base_registerRecordDeviceDriver()
os.environ['TOP'] = _dbapi.XPYDEV_BASE # external code use devsup.XPYDEV_BASE
class TestScan(IOCHelper):
db = """
record(longout, src) {
field(OUT, "tgt PP")
}
record(longin, "tgt") {}
"""
autostart = True
def test_link(self):
src, tgt = getRecord('src'), getRecord('tgt')
with src:
src.VAL = 42
self.assertEqual(src.VAL, 42)
with tgt:
self.assertEqual(tgt.VAL, 0)
src.scan(sync=True) # lock and dbProcess() on this thread
with tgt:
self.assertEqual(tgt.VAL, 42)
class TestField(IOCHelper):
db = """
record(ai, "rec:ai") {
field(VAL , "4.2")
field(RVAL, "42")
}
record(stringin, "rec:si") {
field(VAL, "")
}
record(waveform, "rec:wf:a") {
field(FTVL, "DOUBLE")
field(NELM, "10")
}
record(waveform, "rec:wf:s") {
field(FTVL, "STRING")
field(NELM, "10")
}
"""
def test_ai(self):
rec = getRecord("rec:ai")
with rec:
self.assertEqual(rec.VAL, 4.2)
self.assertEqual(rec.RVAL, 42)
rec.VAL = 5.2
rec.RVAL = 52
self.assertEqual(rec.VAL, 5.2)
self.assertEqual(rec.RVAL, 52)
rec.VAL += 1.0
self.assertEqual(rec.VAL, 6.2)
def test_si(self):
rec = getRecord("rec:si")
with rec:
self.assertEqual(rec.VAL, "")
rec.VAL = "test"
self.assertEqual(rec.VAL, "test")
rec.VAL = ""
self.assertEqual(rec.VAL, "")
# implicitly truncates
rec.VAL = "This is a really long string which should be truncated"
self.assertEqual(rec.VAL, "This is a really long string which shou")
# TODO: test unicode
def test_wf_float(self):
rec = getRecord("rec:wf:a")
with rec:
assert_array_almost_equal(rec.VAL, [])
rec.VAL = numpy.arange(5)
assert_array_almost_equal(rec.VAL, numpy.arange(5))
rec.VAL = numpy.arange(10)
assert_array_almost_equal(rec.VAL, numpy.arange(10))
with self.assertRaises(ValueError):
rec.VAL = numpy.arange(15)
rec.VAL = []
assert_array_almost_equal(rec.VAL, [])
# in-place modification
fld = rec.field('VAL')
fld.putarraylen(5)
arr = fld.getarray()
self.assertEqual(arr.shape, (10,)) # size of NELM
arr[:5] = numpy.arange(5) # we only fill in the part in use
arr[2] = 42
assert_array_almost_equal(rec.VAL, [0, 1, 42, 3, 4])
def test_wf_string(self):
rec = getRecord("rec:wf:s")
with rec:
assert_array_equal(rec.VAL, numpy.asarray([], dtype='S40'))
rec.VAL = ["zero", "", "one", "This is a really long string which should be truncated", "", "last"]
assert_array_equal(rec.VAL,
numpy.asarray(["zero", "", "one", "This is a really long string which shoul", "", "last"], dtype='S40'))
class TestDset(IOCHelper):
db = """
record(longin, "rec:li") {
field(DTYP, "Python Device")
field(INP , "@devsup.test.test_db|TestDset foo bar")
}
"""
class Increment(object):
def process(self, rec, reason):
rec.VAL += 1
def detach(self, rec):
pass
@classmethod
def build(klass, rec, args):
if rec.name()=='rec:li':
return klass.Increment()
else:
raise RuntimeError("Unsupported")
def test_increment(self):
rec = getRecord('rec:li')
with rec:
self.assertEqual(rec.VAL, 0)
self.assertEqual(rec.UDF, 1)
rec.scan(sync=True)
with rec:
self.assertEqual(rec.VAL, 1)
self.assertEqual(rec.UDF, 0)

View File

@ -0,0 +1,74 @@
import os
import unittest
import tempfile
import numpy
from numpy.testing import assert_array_almost_equal, assert_array_equal
from ..db import getRecord
from .. import _dbapi
from .. import _init
__all__ = (
'IOCHelper',
)
class IOCHelper(unittest.TestCase):
"""Test case run in an IOC. ::
from devsup.db import getRecord
from devsup.test.util impmort IOCHelper
class TestScan(IOCHelper): # sub-class of unittest.TestCase
db = \"\"\"
record(longout, foo) {}
\"\"\"
autostart = True
def test_link(self):
rec = getRecord('foo')
with rec: # dbScanLock()
self.assertEqual(rec.VAL, 0)
"""
# DB definition to be used. May include eg. 'record(ai, "blah") {}'
db = None
# Whether to automatically run iocInit() before test methods
# whether iocInit() has been called
autostart = True
running = False
def setUp(self):
print("testdbPrepare()")
_dbapi._UTest.testdbPrepare()
_init(iocMain=False) # load base.dbd
if self.db is not None:
with tempfile.NamedTemporaryFile() as F:
F.write(self.db.encode('ascii'))
F.flush()
_dbapi.dbReadDatabase(F.name)
if self.autostart:
self.iocInit()
def tearDown(self):
self.iocShutdown();
print("testdbCleanup()")
_dbapi.initHookAnnounce(9999) # our magic/fake AtExit hook
_dbapi._UTest.testdbCleanup()
def iocInit(self):
"""If not autostart, then this must be called before runtime database access is possible
"""
if not self.running:
print("testIocInitOk")
_dbapi._UTest.testIocInitOk()
self.running = True
def iocShutdown(self):
"""Call to stop IOC scanning processes. Happens automatically during test tearDown
"""
if self.running:
print("testIocShutdownOk")
_dbapi._UTest.testIocShutdownOk()
self.running = False

View File

@ -1,26 +0,0 @@
registrar(pySetupReg)
device(longin, INST_IO, pydevsupComIn, "Python Device")
device(longout, INST_IO, pydevsupComOut, "Python Device")
device(ai, INST_IO, pydevsupComIn, "Python Device")
device(ao, INST_IO, pydevsupComOut, "Python Device")
device(stringin, INST_IO, pydevsupComIn, "Python Device")
device(stringout, INST_IO, pydevsupComOut, "Python Device")
device(bi, INST_IO, pydevsupComIn, "Python Device")
device(bo, INST_IO, pydevsupComOut, "Python Device")
device(mbbi, INST_IO, pydevsupComIn, "Python Device")
device(mbbo, INST_IO, pydevsupComOut, "Python Device")
device(mbbiDirect, INST_IO, pydevsupComIn, "Python Device")
device(mbboDirect, INST_IO, pydevsupComOut, "Python Device")
device(waveform, INST_IO, pydevsupComIn, "Python Device")
device(aai, INST_IO, pydevsupComIn, "Python Device")
device(aao, INST_IO, pydevsupComOut, "Python Device")
function(python_asub)

View File

@ -15,17 +15,19 @@
#endif
PyMODINIT_FUNC init_dbbase(void);
struct dbCommon;
void pyDBD_cleanup(void);
PyObject* pyDBD_setup(PyObject *unused);
PyObject* pyDBD_cleanup(PyObject *unused);
int pyUTest_prepare(PyObject *module);
int pyField_prepare(PyObject *module);
void pyField_cleanup(void);
int pyRecord_prepare(PyObject *module);
int isPyRecord(dbCommon *);
int canIOScanRecord(dbCommon *);
int isPyRecord(struct dbCommon *);
int canIOScanRecord(struct dbCommon *);
extern epicsThreadPrivateId pyDevReasonID;

140
devsupApp/src/utest.c Normal file
View File

@ -0,0 +1,140 @@
/* python has its own ideas about which version to support */
#undef _POSIX_C_SOURCE
#undef _XOPEN_SOURCE
#include <Python.h>
#include <epicsVersion.h>
#include <dbEvent.h>
#include <iocInit.h>
#include <errlog.h>
#if EPICS_VERSION>3 || (EPICS_VERSION==3 && EPICS_REVISION>=15)
# include <dbUnitTest.h>
# define HAVE_DBTEST
#endif
#include "pydevsup.h"
#ifdef HAVE_DBTEST
static dbEventCtx testEvtCtx;
#endif
typedef struct {
PyObject_HEAD
} UTest;
static PyTypeObject UTest_type = {
#if PY_MAJOR_VERSION >= 3
PyVarObject_HEAD_INIT(NULL, 0)
#else
PyObject_HEAD_INIT(NULL)
0,
#endif
"_dbapi._UTest",
sizeof(UTest),
};
static PyObject* utest_prepare(PyObject *unused)
{
Py_BEGIN_ALLOW_THREADS {
//testdbPrepare(); doesn't do anything essential for us as of 7.0.2
} Py_END_ALLOW_THREADS
Py_RETURN_NONE;
}
static PyObject* utest_init(PyObject *unused)
{
#ifdef HAVE_DBTEST
int ret;
if(testEvtCtx)
return PyErr_Format(PyExc_RuntimeError, "Missing testIocShutdownOk()");
// like, testIocInitOk() without testAbort()
Py_BEGIN_ALLOW_THREADS {
eltc(0);
ret = iocBuildIsolated() || iocRun();
eltc(1);
} Py_END_ALLOW_THREADS
if(ret) {
return PyErr_Format(PyExc_RuntimeError, "iocInit fails with %d", ret);
}
Py_BEGIN_ALLOW_THREADS {
testEvtCtx=db_init_events();
} Py_END_ALLOW_THREADS
if(!testEvtCtx) {
iocShutdown();
return PyErr_Format(PyExc_RuntimeError, "iocInit fails create dbEvent context");
}
Py_BEGIN_ALLOW_THREADS {
ret = db_start_events(testEvtCtx, "CAS-test-py", NULL, NULL, epicsThreadPriorityCAServerLow);
} Py_END_ALLOW_THREADS
if(ret!=DB_EVENT_OK) {
db_close_events(testEvtCtx);
testEvtCtx = NULL;
iocShutdown();
return PyErr_Format(PyExc_RuntimeError, "db_start_events fails with %d", ret);
}
Py_RETURN_NONE;
#else
return PyErr_Format(PyExc_RuntimeError, "Requires Base >=3.15");
#endif
}
static PyObject* utest_shutdown(PyObject *unused)
{
#ifdef HAVE_DBTEST
Py_BEGIN_ALLOW_THREADS {
//testIocShutdownOk();
db_close_events(testEvtCtx);
testEvtCtx = NULL;
iocShutdown();
} Py_END_ALLOW_THREADS
Py_RETURN_NONE;
#else
return PyErr_Format(PyExc_RuntimeError, "Requires Base >=3.15");
#endif
}
static PyObject* utest_cleanup(PyObject *unused)
{
#ifdef HAVE_DBTEST
Py_BEGIN_ALLOW_THREADS {
testdbCleanup();
errlogFlush();
} Py_END_ALLOW_THREADS
Py_RETURN_NONE;
#else
return PyErr_Format(PyExc_RuntimeError, "Requires Base >=3.15");
#endif
}
static PyMethodDef UTest_methods[] = {
{"testdbPrepare", (PyCFunction)&utest_prepare, METH_STATIC|METH_NOARGS, ""},
{"testIocInitOk", (PyCFunction)&utest_init, METH_STATIC|METH_NOARGS, ""},
{"testIocShutdownOk", (PyCFunction)&utest_shutdown, METH_STATIC|METH_NOARGS, ""},
{"testdbCleanup", (PyCFunction)&utest_cleanup, METH_STATIC|METH_NOARGS, ""},
{NULL}
};
int pyUTest_prepare(PyObject *module)
{
PyObject *typeobj=(PyObject*)&UTest_type;
UTest_type.tp_flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE;
UTest_type.tp_methods = UTest_methods;
if(PyType_Ready(&UTest_type)<0)
return -1;
Py_INCREF(typeobj);
if(PyModule_AddObject(module, "_UTest", typeobj)) {
Py_DECREF(typeobj);
return -1;
}
return 0;
}

View File

@ -108,7 +108,7 @@ In somefile.c ::
PyMODINIT_FUNC init_myextname(void)
Installing for several Python versions
------------------------------------
--------------------------------------
The recipe for building and installing the pyDevSup module
for several python version side by side is ::

View File

@ -28,23 +28,39 @@ libdirs = [get_config_var('LIBDIR')]
have_np='NO'
try:
from numpy.distutils.misc_util import get_numpy_include_dirs
incdirs += get_numpy_include_dirs()
incdirs = get_numpy_include_dirs()+incdirs
have_np='YES'
except ImportError:
pass
incdirs = [get_python_inc()]+get_numpy_include_dirs()
libdirs = [get_config_var('LIBDIR')]
print('TARGET_CFLAGS +=',get_config_var('BASECFLAGS'), file=out)
print('TARGET_CXXFLAGS +=',get_config_var('BASECFLAGS'), file=out)
print('PY_VER :=',get_config_var('VERSION'), file=out)
print('PY_LD_VER :=',get_config_var('LDVERSION') or get_config_var('VERSION'), file=out)
ldver = get_config_var('LDVERSION')
if ldver is None:
ldver = get_config_var('VERSION')
if get_config_var('Py_DEBUG'):
ldver = ldver+'_d'
print('PY_LD_VER :=',ldver, file=out)
print('PY_INCDIRS :=',' '.join(incdirs), file=out)
print('PY_LIBDIRS :=',' '.join(libdirs), file=out)
print('HAVE_NUMPY :=',have_np, file=out)
try:
import asyncio
except ImportError:
print('HAVE_ASYNCIO := NO', file=out)
else:
print('HAVE_ASYNCIO := YES', file=out)
try:
import cothread
except ImportError:
print('HAVE_COTHREAD := NO', file=out)
else:
print('HAVE_COTHREAD := YES', file=out)
print('PY_OK := YES', file=out)
out.close()

77
pyIocApp/Makefile Normal file
View File

@ -0,0 +1,77 @@
TOP=..
include $(TOP)/configure/CONFIG
PYMODULE = NO
include $(TOP)/configure/CONFIG_PY
#----------------------------------------
# ADD MACRO DEFINITIONS AFTER THIS LINE
#=============================
#=============================
# Build the IOC application
USR_CPPFLAGS += -I$(TOP)/devsupApp/src
LIBRARY = pyDevSup$(PY_LD_VER)
SHRLIB_VERSION = 0
DBD += pyDevSup.dbd
pyDevSup$(PY_LD_VER)_SYS_LIBS += python$(PY_LD_VER)
pyDevSup$(PY_LD_VER)_LIBS += $(EPICS_BASE_IOC_LIBS)
setup_CPPFLAGS += -DXEPICS_ARCH=\"$(T_A)\"
setup_CPPFLAGS += -DXPYDEV_BASE=\"$(abspath $(INSTALL_LOCATION))\"
setup_CPPFLAGS += -DXEPICS_BASE=\"$(EPICS_BASE)\"
setup_CPPFLAGS += -DPYDIR=\"python$(PY_LD_VER)\"
pyDevSup$(PY_LD_VER)_SRCS += setup.c
PROD_IOC = softIocPy$(PY_VER)
PRODNAME = $(addsuffix $(EXE),$(PROD))
# softIocPy.dbd will be created and installed
DBD += softIocPy.dbd
# softIocPy.dbd will be made up from these files:
softIocPy_DBD += base.dbd
softIocPy_DBD += pyDevSup.dbd
softIocPy_DBD += system.dbd
softIocPy$(PY_VER)_LIBS += pyDevSup$(PY_LD_VER)
# softIocPy_registerRecordDeviceDriver.cpp derives from softIocPy.dbd
softIocPy$(PY_VER)_SRCS += softIocPy_registerRecordDeviceDriver.cpp
# Build the main IOC entry point on workstation OSs.
softIocPy$(PY_VER)_SRCS_DEFAULT += devsupMain.cpp
devsupMain_CPPFLAGS += -DXPYDEV_BASE=\"$(abspath $(INSTALL_LOCATION))\"
ifneq ($(DEVIOCSTATS),)
softIocPy_DBD += devIocStats.dbd
softIocPy$(PY_VER)_LIBS += devIocStats
endif
ifneq ($(AUTOSAVE),)
softIocPy_DBD += asSupport.dbd
softIocPy$(PY_VER)_LIBS += autosave
endif
ifneq ($(CAPUTLOG),)
softIocPy_DBD += caPutLog.dbd
softIocPy$(PY_VER)_LIBS += caPutLog
endif
# Finally link to the EPICS Base libraries
softIocPy$(PY_VER)_LIBS += $(EPICS_BASE_IOC_LIBS)
#===========================
include $(TOP)/configure/RULES
include $(TOP)/configure/RULES_PY
#----------------------------------------
# ADD RULES AFTER THIS LINE

1
pyIocApp/pyDevSup.dbd Normal file
View File

@ -0,0 +1 @@
registrar(pySetupReg)

135
pyIocApp/setup.c Normal file
View File

@ -0,0 +1,135 @@
/* Global interpreter setup
*/
/* python has its own ideas about which version to support */
#undef _POSIX_C_SOURCE
#undef _XOPEN_SOURCE
#include <Python.h>
#ifdef HAVE_NUMPY
#include <numpy/ndarrayobject.h>
#endif
#include <stdio.h>
#include <epicsVersion.h>
#include <dbCommon.h>
#include <dbAccess.h>
#include <dbStaticLib.h>
#include <dbScan.h>
#include <initHooks.h>
#include <epicsThread.h>
#include <epicsExit.h>
#include <alarm.h>
#include "pydevsup.h"
static void cleanupPy(void *junk)
{
PyThreadState *state = PyGILState_GetThisThreadState();
PyEval_RestoreThread(state);
if(PyRun_SimpleString("import devsup\n"
"devsup._fini(iocMain=True)\n"
)) {
PyErr_Print();
PyErr_Clear();
}
Py_Finalize(); // calls python atexit hooks
}
static void extendPath(PyObject *list,
const char *base,
const char *archdir)
{
PyObject *mod, *ret;
mod = PyImport_ImportModule("os.path");
if(!mod)
return;
ret = PyObject_CallMethod(mod, "join", "sss", base, PYDIR, archdir);
if(ret && !PySequence_Contains(list, ret)) {
PyList_Insert(list, 0, ret);
}
Py_XDECREF(ret);
Py_DECREF(mod);
if(PyErr_Occurred()) {
PyErr_Print();
PyErr_Clear();
}
}
static void insertDefaultPath(PyObject *list)
{
const char *basedir, *pydevdir, *top, *arch;
basedir = getenv("EPICS_BASE");
if(!basedir)
basedir = XEPICS_BASE;
pydevdir = getenv("PYDEV_BASE");
if(!pydevdir)
pydevdir = XPYDEV_BASE;
top = getenv("TOP");
arch = getenv("ARCH");
if(!arch)
arch = XEPICS_ARCH;
assert(PyList_Check(list));
assert(PySequence_Check(list));
extendPath(list, basedir, arch);
extendPath(list, pydevdir, arch);
if(top)
extendPath(list, top, arch);
}
static void setupPyPath(void)
{
PyObject *mod, *path = NULL;
mod = PyImport_ImportModule("sys");
if(mod)
path = PyObject_GetAttrString(mod, "path");
Py_XDECREF(mod);
if(path) {
PyObject *cur;
char cwd[PATH_MAX];
insertDefaultPath(path);
/* prepend current directory */
if(getcwd(cwd, sizeof(cwd)-1)) {
cwd[sizeof(cwd)-1] = '\0';
cur = PyString_FromString(cwd);
if(cur)
PyList_Insert(path, 0, cur);
Py_XDECREF(cur);
}
}
Py_XDECREF(path);
}
static void pySetupReg(void)
{
Py_InitializeEx(0);
PyEval_InitThreads();
setupPyPath();
if(PyRun_SimpleString("import devsup\n"
"devsup._init(iocMain=True)\n"
)) {
PyErr_Print();
PyErr_Clear();
}
(void)PyEval_SaveThread();
epicsAtExit(&cleanupPy, NULL);
}
#include <epicsExport.h>
epicsExportRegistrar(pySetupReg);

View File

@ -3,7 +3,6 @@
py "import logging"
py "logging.basicConfig(level=logging.DEBUG)"
py "import devsup; print devsup.HAVE_DBAPI"
py "import sys; sys.path.insert(0,'${PWD}/testApp')"
py "print sys.path"
@ -23,4 +22,4 @@ dbLoadRecords("db/test6.db","P=tst:,TNAME=tsum")
iocInit()
# Start Reference tracker
py "from devsup import disect; disect.periodic(10)"
#py "from devsup import disect; disect.periodic(10)"

View File

@ -27,11 +27,7 @@ class WfSup(object):
x=self.x[:N]
# calculate inplace: uniform(0.5,2.0)*sin(pha*x)+2
val[:] = x
val[:] *= pha
np.sin(val, out=val)
val[:]*=uniform(0.5,2.0)
val[:]+=2
val[:] = np.sin(x*pha)*uniform(0.5,2.0) + 2
self.fld.putarraylen(N)