From 1d8c6ab7b5cb837f8aa5f2d5ae8976ffa3c88c26 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Fri, 2 Nov 2018 16:47:57 -0700 Subject: [PATCH 01/14] Makefile updates from p4p --- configure/CONFIG_PY | 39 ++++++++++++++++++++++++--------------- configure/CONFIG_SITE | 2 +- configure/Makefile | 9 ++++----- makehelper.py | 26 +++++++++++++++++++++----- 4 files changed, 50 insertions(+), 26 deletions(-) diff --git a/configure/CONFIG_PY b/configure/CONFIG_PY index 5e2ea78..f2edef8 100644 --- a/configure/CONFIG_PY +++ b/configure/CONFIG_PY @@ -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 diff --git a/configure/CONFIG_SITE b/configure/CONFIG_SITE index 559e2d8..11ed558 100644 --- a/configure/CONFIG_SITE +++ b/configure/CONFIG_SITE @@ -37,7 +37,7 @@ PY_VER=2.7 # Module will be build against this version of the # Python interpreter -#PYTHON = python$(PY_VER) +PYTHON ?= python$(PY_VER) -include $(TOP)/configure/CONFIG_SITE.local -include $(TOP)/../CONFIG_SITE.local diff --git a/configure/Makefile b/configure/Makefile index 4ab05b9..deec5c9 100644 --- a/configure/Makefile +++ b/configure/Makefile @@ -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) $< $@ diff --git a/makehelper.py b/makehelper.py index c7bdc41..6ce8b8a 100644 --- a/makehelper.py +++ b/makehelper.py @@ -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() From 662b518338911131a91c6d4807f6ed3444e49bb1 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Fri, 2 Nov 2018 17:12:31 -0700 Subject: [PATCH 02/14] separate softIocPy build --- Makefile | 2 + devsupApp/src/Makefile | 42 ---------------- pyIocApp/Makefile | 58 ++++++++++++++++++++++ {devsupApp/src => pyIocApp}/devsupMain.cpp | 0 4 files changed, 60 insertions(+), 42 deletions(-) create mode 100644 pyIocApp/Makefile rename {devsupApp/src => pyIocApp}/devsupMain.cpp (100%) diff --git a/Makefile b/Makefile index 2627e9a..b232814 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,8 @@ 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 diff --git a/devsupApp/src/Makefile b/devsupApp/src/Makefile index 2208847..349b5be 100644 --- a/devsupApp/src/Makefile +++ b/devsupApp/src/Makefile @@ -23,8 +23,6 @@ setup_CPPFLAGS += -DXPYDEV_BASE=\"$(abspath $(INSTALL_LOCATION))\" setup_CPPFLAGS += -DXEPICS_BASE=\"$(EPICS_BASE)\" setup_CPPFLAGS += -DPYDIR=\"python$(PY_VER)\" -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 @@ -33,46 +31,6 @@ 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) - - PY += devsup/__init__.py PY += devsup/_nullapi.py PY += devsup/db.py diff --git a/pyIocApp/Makefile b/pyIocApp/Makefile new file mode 100644 index 0000000..fe2bec3 --- /dev/null +++ b/pyIocApp/Makefile @@ -0,0 +1,58 @@ +TOP=.. + +include $(TOP)/configure/CONFIG +PYMODULE = NO +include $(TOP)/configure/CONFIG_PY +#---------------------------------------- +# ADD MACRO DEFINITIONS AFTER THIS LINE +#============================= + +#============================= +# Build the IOC application + +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 diff --git a/devsupApp/src/devsupMain.cpp b/pyIocApp/devsupMain.cpp similarity index 100% rename from devsupApp/src/devsupMain.cpp rename to pyIocApp/devsupMain.cpp From f428d2a4e4226d547ea1992ae9c2204446c3caee Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sat, 3 Nov 2018 13:33:36 -0700 Subject: [PATCH 03/14] rework to separate out python module Remove the magic from the magic _db* modules. Combine as one 'devsup._dbapi'. libpyDevSup exists only to bootstrap python interpreter in IOCs. --- devsupApp/src/Makefile | 32 ++- devsupApp/src/{setup.c => dbapi.c} | 327 ++++++++++++----------------- devsupApp/src/dbbase.c | 136 ------------ devsupApp/src/dbdset.c | 33 ++- devsupApp/src/dbfield.c | 9 - devsupApp/src/devsup/__init__.py | 88 +++++--- devsupApp/src/devsup/db.py | 5 +- devsupApp/src/devsup/hooks.py | 5 +- devsupApp/src/devsup/ptable.py | 4 +- devsupApp/src/pyDevSup.dbd | 26 --- devsupApp/src/pydevsup.h | 6 +- pyIocApp/Makefile | 19 ++ pyIocApp/pyDevSup.dbd | 1 + pyIocApp/setup.c | 133 ++++++++++++ test.cmd | 3 +- 15 files changed, 401 insertions(+), 426 deletions(-) rename devsupApp/src/{setup.c => dbapi.c} (67%) delete mode 100644 devsupApp/src/dbbase.c delete mode 100644 devsupApp/src/pyDevSup.dbd create mode 100644 pyIocApp/pyDevSup.dbd create mode 100644 pyIocApp/setup.c diff --git a/devsupApp/src/Makefile b/devsupApp/src/Makefile index 349b5be..55479e7 100644 --- a/devsupApp/src/Makefile +++ b/devsupApp/src/Makefile @@ -1,35 +1,31 @@ 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 -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) +_dbapi_SRCS += pyDevSupCommon_registerRecordDeviceDriver.cpp PY += devsup/__init__.py PY += devsup/_nullapi.py diff --git a/devsupApp/src/setup.c b/devsupApp/src/dbapi.c similarity index 67% rename from devsupApp/src/setup.c rename to devsupApp/src/dbapi.c index 9b4ad51..6632901 100644 --- a/devsupApp/src/setup.c +++ b/devsupApp/src/dbapi.c @@ -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,9 +18,13 @@ #include #include #include +#include +#include #include "pydevsup.h" +extern int pyDevSupCommon_registerRecordDeviceDriver(DBBASE *pbase); + typedef struct { const initHookState state; const char * const name; @@ -102,6 +103,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) @@ -137,28 +152,127 @@ fail: PyGILState_Release(gilstate); } +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) +{ + Py_BEGIN_ALLOW_THREADS { + iocInit(); + } Py_END_ALLOW_THREADS + + 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[] = { + {"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 */ PyMODINIT_FUNC init_dbapi(void) { - PyObject *mod = NULL, *hookdict; + PyObject *mod = NULL, *hookdict, *vertup, *obj; 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; @@ -178,43 +292,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); @@ -277,150 +354,24 @@ PyMODINIT_FUNC init_dbconstants(void) if(vertup) PyModule_AddObject(mod, "pydevver", vertup); +#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); + + if(pyField_prepare(mod)) + goto fail; + if(pyRecord_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 - -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 -epicsExportRegistrar(pySetupReg); diff --git a/devsupApp/src/dbbase.c b/devsupApp/src/dbbase.c deleted file mode 100644 index 5d9e2f1..0000000 --- a/devsupApp/src/dbbase.c +++ /dev/null @@ -1,136 +0,0 @@ - -/* python has its own ideas about which version to support */ -#undef _POSIX_C_SOURCE -#undef _XOPEN_SOURCE - -#include - -#include -#include -#include -#include -#include -#include -#include - -#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); -} diff --git a/devsupApp/src/dbdset.c b/devsupApp/src/dbdset.c index 1ecc6cd..badfb5c 100644 --- a/devsupApp/src/dbdset.c +++ b/devsupApp/src/dbdset.c @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #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 - -epicsExportAddress(dset, pydevsupComSpec); -epicsExportAddress(dset, pydevsupComIn); -epicsExportAddress(dset, pydevsupComOut); - -epicsRegisterFunction(python_asub); diff --git a/devsupApp/src/dbfield.c b/devsupApp/src/dbfield.c index cf0588a..895fd75 100644 --- a/devsupApp/src/dbfield.c +++ b/devsupApp/src/dbfield.c @@ -569,12 +569,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; - } -} diff --git a/devsupApp/src/devsup/__init__.py b/devsupApp/src/devsup/__init__.py index 977f926..f7a6988 100644 --- a/devsupApp/src/devsup/__init__.py +++ b/devsupApp/src/devsup/__init__.py @@ -1,28 +1,68 @@ -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, + INVALID_ALARM, + UDF_ALARM, + ) __all__ = [] + +_ready = [False] + +def _init(iocMain=False): + if _ready[0]: + return + _ready[0] = True + + if not iocMain: + # we haven't read/register base.dbd + _dbapi.dbReadDatabase(os.path.join(XEPICS_BASE, "dbd", "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") +""") + F.flush() + _dbapi.dbReadDatabase(F.name) + _dbapi._dbd_setup() + +@atexit.register +def _fini(): + print("ATEXIT") + _dbapi._dbd_cleanup() diff --git a/devsupApp/src/devsup/db.py b/devsupApp/src/devsup/db.py index f36d2f6..f936f34 100644 --- a/devsupApp/src/devsup/db.py +++ b/devsupApp/src/devsup/db.py @@ -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() diff --git a/devsupApp/src/devsup/hooks.py b/devsupApp/src/devsup/hooks.py index 0cc8abb..9ac261a 100644 --- a/devsupApp/src/devsup/hooks.py +++ b/devsupApp/src/devsup/hooks.py @@ -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", diff --git a/devsupApp/src/devsup/ptable.py b/devsupApp/src/devsup/ptable.py index 24e7570..800c0e6 100644 --- a/devsupApp/src/devsup/ptable.py +++ b/devsupApp/src/devsup/ptable.py @@ -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', diff --git a/devsupApp/src/pyDevSup.dbd b/devsupApp/src/pyDevSup.dbd deleted file mode 100644 index 4ca815f..0000000 --- a/devsupApp/src/pyDevSup.dbd +++ /dev/null @@ -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) - diff --git a/devsupApp/src/pydevsup.h b/devsupApp/src/pydevsup.h index 9b7cbf8..76bcf28 100644 --- a/devsupApp/src/pydevsup.h +++ b/devsupApp/src/pydevsup.h @@ -17,12 +17,10 @@ initHookState pyInitLastState; -PyMODINIT_FUNC init_dbbase(void); - -void pyDBD_cleanup(void); +PyObject* pyDBD_setup(PyObject *unused); +PyObject* pyDBD_cleanup(PyObject *unused); int pyField_prepare(PyObject *module); -void pyField_cleanup(void); int pyRecord_prepare(PyObject *module); diff --git a/pyIocApp/Makefile b/pyIocApp/Makefile index fe2bec3..8e41d4d 100644 --- a/pyIocApp/Makefile +++ b/pyIocApp/Makefile @@ -10,6 +10,25 @@ include $(TOP)/configure/CONFIG_PY #============================= # 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_VER)\" + +pyDevSup$(PY_LD_VER)_SRCS += setup.c + PROD_IOC = softIocPy$(PY_VER) PRODNAME = $(addsuffix $(EXE),$(PROD)) diff --git a/pyIocApp/pyDevSup.dbd b/pyIocApp/pyDevSup.dbd new file mode 100644 index 0000000..b09ebf3 --- /dev/null +++ b/pyIocApp/pyDevSup.dbd @@ -0,0 +1 @@ +registrar(pySetupReg) diff --git a/pyIocApp/setup.c b/pyIocApp/setup.c new file mode 100644 index 0000000..263baf3 --- /dev/null +++ b/pyIocApp/setup.c @@ -0,0 +1,133 @@ +/* Global interpreter setup + */ + +/* python has its own ideas about which version to support */ +#undef _POSIX_C_SOURCE +#undef _XOPEN_SOURCE + +#include +#ifdef HAVE_NUMPY +#include +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pydevsup.h" + +static void cleanupPy(void *junk) +{ + PyThreadState *state = PyGILState_GetThisThreadState(); + + PyEval_RestoreThread(state); + + /* special "fake" hook for shutdown */ + //pyhook((initHookState)9999); + + 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) +{ + PyGILState_STATE state; + + Py_Initialize(); + PyEval_InitThreads(); + + setupPyPath(); + + if(PyRun_SimpleString("import devsup\n" + "devsup._init(True)\n" + )) { + PyErr_Print(); + PyErr_Clear(); + } + + (void)PyEval_SaveThread(); + + epicsAtExit(&cleanupPy, NULL); +} + +#include +epicsExportRegistrar(pySetupReg); diff --git a/test.cmd b/test.cmd index 210dcdf..f1ad20e 100644 --- a/test.cmd +++ b/test.cmd @@ -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)" From a3524c5aa0121123f6a0dcb7baba06e7c1cc3c1f Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sat, 3 Nov 2018 14:39:57 -0700 Subject: [PATCH 04/14] start PDB unittest --- Makefile | 16 +++- configure/CONFIG_SITE | 2 + devsupApp/src/Makefile | 23 +++++ devsupApp/src/dbapi.c | 34 +++++--- devsupApp/src/devsup/__init__.py | 9 +- devsupApp/src/devsup/test/__init__.py | 0 devsupApp/src/devsup/test/test_db.py | 82 +++++++++++++++++ devsupApp/src/pydevsup.h | 2 + devsupApp/src/utest.c | 121 ++++++++++++++++++++++++++ 9 files changed, 264 insertions(+), 25 deletions(-) create mode 100644 devsupApp/src/devsup/test/__init__.py create mode 100644 devsupApp/src/devsup/test/test_db.py create mode 100644 devsupApp/src/utest.c diff --git a/Makefile b/Makefile index b232814..7500ef2 100644 --- a/Makefile +++ b/Makefile @@ -18,8 +18,16 @@ 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 diff --git a/configure/CONFIG_SITE b/configure/CONFIG_SITE index 11ed558..49c12e8 100644 --- a/configure/CONFIG_SITE +++ b/configure/CONFIG_SITE @@ -39,5 +39,7 @@ PY_VER=2.7 # Python interpreter PYTHON ?= python$(PY_VER) +USR_CPPFLAGS += -DUSE_TYPED_RSET + -include $(TOP)/configure/CONFIG_SITE.local -include $(TOP)/../CONFIG_SITE.local diff --git a/devsupApp/src/Makefile b/devsupApp/src/Makefile index 55479e7..6e324b1 100644 --- a/devsupApp/src/Makefile +++ b/devsupApp/src/Makefile @@ -24,9 +24,12 @@ _dbapi_SRCS += dbapi.c _dbapi_SRCS += dbrec.c _dbapi_SRCS += dbfield.c _dbapi_SRCS += dbdset.c +_dbapi_SRCS += utest.c _dbapi_SRCS += pyDevSupCommon_registerRecordDeviceDriver.cpp +_dbapi_LIBS += $(EPICS_BASE_IOC_LIBS) + PY += devsup/__init__.py PY += devsup/_nullapi.py PY += devsup/db.py @@ -37,6 +40,9 @@ PY += devsup/util.py PY += devsup/disect.py PY += devsup/ptable.py +PY += devsup/test/__init__.py +PY += devsup/test/test_db.py + #=========================== include $(TOP)/configure/RULES @@ -52,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 diff --git a/devsupApp/src/dbapi.c b/devsupApp/src/dbapi.c index 6632901..200e4f4 100644 --- a/devsupApp/src/dbapi.c +++ b/devsupApp/src/dbapi.c @@ -194,9 +194,9 @@ PyObject *py_dbReadDatabase(PyObject *unused, PyObject *args, PyObject *kws) } Py_BEGIN_ALLOW_THREADS { - if(fname) + if(fname) { status = dbReadDatabase(&pdbbase, fname, path, sub); - else { + } else { FILE *ff = fdopen(fd, "r"); status = dbReadDatabaseFP(&pdbbase, ff, path, sub); // dbReadDatabaseFP() has called fclose() @@ -213,18 +213,31 @@ PyObject *py_dbReadDatabase(PyObject *unused, PyObject *args, PyObject *kws) } static -PyObject *py_iocInit(PyObject *unused) +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 { - iocInit(); + 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) { + static int once; Py_BEGIN_ALLOW_THREADS { pyDevSupCommon_registerRecordDeviceDriver(pdbbase); } Py_END_ALLOW_THREADS @@ -258,7 +271,7 @@ static struct PyModuleDef dbapimodule = { PyMODINIT_FUNC init_dbapi(void) { - PyObject *mod = NULL, *hookdict, *vertup, *obj; + PyObject *mod = NULL, *hookdict, *vertup; pystate *st; pyDevReasonID = epicsThreadPrivateCreate(); @@ -354,19 +367,12 @@ PyMODINIT_FUNC init_dbapi(void) if(vertup) PyModule_AddObject(mod, "pydevver", vertup); -#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); - if(pyField_prepare(mod)) goto fail; if(pyRecord_prepare(mod)) goto fail; + if(pyUTest_prepare(mod)) + goto fail; MODINIT_RET(mod); diff --git a/devsupApp/src/devsup/__init__.py b/devsupApp/src/devsup/__init__.py index f7a6988..bf051fc 100644 --- a/devsupApp/src/devsup/__init__.py +++ b/devsupApp/src/devsup/__init__.py @@ -22,16 +22,11 @@ from ._dbapi import (EPICS_VERSION_STRING, __all__ = [] -_ready = [False] - def _init(iocMain=False): - if _ready[0]: - return - _ready[0] = True - if not iocMain: # we haven't read/register base.dbd - _dbapi.dbReadDatabase(os.path.join(XEPICS_BASE, "dbd", "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: diff --git a/devsupApp/src/devsup/test/__init__.py b/devsupApp/src/devsup/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/devsupApp/src/devsup/test/test_db.py b/devsupApp/src/devsup/test/test_db.py new file mode 100644 index 0000000..310d011 --- /dev/null +++ b/devsupApp/src/devsup/test/test_db.py @@ -0,0 +1,82 @@ + +import os +import unittest +import tempfile + +from ..db import getRecord +from .. import _dbapi +from .. import _init + +# short-circuit warning from _dbapi._init() +os.environ['TOP'] = _dbapi.XPYDEV_BASE + +class IOCHelper(unittest.TestCase): + db = None + autostart = 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) + F.flush() + _dbapi.dbReadDatabase(F.name) + + if self.autostart: + self.iocInit() + + def tearDown(self): + self.iocShutdown(); + print("testdbCleanup()") + _dbapi._UTest.testdbCleanup() + + def iocInit(self): + if not self.running: + print("testIocInitOk") + _dbapi._UTest.testIocInitOk() + self.running = True + + def iocShutdown(self): + if self.running: + print("testIocShutdownOk") + _dbapi._UTest.testIocShutdownOk() + self.running = False + +class TestIOC(IOCHelper): + def test_base(self): + pass + + def test_start(self): + self.iocInit() + self.iocShutdown() + + def test_db(self): + with tempfile.NamedTemporaryFile() as F: + F.write('record(longin, "test") {}\n') + F.flush() + _dbapi.dbReadDatabase(F.name) + + rec = getRecord("test") + self.assertEqual(rec.VAL, 0) + rec.VAL = 5 + self.assertEqual(rec.VAL, 5) + +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') + + src.VAL = 42 + self.assertEqual(src.VAL, 42) + self.assertEqual(tgt.VAL, 0) + src.scan(sync=True) + self.assertEqual(tgt.VAL, 42) diff --git a/devsupApp/src/pydevsup.h b/devsupApp/src/pydevsup.h index 76bcf28..93e75d8 100644 --- a/devsupApp/src/pydevsup.h +++ b/devsupApp/src/pydevsup.h @@ -20,6 +20,8 @@ initHookState pyInitLastState; PyObject* pyDBD_setup(PyObject *unused); PyObject* pyDBD_cleanup(PyObject *unused); +int pyUTest_prepare(PyObject *module); + int pyField_prepare(PyObject *module); int pyRecord_prepare(PyObject *module); diff --git a/devsupApp/src/utest.c b/devsupApp/src/utest.c new file mode 100644 index 0000000..344c5bb --- /dev/null +++ b/devsupApp/src/utest.c @@ -0,0 +1,121 @@ + +/* python has its own ideas about which version to support */ +#undef _POSIX_C_SOURCE +#undef _XOPEN_SOURCE + +#include + +#include +#include +#include +#include + +#include "pydevsup.h" + +static dbEventCtx testEvtCtx; + +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) +{ + 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; +} + +static PyObject* utest_shutdown(PyObject *unused) +{ + Py_BEGIN_ALLOW_THREADS { + //testIocShutdownOk(); + db_close_events(testEvtCtx); + testEvtCtx = NULL; + iocShutdown(); + } Py_END_ALLOW_THREADS + Py_RETURN_NONE; +} + +static PyObject* utest_cleanup(PyObject *unused) +{ + Py_BEGIN_ALLOW_THREADS { + testdbCleanup(); + errlogFlush(); + } Py_END_ALLOW_THREADS + Py_RETURN_NONE; +} + +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; +} From 11a93fce21666a2c1245c706a1aa1815be9eca87 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 4 Nov 2018 12:33:09 -0800 Subject: [PATCH 05/14] test field access and dset --- devsupApp/src/dbfield.c | 8 +- devsupApp/src/devsup/__init__.py | 4 +- devsupApp/src/devsup/db.py | 3 + devsupApp/src/devsup/test/test_db.py | 164 +++++++++++++++++++++++---- pyIocApp/setup.c | 9 +- 5 files changed, 154 insertions(+), 34 deletions(-) diff --git a/devsupApp/src/dbfield.c b/devsupApp/src/dbfield.c index 895fd75..ed48687 100644 --- a/devsupApp/src/dbfield.c +++ b/devsupApp/src/dbfield.c @@ -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; } diff --git a/devsupApp/src/devsup/__init__.py b/devsupApp/src/devsup/__init__.py index bf051fc..ea6c48b 100644 --- a/devsupApp/src/devsup/__init__.py +++ b/devsupApp/src/devsup/__init__.py @@ -57,7 +57,5 @@ device(aao, INST_IO, pydevsupComOut, "Python Device") _dbapi.dbReadDatabase(F.name) _dbapi._dbd_setup() -@atexit.register -def _fini(): - print("ATEXIT") +def _fini(iocMain=False): _dbapi._dbd_cleanup() diff --git a/devsupApp/src/devsup/db.py b/devsupApp/src/devsup/db.py index f936f34..8b29498 100644 --- a/devsupApp/src/devsup/db.py +++ b/devsupApp/src/devsup/db.py @@ -338,5 +338,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) diff --git a/devsupApp/src/devsup/test/test_db.py b/devsupApp/src/devsup/test/test_db.py index 310d011..f543439 100644 --- a/devsupApp/src/devsup/test/test_db.py +++ b/devsupApp/src/devsup/test/test_db.py @@ -3,6 +3,9 @@ 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 @@ -44,25 +47,6 @@ class IOCHelper(unittest.TestCase): _dbapi._UTest.testIocShutdownOk() self.running = False -class TestIOC(IOCHelper): - def test_base(self): - pass - - def test_start(self): - self.iocInit() - self.iocShutdown() - - def test_db(self): - with tempfile.NamedTemporaryFile() as F: - F.write('record(longin, "test") {}\n') - F.flush() - _dbapi.dbReadDatabase(F.name) - - rec = getRecord("test") - self.assertEqual(rec.VAL, 0) - rec.VAL = 5 - self.assertEqual(rec.VAL, 5) - class TestScan(IOCHelper): db = """ record(longout, src) { @@ -75,8 +59,140 @@ class TestScan(IOCHelper): def test_link(self): src, tgt = getRecord('src'), getRecord('tgt') - src.VAL = 42 - self.assertEqual(src.VAL, 42) - self.assertEqual(tgt.VAL, 0) - src.scan(sync=True) - self.assertEqual(tgt.VAL, 42) + 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") + } + """ + autostart = True + + 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, ["zero", "", "one", "This is a really long string which shoul", "", "last"]) + + +class TestDset(IOCHelper): + db = """ + record(longin, "rec:li") { + field(DTYP, "Python Device") + field(INP , "@devsup.test.test_db|TestDset foo bar") + } + """ + autostart = True + + 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) diff --git a/pyIocApp/setup.c b/pyIocApp/setup.c index 263baf3..b5197fa 100644 --- a/pyIocApp/setup.c +++ b/pyIocApp/setup.c @@ -33,6 +33,13 @@ static void cleanupPy(void *junk) /* special "fake" hook for shutdown */ //pyhook((initHookState)9999); + if(PyRun_SimpleString("import devsup\n" + "devsup._fini(iocMain=True)\n" + )) { + PyErr_Print(); + PyErr_Clear(); + } + Py_Finalize(); // calls python atexit hooks } @@ -118,7 +125,7 @@ static void pySetupReg(void) setupPyPath(); if(PyRun_SimpleString("import devsup\n" - "devsup._init(True)\n" + "devsup._init(iocMain=True)\n" )) { PyErr_Print(); PyErr_Clear(); From 1abc7925161b731b8e1eb27b516652260cc1c13c Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 4 Nov 2018 13:10:08 -0800 Subject: [PATCH 06/14] travis-ci update --- .travis.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3bd1034..d32fd56 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,16 +14,19 @@ 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.16 PROF=deb8 - python: "2.7" env: BRBASE=3.15 PROF=deb8 - python: "2.7" From 2c34d9df52ed6215c68dec1fc256fe610d510e94 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 4 Nov 2018 15:42:34 -0800 Subject: [PATCH 07/14] py3 --- devsupApp/src/dbapi.c | 4 ++++ devsupApp/src/devsup/__init__.py | 2 +- devsupApp/src/devsup/test/test_db.py | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/devsupApp/src/dbapi.c b/devsupApp/src/dbapi.c index 200e4f4..bff2e7f 100644 --- a/devsupApp/src/dbapi.c +++ b/devsupApp/src/dbapi.c @@ -269,7 +269,11 @@ static struct PyModuleDef dbapimodule = { }; #endif +#if PY_MAJOR_VERSION >= 3 +PyMODINIT_FUNC PyInit__dbapi(void) +#else PyMODINIT_FUNC init_dbapi(void) +#endif { PyObject *mod = NULL, *hookdict, *vertup; pystate *st; diff --git a/devsupApp/src/devsup/__init__.py b/devsupApp/src/devsup/__init__.py index ea6c48b..a84fcf8 100644 --- a/devsupApp/src/devsup/__init__.py +++ b/devsupApp/src/devsup/__init__.py @@ -52,7 +52,7 @@ 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() diff --git a/devsupApp/src/devsup/test/test_db.py b/devsupApp/src/devsup/test/test_db.py index f543439..811292a 100644 --- a/devsupApp/src/devsup/test/test_db.py +++ b/devsupApp/src/devsup/test/test_db.py @@ -23,7 +23,7 @@ class IOCHelper(unittest.TestCase): if self.db is not None: with tempfile.NamedTemporaryFile() as F: - F.write(self.db) + F.write(self.db.encode('ascii')) F.flush() _dbapi.dbReadDatabase(F.name) @@ -159,7 +159,8 @@ class TestField(IOCHelper): rec.VAL = ["zero", "", "one", "This is a really long string which should be truncated", "", "last"] - assert_array_equal(rec.VAL, ["zero", "", "one", "This is a really long string which shoul", "", "last"]) + assert_array_equal(rec.VAL, + numpy.asarray(["zero", "", "one", "This is a really long string which shoul", "", "last"], dtype='S40')) class TestDset(IOCHelper): From 4b05477417fec18ed1956cd5a4bd5a50aa2c2f2e Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 4 Nov 2018 15:48:20 -0800 Subject: [PATCH 08/14] Base 3.14 --- .travis.yml | 4 ++-- devsupApp/src/pydevsup.h | 6 ++++-- devsupApp/src/utest.c | 21 ++++++++++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index d32fd56..58fcdd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,8 +26,8 @@ matrix: - python: "3.6" env: BRBASE=7.0 PROF=deb9 TEST=YES - python: "2.7" - env: BRBASE=3.16 PROF=deb8 + env: BRBASE=3.16 PROF=deb8 TEST=YES - python: "2.7" - env: BRBASE=3.15 PROF=deb8 + env: BRBASE=3.15 PROF=deb8 TEST=YES - python: "2.7" env: BRBASE=3.14 PROF=deb8 diff --git a/devsupApp/src/pydevsup.h b/devsupApp/src/pydevsup.h index 93e75d8..cd5e047 100644 --- a/devsupApp/src/pydevsup.h +++ b/devsupApp/src/pydevsup.h @@ -15,6 +15,8 @@ #endif +struct dbCommon; + initHookState pyInitLastState; PyObject* pyDBD_setup(PyObject *unused); @@ -26,8 +28,8 @@ int pyField_prepare(PyObject *module); int pyRecord_prepare(PyObject *module); -int isPyRecord(dbCommon *); -int canIOScanRecord(dbCommon *); +int isPyRecord(struct dbCommon *); +int canIOScanRecord(struct dbCommon *); extern epicsThreadPrivateId pyDevReasonID; diff --git a/devsupApp/src/utest.c b/devsupApp/src/utest.c index 344c5bb..95eeb1d 100644 --- a/devsupApp/src/utest.c +++ b/devsupApp/src/utest.c @@ -5,14 +5,21 @@ #include -#include +#include #include #include #include +#if EPICS_VERSION>3 || (EPICS_VERSION==3 && EPICS_REVISION>=15) +# include +# define HAVE_DBTEST +#endif + #include "pydevsup.h" +#ifdef HAVE_DBTEST static dbEventCtx testEvtCtx; +#endif typedef struct { PyObject_HEAD @@ -39,6 +46,7 @@ static PyObject* utest_prepare(PyObject *unused) static PyObject* utest_init(PyObject *unused) { +#ifdef HAVE_DBTEST int ret; if(testEvtCtx) return PyErr_Format(PyExc_RuntimeError, "Missing testIocShutdownOk()"); @@ -71,10 +79,14 @@ static PyObject* utest_init(PyObject *unused) 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); @@ -82,15 +94,22 @@ static PyObject* utest_shutdown(PyObject *unused) 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[] = { From dcf81c37442746f1285c453d8b75afce002d9eff Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 4 Nov 2018 16:25:24 -0800 Subject: [PATCH 09/14] update doc --- devsupApp/src/Makefile | 1 - devsupApp/src/dbfield.c | 17 +-- devsupApp/src/dbrec.c | 19 ++- devsupApp/src/devsup/_nullapi.py | 205 ------------------------------- devsupApp/src/devsup/db.py | 70 +++++++++++ devsupApp/src/devsup/hooks.py | 2 +- documentation/environment.rst | 2 +- 7 files changed, 96 insertions(+), 220 deletions(-) delete mode 100644 devsupApp/src/devsup/_nullapi.py diff --git a/devsupApp/src/Makefile b/devsupApp/src/Makefile index 6e324b1..2d01d17 100644 --- a/devsupApp/src/Makefile +++ b/devsupApp/src/Makefile @@ -31,7 +31,6 @@ _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 diff --git a/devsupApp/src/dbfield.c b/devsupApp/src/dbfield.c index ed48687..42f7fd9 100644 --- a/devsupApp/src/dbfield.c +++ b/devsupApp/src/dbfield.c @@ -445,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} }; diff --git a/devsupApp/src/dbrec.c b/devsupApp/src/dbrec.c index aa8a932..e85b5e4 100644 --- a/devsupApp/src/dbrec.c +++ b/devsupApp/src/dbrec.c @@ -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, diff --git a/devsupApp/src/devsup/_nullapi.py b/devsupApp/src/devsup/_nullapi.py deleted file mode 100644 index 9d5fda6..0000000 --- a/devsupApp/src/devsup/_nullapi.py +++ /dev/null @@ -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 ` (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 ` 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 ` 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 ` 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 `). - """ - - 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 `). - """ - - 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 = {} diff --git a/devsupApp/src/devsup/db.py b/devsupApp/src/devsup/db.py index 8b29498..996df5b 100644 --- a/devsupApp/src/devsup/db.py +++ b/devsupApp/src/devsup/db.py @@ -260,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 ` (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 ` 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 ` 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 ` 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.asyncStart(self, reason=reason) + def __getattr__(self, name): try: F = self.field(name) diff --git a/devsupApp/src/devsup/hooks.py b/devsupApp/src/devsup/hooks.py index 9ac261a..c9a4a6e 100644 --- a/devsupApp/src/devsup/hooks.py +++ b/devsupApp/src/devsup/hooks.py @@ -42,7 +42,7 @@ def initHook(state): @initHook("AfterIocRunning") def myfn(): - # do stuff + pass """ def _add(fn): addHook(state, fn) diff --git a/documentation/environment.rst b/documentation/environment.rst index 6384c06..a33677b 100644 --- a/documentation/environment.rst +++ b/documentation/environment.rst @@ -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 :: From 6b8d7ff7c56c0b717ec0cdf0197c18df776b867c Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 4 Nov 2018 16:30:39 -0800 Subject: [PATCH 10/14] minor --- devsupApp/src/devsup/__init__.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/devsupApp/src/devsup/__init__.py b/devsupApp/src/devsup/__init__.py index a84fcf8..b345cde 100644 --- a/devsupApp/src/devsup/__init__.py +++ b/devsupApp/src/devsup/__init__.py @@ -16,8 +16,31 @@ from ._dbapi import (EPICS_VERSION_STRING, XEPICS_BASE, epicsver, pydevver, - INVALID_ALARM, + 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__ = [] From 574ca20bdb67a52694dd087f1cf974204e352928 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 4 Nov 2018 16:47:23 -0800 Subject: [PATCH 11/14] cleanup and inithook --- devsupApp/src/dbapi.c | 18 +++++++++++++++++- devsupApp/src/devsup/__init__.py | 2 ++ devsupApp/src/devsup/db.py | 2 +- devsupApp/src/devsup/test/test_db.py | 1 + pyIocApp/setup.c | 5 ----- testApp/test3.py | 6 +----- 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/devsupApp/src/dbapi.c b/devsupApp/src/dbapi.c index bff2e7f..8cd3c0c 100644 --- a/devsupApp/src/dbapi.c +++ b/devsupApp/src/dbapi.c @@ -152,6 +152,21 @@ 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) { @@ -237,7 +252,6 @@ PyObject *py_iocInit(PyObject *unused, PyObject *args, PyObject *kws) static PyObject *py_pyDevSupCommon(PyObject *unused) { - static int once; Py_BEGIN_ALLOW_THREADS { pyDevSupCommon_registerRecordDeviceDriver(pdbbase); } Py_END_ALLOW_THREADS @@ -246,6 +260,8 @@ PyObject *py_pyDevSupCommon(PyObject *unused) } 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, diff --git a/devsupApp/src/devsup/__init__.py b/devsupApp/src/devsup/__init__.py index b345cde..dd8da5a 100644 --- a/devsupApp/src/devsup/__init__.py +++ b/devsupApp/src/devsup/__init__.py @@ -81,4 +81,6 @@ device(aao, INST_IO, pydevsupComOut, "Python Device") _dbapi._dbd_setup() def _fini(iocMain=False): + if iocMain: + _dbapi.initHookAnnounce(9999) # our magic/fake AtExit hook _dbapi._dbd_cleanup() diff --git a/devsupApp/src/devsup/db.py b/devsupApp/src/devsup/db.py index 996df5b..b0a5605 100644 --- a/devsupApp/src/devsup/db.py +++ b/devsupApp/src/devsup/db.py @@ -328,7 +328,7 @@ class Record(_dbapi._Record): threading.Timer(1.0, record.asyncFinish, kwargs={'reason':AsyncDone}) record.asyncStart() """ - return _dbapi._Record.asyncStart(self, reason=reason) + return _dbapi._Record.asyncFinish(self, reason=reason) def __getattr__(self, name): try: diff --git a/devsupApp/src/devsup/test/test_db.py b/devsupApp/src/devsup/test/test_db.py index 811292a..0ab6247 100644 --- a/devsupApp/src/devsup/test/test_db.py +++ b/devsupApp/src/devsup/test/test_db.py @@ -33,6 +33,7 @@ class IOCHelper(unittest.TestCase): def tearDown(self): self.iocShutdown(); print("testdbCleanup()") + _dbapi.initHookAnnounce(9999) # our magic/fake AtExit hook _dbapi._UTest.testdbCleanup() def iocInit(self): diff --git a/pyIocApp/setup.c b/pyIocApp/setup.c index b5197fa..6d02940 100644 --- a/pyIocApp/setup.c +++ b/pyIocApp/setup.c @@ -30,9 +30,6 @@ static void cleanupPy(void *junk) PyEval_RestoreThread(state); - /* special "fake" hook for shutdown */ - //pyhook((initHookState)9999); - if(PyRun_SimpleString("import devsup\n" "devsup._fini(iocMain=True)\n" )) { @@ -117,8 +114,6 @@ static void setupPyPath(void) static void pySetupReg(void) { - PyGILState_STATE state; - Py_Initialize(); PyEval_InitThreads(); diff --git a/testApp/test3.py b/testApp/test3.py index fbe1cca..2b275ca 100644 --- a/testApp/test3.py +++ b/testApp/test3.py @@ -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) From 93a4d05866a151b1192fd359e94013b9530391e3 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Sun, 4 Nov 2018 17:05:03 -0800 Subject: [PATCH 12/14] testing cleanup --- devsupApp/src/Makefile | 1 + devsupApp/src/devsup/test/test_db.py | 41 ++------------- devsupApp/src/devsup/test/util.py | 74 ++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 38 deletions(-) create mode 100644 devsupApp/src/devsup/test/util.py diff --git a/devsupApp/src/Makefile b/devsupApp/src/Makefile index 2d01d17..3a26547 100644 --- a/devsupApp/src/Makefile +++ b/devsupApp/src/Makefile @@ -40,6 +40,7 @@ PY += devsup/disect.py PY += devsup/ptable.py PY += devsup/test/__init__.py +PY += devsup/test/util.py PY += devsup/test/test_db.py #=========================== diff --git a/devsupApp/src/devsup/test/test_db.py b/devsupApp/src/devsup/test/test_db.py index 0ab6247..2dc33e0 100644 --- a/devsupApp/src/devsup/test/test_db.py +++ b/devsupApp/src/devsup/test/test_db.py @@ -10,43 +10,10 @@ from ..db import getRecord from .. import _dbapi from .. import _init -# short-circuit warning from _dbapi._init() -os.environ['TOP'] = _dbapi.XPYDEV_BASE +from .util import IOCHelper -class IOCHelper(unittest.TestCase): - db = None - autostart = 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 self.running: - print("testIocInitOk") - _dbapi._UTest.testIocInitOk() - self.running = True - - def iocShutdown(self): - if self.running: - print("testIocShutdownOk") - _dbapi._UTest.testIocShutdownOk() - self.running = False +# short-circuit warning from base_registerRecordDeviceDriver() +os.environ['TOP'] = _dbapi.XPYDEV_BASE # external code use devsup.XPYDEV_BASE class TestScan(IOCHelper): db = """ @@ -90,7 +57,6 @@ class TestField(IOCHelper): field(NELM, "10") } """ - autostart = True def test_ai(self): rec = getRecord("rec:ai") @@ -171,7 +137,6 @@ class TestDset(IOCHelper): field(INP , "@devsup.test.test_db|TestDset foo bar") } """ - autostart = True class Increment(object): def process(self, rec, reason): diff --git a/devsupApp/src/devsup/test/util.py b/devsupApp/src/devsup/test/util.py new file mode 100644 index 0000000..6177a4b --- /dev/null +++ b/devsupApp/src/devsup/test/util.py @@ -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 From 6e3c3d6aed033a417e289f0a71a5184984b0c7c3 Mon Sep 17 00:00:00 2001 From: Bruno Martins Date: Tue, 12 Mar 2019 11:54:13 -0400 Subject: [PATCH 13/14] Fix pyIocApp's Makefile `PYDIR` was being set to the wrong value --- pyIocApp/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyIocApp/Makefile b/pyIocApp/Makefile index 8e41d4d..cab887f 100644 --- a/pyIocApp/Makefile +++ b/pyIocApp/Makefile @@ -25,7 +25,7 @@ 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_VER)\" +setup_CPPFLAGS += -DPYDIR=\"python$(PY_LD_VER)\" pyDevSup$(PY_LD_VER)_SRCS += setup.c From 15dfe2aa92d7265ea4b9aedc531fb4a3065d8b7e Mon Sep 17 00:00:00 2001 From: Bruno Martins Date: Tue, 12 Mar 2019 12:03:22 -0400 Subject: [PATCH 14/14] Python init: don't install sig handlers. Fix #5 Py_InitializeEx(0) tells the embedded interpreter to not install signal handlers. This fix is ignored by Python <3.7. See https://bugs.python.org/issue35233. --- pyIocApp/setup.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyIocApp/setup.c b/pyIocApp/setup.c index 6d02940..8a579cd 100644 --- a/pyIocApp/setup.c +++ b/pyIocApp/setup.c @@ -114,7 +114,7 @@ static void setupPyPath(void) static void pySetupReg(void) { - Py_Initialize(); + Py_InitializeEx(0); PyEval_InitThreads(); setupPyPath();