diff --git a/.ci b/.ci index 1e0e326..0e93b70 160000 --- a/.ci +++ b/.ci @@ -1 +1 @@ -Subproject commit 1e0e326f74ffac4154ce80b5d41c410c754cf5d8 +Subproject commit 0e93b708551cb7bb212fda7029eccdf872dabc81 diff --git a/.github/workflows/ci-scripts-build.yml b/.github/workflows/ci-scripts-build.yml index c400628..54e7a1b 100644 --- a/.github/workflows/ci-scripts-build.yml +++ b/.github/workflows/ci-scripts-build.yml @@ -1,7 +1,7 @@ name: pyDevSup # Trigger on pushes and PRs to any branch -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] env: SETUP_PATH: .ci-local:.ci @@ -11,8 +11,9 @@ env: jobs: build-base: - name: ${{ matrix.base }}/${{ matrix.os }}/${{ matrix.python }}/${{ matrix.extra }} + name: ${{ matrix.base }}/${{ matrix.os }}/${{ matrix.profile }}/${{ matrix.python }}/${{ matrix.extra }} runs-on: ${{ matrix.os }} + container: ${{ matrix.container }} # Set environment variables from matrix parameters env: CMP: ${{ matrix.cmp }} @@ -31,16 +32,42 @@ jobs: cmp: gcc configuration: default base: "7.0" - python: "2.7" - profile: deb8 + python: "3.7" + profile: deb10 + container: "python:3.7" + test: yes + + - os: ubuntu-latest + cmp: gcc + configuration: default + base: "7.0" + python: "3.9" + profile: deb11 + test: yes + + - os: ubuntu-latest + cmp: gcc + configuration: default + base: "7.0" + python: "3.11" + profile: deb12 + test: yes + + - os: ubuntu-latest + cmp: gcc + configuration: default + base: "7.0" + python: "3.13" + profile: deb13 test: yes - os: ubuntu-latest cmp: gcc configuration: default base: "7.0" - python: "2.7" - profile: deb8 + python: "3.6" + container: "python:3.6" + profile: latest test: yes - os: ubuntu-latest @@ -48,7 +75,8 @@ jobs: configuration: default base: "7.0" python: "3.7" - profile: deb10 + container: "python:3.7" + profile: latest test: yes - os: ubuntu-latest @@ -75,17 +103,33 @@ jobs: profile: latest test: yes - - os: macos-latest + - os: ubuntu-latest cmp: gcc configuration: default base: "7.0" - python: "3.10" + python: "3.11" profile: latest test: yes - - os: windows-latest - cmp: vs2019 - configuration: debug + - os: ubuntu-latest + cmp: gcc + configuration: default + base: "7.0" + python: "3.12" + profile: latest + test: yes + + - os: ubuntu-latest + cmp: gcc + configuration: default + base: "7.0" + python: "3.13" + profile: latest + test: yes + + - os: macos-latest + cmp: gcc + configuration: default base: "7.0" python: "3.10" profile: latest @@ -95,22 +139,66 @@ jobs: cmp: gcc configuration: default base: "3.15" - python: "2.7" - profile: deb8 + python: "3.7" + container: "python:3.7" + profile: deb10 test: yes - os: ubuntu-latest cmp: gcc configuration: default - base: "3.14" - python: "2.7" - profile: deb8 + base: "3.15" + python: "3.13" + profile: deb13 + test: yes + + #- os: ubuntu-latest + # cmp: gcc + # configuration: default + # base: "3.14" # RuntimeError: Requires Base >=3.15 ?? + # python: "3.7" + # container: "python:3.7" + # profile: deb10 + # test: yes + + - os: windows-latest + cmp: vs2022 + configuration: default + base: "3.15" + python: "3.11" + profile: latest + test: yes + + - os: windows-latest + cmp: vs2022 + configuration: default + base: "7.0" + python: "3.11" + profile: latest + test: yes + + - os: windows-latest + cmp: vs2022 + configuration: default + base: "7.0" + python: "3.12" + profile: latest + test: yes + + - os: windows-latest + cmp: vs2022 + configuration: default + base: "7.0" + python: "3.13" + profile: latest + test: yes steps: - uses: actions/checkout@v3 with: submodules: true - name: Set up Python ${{ matrix.python }} + if: ${{ !matrix.container }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} @@ -125,13 +213,8 @@ jobs: EOF echo === configure/CONFIG_SITE.local === cat configure/CONFIG_SITE.local - - name: makehelper.py - run: python makehelper.py - - name: DSOs - shell: bash - run: find ${Python_ROOT_DIR} -name '*.dll' -o -name '*.so' -o -name '*.dylib' - - name: get_config_vars - run: python -c 'from sysconfig import get_config_vars; from pprint import pprint; pprint(get_config_vars())' + - name: Python sysconfig + run: python -m sysconfig - name: Prepare and compile dependencies run: python .ci/cue.py prepare - name: Build main module diff --git a/README.md b/README.md index 7251f5a..2fecd12 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,6 @@ pyDevSup EPICS Device support in Python. -See [documentation](http://mdavidsaver.github.io/pyDevSup) +See [documentation](http://epics-modules.github.io/pyDevSup) -For file [releases](https://github.com/mdavidsaver/pyDevSup/releases) +For file [releases](https://github.com/epics-modules/pyDevSup/releases) diff --git a/configure/CONFIG_PY b/configure/CONFIG_PY index f2edef8..0eeb5f3 100644 --- a/configure/CONFIG_PY +++ b/configure/CONFIG_PY @@ -48,6 +48,10 @@ LOADABLE_SHRLIB_LDFLAGS = -bundle -flat_namespace -undefined dynamic_lookup LOADABLE_SHRLIB_SUFFIX = .so endif +ifeq ($(OS_CLASS),WIN32) +SHRLIB_SUFFIX_BASE = .pyd +endif + endif endif diff --git a/configure/RULES_PY b/configure/RULES_PY index c635d51..9f15b01 100644 --- a/configure/RULES_PY +++ b/configure/RULES_PY @@ -1,15 +1,15 @@ ifneq ($(T_A),) - ifndef PY_VER $(error Must include CONFIG_PY) endif +include $(CONFIG)/CONFIG_BASE + PY_FILES += $(PY:%=$(PY_INSTALL_DIR)/%) $(PY_FILES) : $(PY_INSTALL_DIR)/%: ../% - @[ -d $(dir $@) ] || install -d $(dir $@) - @echo "Install PY $@" - install -m 644 $< $@ + $(ECHO) "Install PY $@" + @$(INSTALL) -d -m $(INSTALL_PERMISSIONS) $< $(@D) build: $(PY_FILES) diff --git a/devsupApp/src/Makefile b/devsupApp/src/Makefile index 4968e84..53500fd 100644 --- a/devsupApp/src/Makefile +++ b/devsupApp/src/Makefile @@ -29,10 +29,13 @@ _dbapi_SRCS += utest.c _dbapi_SRCS += pyDevSupCommon_registerRecordDeviceDriver.cpp _dbapi_LIBS += $(EPICS_BASE_IOC_LIBS) -_dbapi_SYS_LIBS_WIN32 += python$(PY_LD_VER) +_dbapi_LDFLAGS_WIN32 += $(PY_LDLIBS) _dbapi_SYS_LIBS_WIN32 += ws2_32 Advapi32 Dbghelp PY += devsup/__init__.py +PY += devsup/_dbapi.dbd +PY += devsup/_int64.dbd +PY += devsup/_lsilso.dbd PY += devsup/db.py PY += devsup/dset.py PY += devsup/hooks.py @@ -63,7 +66,11 @@ pyconfig: ifneq (,$(T_A)) nose: +ifeq ($(OS),Windows_NT) + pushd "$(abspath $(TOP))/python$(PY_LD_VER)/$(EPICS_HOST_ARCH)" && $(PYTHON) -m nose2 -v devsup $(NOSEFLAGS) && popd +else PYTHONPATH="${PYTHONPATH}:$(abspath $(TOP))/python$(PY_LD_VER)/$(EPICS_HOST_ARCH)" $(PYTHON) -m nose2 -v devsup $(NOSEFLAGS) +endif # bounce back down to the sphinx generated Makefile # aren't Makefiles fun... diff --git a/devsupApp/src/dbfield.c b/devsupApp/src/dbfield.c index 4a03cba..ee17c6c 100644 --- a/devsupApp/src/dbfield.c +++ b/devsupApp/src/dbfield.c @@ -40,6 +40,10 @@ static const int dbf2np_map[DBF_MENU+1] = { NPY_INT16, // DBF_MENU }; static PyArray_Descr* dbf2np[DBF_MENU+1]; +#if NPY_ABI_VERSION < 0x02000000 + #define PyDataType_ELSIZE(descr) ((descr)->elsize) + #define PyDataType_SET_ELSIZE(descr, size) (descr)->elsize = size +#endif #endif typedef struct { @@ -98,7 +102,7 @@ static PyObject* build_array(PyObject* obj, void *data, unsigned short ftype, un desc = dbf2np[ftype]; if(ftype==DBF_STRING) { - desc->elsize = MAX_STRING_SIZE; + PyDataType_SET_ELSIZE(desc, MAX_STRING_SIZE); } Py_XINCREF(desc); @@ -114,23 +118,26 @@ static int assign_array(DBADDR *paddr, PyObject *arr) #ifdef HAVE_NUMPY void *rawfield = paddr->pfield; rset *prset; - PyObject *aval; + PyArrayObject *aval; + PyArrayObject * array = (PyArrayObject *)arr; unsigned elemsize = dbValueSize(paddr->field_type); unsigned long maxlen = paddr->no_elements, insize; PyArray_Descr *desc = dbf2np[paddr->field_type]; if(paddr->field_type==DBF_STRING && - (PyArray_NDIM(arr)!=2 || PyArray_DIM(arr,0)>maxlen || PyArray_DIM(arr,1)!=MAX_STRING_SIZE)) + (PyArray_NDIM(array) != 2 || + PyArray_DIM(array, 0) > (npy_intp) maxlen || + PyArray_DIM(array, 1) != MAX_STRING_SIZE)) { PyErr_Format(PyExc_ValueError, "String array has incorrect shape or is too large"); return 1; - } else if(PyArray_NDIM(arr)!=1 || PyArray_DIM(arr,0)>maxlen) { + } else if(PyArray_NDIM(array)!=1 || PyArray_DIM(array,0)>maxlen) { PyErr_Format(PyExc_ValueError, "Array has incorrect shape or is too large"); return 1; } - insize = PyArray_DIM(arr, 0); + insize = PyArray_DIM(array, 0); if(paddr->special==SPC_DBADDR && (prset=dbGetRset(paddr)) && @@ -152,7 +159,7 @@ static int assign_array(DBADDR *paddr, PyObject *arr) } Py_XINCREF(desc); - if(!(aval = PyArray_FromAny(arr, desc, 1, 2, NPY_CARRAY, arr))) + if(!(aval = PyArray_FromAny(arr, desc, 1, 2, NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_ALIGNED | NPY_ARRAY_WRITEABLE, arr))) return 1; if(elemsize!=PyArray_ITEMSIZE(aval)) { @@ -207,7 +214,7 @@ static PyObject* pyField_getval(pyField *self) if(self->addr.no_elements>1) { return build_array((PyObject*)self, rawfield, self->addr.field_type, - noe, NPY_CARRAY_RO); + noe, NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_ALIGNED); } } @@ -296,8 +303,8 @@ static PyObject* pyField_putval(pyField *self, PyObject* args) OP(LONG, epicsInt32, PyInt_AsLong); OP(ULONG, epicsUInt32, PyInt_AsLong); #ifdef HAVE_INT64 - OP(INT64, epicsInt32, PyLong_AsLongLong); - OP(UINT64, epicsUInt32, PyLong_AsLongLong); + OP(INT64, epicsInt64, PyLong_AsLongLong); + OP(UINT64, epicsUInt64, PyLong_AsLongLong); #endif OP(FLOAT, epicsFloat32,PyFloat_AsDouble); OP(DOUBLE,epicsFloat64,PyFloat_AsDouble); @@ -314,11 +321,14 @@ static PyObject* pyField_putval(pyField *self, PyObject* args) fld = PyString_AsString(val); #endif if(fld) { - strncpy(dest, fld, MAX_STRING_SIZE); - dest[MAX_STRING_SIZE-1]='\0'; + strncpy(dest, fld, self->addr.field_size); + dest[self->addr.field_size-1]='\0'; } else { dest[0] = '\0'; } + if ((self->addr.special == SPC_MOD) || (self->addr.special == SPC_NOMOD)) + if (prset = dbGetRset(&self->addr)) + prset->special(&self->addr, 1); #if PY_MAJOR_VERSION >= 3 Py_DECREF(data); #endif @@ -362,7 +372,7 @@ static PyObject *pyField_getarray(pyField *self) } else data = self->addr.pfield; - return build_array((PyObject*)self, data, self->addr.field_type, self->addr.no_elements, NPY_CARRAY); + return build_array((PyObject*)self, data, self->addr.field_type, self->addr.no_elements, NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_ALIGNED | NPY_ARRAY_WRITEABLE); } static PyObject *pyField_getlen(pyField *self) diff --git a/devsupApp/src/dbrec.c b/devsupApp/src/dbrec.c index 7802fe5..6c8584a 100644 --- a/devsupApp/src/dbrec.c +++ b/devsupApp/src/dbrec.c @@ -2,9 +2,9 @@ /* python has its own ideas about which version to support */ #undef _POSIX_C_SOURCE #undef _XOPEN_SOURCE - +/* See https://stackoverflow.com/questions/70705404/systemerror-py-ssize-t-clean-macro-must-be-defined-for-formats */ +#define PY_SSIZE_T_CLEAN #include - #include #include #include @@ -125,20 +125,27 @@ static PyObject* pyRecord_setSevr(pyRecord *self, PyObject *args, PyObject *kws) { dbCommon *prec = self->entry.precnode->precord; - static char* names[] = {"sevr", "stat", NULL}; + static char* names[] = {"sevr", "stat", "amsg", NULL}; short sevr = INVALID_ALARM, stat=COMM_ALARM; + char* amsg = ""; + Py_ssize_t lMsg; - if(!PyArg_ParseTupleAndKeywords(args, kws, "|hh", names, &sevr, &stat)) + /* see https://colinpaice.blog/2022/06/28/using-pyarg_parsetupleandkeywords-to-parse-data-in-python-external-functions/ */ + if(!PyArg_ParseTupleAndKeywords(args, kws, "|hhs#", names, &sevr, &stat, &amsg, &lMsg)) return NULL; if(sevrlastEpicsAlarmSev || statlastEpicsAlarmCond) { - PyErr_Format(PyExc_ValueError, "%s: Can't set alarms %d %d", prec->name, sevr, stat); + PyErr_Format(PyExc_ValueError, "%s: Can't set alarms %d %d %s", prec->name, sevr, stat, amsg); return NULL; } +#if EPICS_VERSION_INT {'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" + "setSevr(sevr=INVALID_ALARM, stat=COMM_ALARM, amsg="")\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!"}, diff --git a/devsupApp/src/devsup/__init__.py b/devsupApp/src/devsup/__init__.py index dd8da5a..2ce3ba3 100644 --- a/devsupApp/src/devsup/__init__.py +++ b/devsupApp/src/devsup/__init__.py @@ -1,7 +1,19 @@ import os +import sys import atexit import tempfile +if sys.platform == 'win32': + # See https://stackoverflow.com/questions/72858093/how-to-specify-pyd-dll-dependencies-search-paths-at-runtime-with-python + # This is required for use of e.g. nose testing, but + # not when running as an IOC, since the IOC will already have loaded EPICS base DLLs. + epics_base = os.getenv('EPICS_BASE') + epics_host_arch = os.getenv('EPICS_HOST_ARCH') + if epics_base is not None and epics_host_arch is not None: + epics_base = epics_base.replace("/",'\\') + dll_path = epics_base + "\\bin\\" + epics_host_arch + os.add_dll_directory(dll_path) + from . import _dbapi from ._dbapi import (EPICS_VERSION_STRING, @@ -45,39 +57,28 @@ __all__ = [] + +def epics_version_int(): + return (EPICS_VERSION, EPICS_REVISION, EPICS_MODIFICATION, EPICS_PATCH_LEVEL) + 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) + + dirname = os.path.dirname(__file__) + dbd_name = dirname + "/_dbapi.dbd" + _dbapi.dbReadDatabase(dbd_name) + if epics_version_int() >= (3, 15, 0, 2): + # Long strings are impletemented. + dbd_name = dirname + "/_lsilso.dbd" + _dbapi.dbReadDatabase(dbd_name) + if epics_version_int() >= (3, 16, 1, 0): + # Long ints are impletemented. + dbd_name = dirname + "/_int64.dbd" + _dbapi.dbReadDatabase(dbd_name) _dbapi._dbd_setup() def _fini(iocMain=False): diff --git a/devsupApp/src/devsup/_dbapi.dbd b/devsupApp/src/devsup/_dbapi.dbd new file mode 100644 index 0000000..0d791aa --- /dev/null +++ b/devsupApp/src/devsup/_dbapi.dbd @@ -0,0 +1,22 @@ + +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") diff --git a/devsupApp/src/devsup/_int64.dbd b/devsupApp/src/devsup/_int64.dbd new file mode 100644 index 0000000..120fb64 --- /dev/null +++ b/devsupApp/src/devsup/_int64.dbd @@ -0,0 +1,2 @@ +device(int64in, INST_IO, pydevsupComIn, "Python Device") +device(int64out, INST_IO, pydevsupComOut, "Python Device") diff --git a/devsupApp/src/devsup/_lsilso.dbd b/devsupApp/src/devsup/_lsilso.dbd new file mode 100644 index 0000000..761ef5b --- /dev/null +++ b/devsupApp/src/devsup/_lsilso.dbd @@ -0,0 +1,2 @@ +device(lsi, INST_IO, pydevsupComIn, "Python Device") +device(lso, INST_IO, pydevsupComOut, "Python Device") \ No newline at end of file diff --git a/devsupApp/src/devsup/disect.py b/devsupApp/src/devsup/disect.py index d166f74..b826361 100644 --- a/devsupApp/src/devsup/disect.py +++ b/devsupApp/src/devsup/disect.py @@ -4,6 +4,7 @@ import sys, gc, inspect, time try: + InstanceType = None from types import InstanceType except ImportError: pass # py3 diff --git a/devsupApp/src/devsup/ptable.py b/devsupApp/src/devsup/ptable.py index 365cb91..b575c08 100644 --- a/devsupApp/src/devsup/ptable.py +++ b/devsupApp/src/devsup/ptable.py @@ -10,7 +10,7 @@ _tables = {} from .db import IOScanListThread -from . import INVALID_ALARM, UDF_ALARM +from . import INVALID_ALARM, UDF_ALARM, NO_ALARM __all__ = [ 'Parameter', @@ -161,6 +161,8 @@ def __init__(self, table, name, scan): self.name = name self.table, self.scan, self._value = table, scan, None self.alarm, self.actions = 0, [] + self.stat = NO_ALARM + self.amsg = "" self._groups = set() def _get_value(self): return self._value @@ -232,19 +234,19 @@ def process(self, rec, reason=None): """Read a value from the table into the record """ with self.inst.table.lock: - nval, alrm = self.inst.value, self.inst.alarm - self.inst.table.log.debug('%s -> %s (%s)', self.inst.name, rec.NAME, nval) + value, alarm, stat, msg = self.inst.value, self.inst.alarm, self.inst.stat, self.inst.amsg + self.inst.table.log.debug('%s -> %s (%s)', self.inst.name, rec.NAME, value) - if nval is not None: + if value is not None: if self.vdata is None: - self.vfld.putval(nval) + self.vfld.putval(value) else: - if len(nval)>len(self.vdata): + if len(value)>len(self.vdata): nval = nval[:len(self.vdata)] - self.vdata[:len(nval)] = nval - self.vfld.putarraylen(len(nval)) - if alrm: - rec.setSevr(alrm) + self.vdata[:len(value)] = value + self.vfld.putarraylen(len(value)) + if alarm: + rec.setSevr(alarm, stat, msg) else: # undefined value rec.setSevr(INVALID_ALARM, UDF_ALARM) @@ -271,8 +273,9 @@ def process(self, rec, reason=None): # Execute actions self.inst._exec(oval) - for G in self.inst._groups: - G._exec() + rec.setSevr(self.inst.alarm, self.inst.stat, self.inst.amsg) + for G in self.inst._groups: + G._exec() class TableBase(object): """Base class for all parameter tables. diff --git a/devsupApp/src/devsup/test/test_db.py b/devsupApp/src/devsup/test/test_db.py index 2dc33e0..cc8ec89 100644 --- a/devsupApp/src/devsup/test/test_db.py +++ b/devsupApp/src/devsup/test/test_db.py @@ -2,13 +2,14 @@ import os import unittest import tempfile +import time import numpy from numpy.testing import assert_array_almost_equal, assert_array_equal from ..db import getRecord from .. import _dbapi -from .. import _init +from .. import _init, epics_version_int from .util import IOCHelper @@ -129,6 +130,87 @@ def test_wf_string(self): assert_array_equal(rec.VAL, numpy.asarray(["zero", "", "one", "This is a really long string which shoul", "", "last"], dtype='S40')) +class TestLongStringField(IOCHelper): + db = """ + record(lsi, "rec:lsi") { + field(SIZV, 128) + field(SCAN, "I/O Intr") + } + record(lso, "rec:lso") { + field(SIZV, 128) + field(DOL, "rec:lsi.VAL$") + field(OMSL, "closed_loop") + } + """ + if epics_version_int() < (3, 15, 0, 2): + # Long strings not impletemented yet. + db = None + + def test_lsilso(self): + if epics_version_int() < (3, 15, 0, 2): + # Long strings not impletemented yet. + return + + lsi = getRecord("rec:lsi") + lso = getRecord("rec:lso") + + with lsi: + self.assertEqual(lsi.VAL, "") + + lsi.VAL = "test" + self.assertEqual(lsi.VAL, "test") + + lsi.VAL = "" + self.assertEqual(lsi.VAL, "") + + # does not truncate + lsi.VAL = "This is a really long string which should NOT be truncated" + self.assertEqual(lsi.VAL, "This is a really long string which should NOT be truncated") + + lso.scan() + time.sleep(0.01) # The linked value needs a small amount of time to update. + + with lso: + self.assertEqual(lso.VAL, lsi.VAL) + +class TestInt64Field(IOCHelper): + db = """ + record(int64in, "rec:in64") { + field(SCAN, "I/O Intr") + } + record(int64out, "rec:out64") { + field(DOL, "rec:in64.VAL") + field(OMSL, "closed_loop") + } + """ + if epics_version_int() < (3, 16, 1, 0): + # Long ints not impletemented yet. + db = None + + def testint64(self): + if epics_version_int() < (3, 16, 1, 0): + # Long ints not impletemented yet. + return + in64 = getRecord("rec:in64") + out64 = getRecord("rec:out64") + + with in64: + self.assertEqual(in64.VAL, 0) + + in64.VAL = 42 + self.assertEqual(in64.VAL, 42) + + in64.VAL = 0x7FFFFFFFFFFFFFFE + self.assertEqual(in64.VAL, 0x7FFFFFFFFFFFFFFE) + + in64.VAL = 0x7FFFFFFFFFFFFFFF + self.assertEqual(in64.VAL, 0x7FFFFFFFFFFFFFFF) + + out64.scan() + time.sleep(0.01) # The linked value needs a small amount of time to update. + + with out64: + self.assertEqual(out64.VAL, in64.VAL) class TestDset(IOCHelper): db = """ diff --git a/devsupApp/src/devsup/test/util.py b/devsupApp/src/devsup/test/util.py index 6177a4b..70d1ff6 100644 --- a/devsupApp/src/devsup/test/util.py +++ b/devsupApp/src/devsup/test/util.py @@ -43,10 +43,11 @@ def setUp(self): _init(iocMain=False) # load base.dbd if self.db is not None: - with tempfile.NamedTemporaryFile() as F: + with tempfile.NamedTemporaryFile(delete=False) as F: F.write(self.db.encode('ascii')) - F.flush() + F.close() _dbapi.dbReadDatabase(F.name) + os.unlink(F.name) if self.autostart: self.iocInit() diff --git a/documentation/environment.rst b/documentation/environment.rst index a33677b..076d7cb 100644 --- a/documentation/environment.rst +++ b/documentation/environment.rst @@ -4,7 +4,7 @@ Runtime Environment The pyDevSup module initializes the interpreter during the registration phase of IOC startup with the *pySetupReg* registrar function. :: - #!../../bin/linux-x86_64/softIocPy2.7 + #!../../bin/linux-x86_64/softIocPy3.6 # Interpreter not started dbLoadDatabase("../../dbd/softIocPy.dbd",0,0) softIocPy_registerRecordDeviceDriver(pdbbase) @@ -51,7 +51,7 @@ file. :: The default or preferred Python version can be specificed in *configure/CONFIG_SITE* :: - PY_VER ?= 2.7 + PY_VER ?= 3.6 The following should be added to individual EPICS Makefiles. :: @@ -64,8 +64,8 @@ The following should be added to individual EPICS Makefiles. :: This will add or amend several make variables. The ``USR_*FLAGS`` variables may be extended with appropriate flags for building python modules. The ``PY_VER`` -variable is defined with the Python version number found in install directories (eg "2.7"). -The ``PY_LD_VER`` variable is defined with the python library version number (eg "3.2mu"), +variable is defined with the Python version number found in install directories (eg "3.6"). +The ``PY_LD_VER`` variable is defined with the python library version number (eg "3.6mu"), which may be the same as ``PY_VER``. Include pyDevSup in your IOC @@ -113,11 +113,11 @@ Installing for several Python versions The recipe for building and installing the pyDevSup module for several python version side by side is :: - make PY_VER=2.6 + make PY_VER=3.6 make clean - make PY_VER=2.7 + make PY_VER=3.10 make clean - make PY_VER=3.2 + make PY_VER=3.14 make clean The ``PYTHON`` make variable can be specified if the interpreter executable diff --git a/documentation/gettingstarted.rst b/documentation/gettingstarted.rst index ef542ef..294494a 100644 --- a/documentation/gettingstarted.rst +++ b/documentation/gettingstarted.rst @@ -24,7 +24,7 @@ The :py:meth:`process ` method increments the *VAL* field Start this IOC with. :: - $ ./bin/linux-x86_64/softIocPy2.7 -d cntrec.db + $ ./bin/linux-x86_64/softIocPy3.6 -d cntrec.db Starting iocInit ... iocRun: All initialization complete diff --git a/documentation/index.rst b/documentation/index.rst index 2acf7b1..ec7ae61 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -8,7 +8,7 @@ pydevsup documentation *pyDevSup* is a means of writing EPICS device support code in Python. -It currently supports EPICS >=3.14.12 and python versions: 2.7, and >=3.2. +It currently supports EPICS >=3.15 and python versions: >=3.6 The numpy package is also required. The source can be found at http://github.com/mdavidsaver/pyDevSup diff --git a/iocBoot/iocFPM/Makefile b/iocBoot/iocFPM/Makefile index 79c4ce6..e1b9aa4 100644 --- a/iocBoot/iocFPM/Makefile +++ b/iocBoot/iocFPM/Makefile @@ -1,5 +1,5 @@ TOP = ../.. include $(TOP)/configure/CONFIG -ARCH = linux-x86_64 +ARCH = $(EPICS_HOST_ARCH) TARGETS = envPaths include $(TOP)/configure/RULES.ioc diff --git a/iocBoot/iocapplmon/Makefile b/iocBoot/iocapplmon/Makefile index 79c4ce6..e1b9aa4 100644 --- a/iocBoot/iocapplmon/Makefile +++ b/iocBoot/iocapplmon/Makefile @@ -1,5 +1,5 @@ TOP = ../.. include $(TOP)/configure/CONFIG -ARCH = linux-x86_64 +ARCH = $(EPICS_HOST_ARCH) TARGETS = envPaths include $(TOP)/configure/RULES.ioc diff --git a/iocBoot/iocapplmon/st.cmd b/iocBoot/iocapplmon/st.cmd index c931fb9..f345464 100755 --- a/iocBoot/iocapplmon/st.cmd +++ b/iocBoot/iocapplmon/st.cmd @@ -1,4 +1,4 @@ -#!../../bin/linux-x86/softIocPy2.7 +#!../../bin/linux-x86_64/softIocPy3.6 < envPaths diff --git a/iocBoot/iocarchivemon/Makefile b/iocBoot/iocarchivemon/Makefile index 79c4ce6..e1b9aa4 100644 --- a/iocBoot/iocarchivemon/Makefile +++ b/iocBoot/iocarchivemon/Makefile @@ -1,5 +1,5 @@ TOP = ../.. include $(TOP)/configure/CONFIG -ARCH = linux-x86_64 +ARCH = $(EPICS_HOST_ARCH) TARGETS = envPaths include $(TOP)/configure/RULES.ioc diff --git a/iocBoot/iocarchivemon/st.cmd b/iocBoot/iocarchivemon/st.cmd index 20fef52..632e7a6 100755 --- a/iocBoot/iocarchivemon/st.cmd +++ b/iocBoot/iocarchivemon/st.cmd @@ -1,4 +1,4 @@ -#!../../bin/linux-x86/softIocPy2.7 +#!../../bin/linux-x86_64/softIocPy3.6 < envPaths diff --git a/iocBoot/iocarchivemon/st.cmd.main b/iocBoot/iocarchivemon/st.cmd.main index df00b9c..ea10a94 100755 --- a/iocBoot/iocarchivemon/st.cmd.main +++ b/iocBoot/iocarchivemon/st.cmd.main @@ -1,4 +1,4 @@ -#!../../bin/linux-x86_64/softIocPy2.7 +#!../../bin/linux-x86_64/softIocPy3.6 < envPaths diff --git a/iocBoot/ioccaputlog/Makefile b/iocBoot/ioccaputlog/Makefile index 79c4ce6..e1b9aa4 100644 --- a/iocBoot/ioccaputlog/Makefile +++ b/iocBoot/ioccaputlog/Makefile @@ -1,5 +1,5 @@ TOP = ../.. include $(TOP)/configure/CONFIG -ARCH = linux-x86_64 +ARCH = $(EPICS_HOST_ARCH) TARGETS = envPaths include $(TOP)/configure/RULES.ioc diff --git a/iocBoot/iocweatherbnl/Makefile b/iocBoot/iocweatherbnl/Makefile index 79c4ce6..25b7be6 100644 --- a/iocBoot/iocweatherbnl/Makefile +++ b/iocBoot/iocweatherbnl/Makefile @@ -1,5 +1,5 @@ TOP = ../.. include $(TOP)/configure/CONFIG -ARCH = linux-x86_64 -TARGETS = envPaths +ARCH = $(EPICS_HOST_ARCH) +TARGETS = envPaths dllPath.bat include $(TOP)/configure/RULES.ioc diff --git a/iocBoot/iocweatherbnl/st.bat b/iocBoot/iocweatherbnl/st.bat new file mode 100644 index 0000000..0c0ae02 --- /dev/null +++ b/iocBoot/iocweatherbnl/st.bat @@ -0,0 +1,7 @@ +set HOSTNAME=localhost +set IOCNAME=weatherbnl +set TOP=../../ +set PATH=C:\Python310;C:\Windows +set PYTHONPATH=%TOP%devsupApp\src;%TOP%weatherApp +call dllPath.bat +..\..\bin\windows-x64\softIocPy310.exe st.cmd diff --git a/makehelper.py b/makehelper.py index ed0c0a0..ec25b7a 100644 --- a/makehelper.py +++ b/makehelper.py @@ -14,6 +14,7 @@ from __future__ import print_function import sys +import errno import os if len(sys.argv)<2: @@ -25,23 +26,16 @@ pass out = open(sys.argv[1], 'w') -from sysconfig import get_config_var -try: - from distutils.sysconfig import get_python_inc -except ImportError: - def get_python_inc(): - return get_config_var('INCLUDEPY') or '' +from sysconfig import get_config_var, get_path, get_python_version -incdirs = [get_python_inc()] -libdirs = [ - get_config_var('LIBDIR') or get_config_var('LIBDEST') or '', - get_config_var('BINDIR') or '', -] +incdirs = [get_path("include")] +libdir = get_config_var('LIBDIR') or get_config_var('LIBDEST') or '' have_np='NO' try: - from numpy.distutils.misc_util import get_numpy_include_dirs - incdirs = get_numpy_include_dirs()+incdirs + from numpy import get_include + numpy_dir = [get_include()] + incdirs = numpy_dir+incdirs have_np='YES' except ImportError: pass @@ -49,7 +43,7 @@ def get_python_inc(): print('TARGET_CFLAGS +=',get_config_var('BASECFLAGS') or '', file=out) print('TARGET_CXXFLAGS +=',get_config_var('BASECFLAGS') or '', file=out) -print('PY_VER :=',get_config_var('VERSION'), file=out) +print('PY_VER :=',get_python_version(), file=out) ldver = get_config_var('LDVERSION') if ldver is None: ldver = get_config_var('VERSION') @@ -57,8 +51,9 @@ def get_python_inc(): 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('PY_LDLIBS :=', get_config_var('BLDLIBRARY') or '', file=out) +print('PY_LIBDIRS :=',libdir, file=out) +if sys.platform == 'win32': + print('PY_LDLIBS :=', '-LIBPATH:' + os.path.join(sys.prefix, 'libs'), file=out) print('HAVE_NUMPY :=',have_np, file=out) try: @@ -77,4 +72,4 @@ def get_python_inc(): print('PY_OK := YES', file=out) -out.close() +out.close() \ No newline at end of file diff --git a/pyIocApp/Makefile b/pyIocApp/Makefile index 198e833..dc5a9e3 100644 --- a/pyIocApp/Makefile +++ b/pyIocApp/Makefile @@ -19,6 +19,7 @@ SHRLIB_VERSION = 0 DBD += pyDevSup.dbd pyDevSup$(PY_LD_VER)_SYS_LIBS += python$(PY_LD_VER) +pyDevSup$(PY_LD_VER)_LDFLAGS += $(PY_LDLIBS) pyDevSup$(PY_LD_VER)_LIBS += $(EPICS_BASE_IOC_LIBS) @@ -43,6 +44,7 @@ softIocPy_DBD += system.dbd softIocPy$(PY_VER)_LIBS += pyDevSup$(PY_LD_VER) softIocPy$(PY_VER)_SYS_LIBS += python$(PY_LD_VER) +softIocPy$(PY_VER)_LDFLAGS += $(PY_LDLIBS) # softIocPy_registerRecordDeviceDriver.cpp derives from softIocPy.dbd softIocPy$(PY_VER)_SRCS += softIocPy_registerRecordDeviceDriver.cpp diff --git a/pyIocApp/setup.c b/pyIocApp/setup.c index 61c3861..fa29ed7 100644 --- a/pyIocApp/setup.c +++ b/pyIocApp/setup.c @@ -23,6 +23,9 @@ #include #include "pydevsup.h" +#ifdef _WIN32 +#define PATH_MAX _MAX_PATH +#endif static void cleanupPy(void *junk) { @@ -130,8 +133,10 @@ static void cleanupPrep(initHookState state) static void pySetupReg(void) { Py_InitializeEx(0); +#if NPY_TARGET_VERSION < NPY_1_9_API_VERSION + /* See https://docs.python.org/3/whatsnew/3.9.html */ PyEval_InitThreads(); - +#endif setupPyPath(); if(PyRun_SimpleString("import devsup\n" diff --git a/requirements-deb10.txt b/requirements-deb10.txt index 91aeff2..039a17d 100644 --- a/requirements-deb10.txt +++ b/requirements-deb10.txt @@ -1,3 +1,2 @@ numpy==1.16.2 -#nose==1.3.7 nose2 diff --git a/requirements-deb11.txt b/requirements-deb11.txt new file mode 100644 index 0000000..bc9b6d9 --- /dev/null +++ b/requirements-deb11.txt @@ -0,0 +1,2 @@ +numpy==1.19.5 +nose2==0.9.2 diff --git a/requirements-deb12.txt b/requirements-deb12.txt new file mode 100644 index 0000000..13118a8 --- /dev/null +++ b/requirements-deb12.txt @@ -0,0 +1,2 @@ +numpy==1.24.2 +nose2==0.12.0 diff --git a/requirements-deb13.txt b/requirements-deb13.txt new file mode 100644 index 0000000..abb51c7 --- /dev/null +++ b/requirements-deb13.txt @@ -0,0 +1,2 @@ +numpy==2.2.4 +nose2==0.15.1