diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 48eb03ae..1b557b99 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,12 +17,6 @@ jobs: include: # see compat matrix: https://github.com/pypa/manylinux - - ml: manylinux1_x86_64 - py: cp27-cp27m - cy: "Cython<3.0" - numpy: numpy==1.8.2 - skip: true - - ml: manylinux2014_x86_64 py: cp38-cp38 cy: "Cython<3.1" diff --git a/documentation/building.rst b/documentation/building.rst index 46b6df3c..3380205b 100644 --- a/documentation/building.rst +++ b/documentation/building.rst @@ -8,33 +8,21 @@ Release tars available from https://github.com/epics-base/p4p/releases The P4P modules requires: -* Python 2.7 or >=3.5 (>=3.6 for asyncio support) -* numpy >=1.6 -* Cython >=0.25.2 +* Python >=3.8. +* numpy >=1.7 +* Cython >=3.0.0a1 * nose2 (Optional, recommended to run unittests) and... -Current (>= 4.0) - * EPICS Base >= 3.14.12 * PVXS >= 0.2.0 -Prior to 4.0 - -* EPICS >= 7.0.2 - -or - -* EPICS Base >= 3.14.12 -* pvDataCPP >=7.1.0 -* pvAccessCPP >=6.1.0 - P4P can be built and installed in one of two ways. As a python package, preferably managed by PIP. As an EPICS module. -Optional +Optional Dependencies: * `cothread `_ needed by `p4p.client.cothread.Context`. * `qtpy `_ needed for `p4p.client.Qt.Context`. @@ -80,7 +68,7 @@ Then prepare the virtualenv env with. :: tar -xaf p4p-env-*.tar.gz python virtualenv.py --never-download env . env/bin/activate - pip install --no-index -f virtualenv_support p4p + pip install --no-index -f ./virtualenv_support p4p python -m nose2 p4p # Optional: runs automatic tests Utilities to automate this process include https://pypi.org/project/pyutilib.virtualenv/ @@ -173,4 +161,4 @@ To run the unittests: :: For testing purposes several simple command line client tools are provided. For further information run: :: - PYTHONPATH=$PWD/python2.7/linux-x86_64 python -m p4p.client.cli -h + PYTHONPATH=$PWD/python3.8/linux-x86_64 python -m p4p.client.cli -h diff --git a/documentation/gw.rst b/documentation/gw.rst index 8b663066..55c9c967 100644 --- a/documentation/gw.rst +++ b/documentation/gw.rst @@ -368,7 +368,6 @@ These values are aggregated from all defined internal gateway Servers and Client May be useful for detecting resource leaks while troubleshooting. **threads** - Available when running with python >= 3.5. An RPC call which returns a text description of all python threads. .. note:: diff --git a/documentation/index.rst b/documentation/index.rst index fbda1843..793c1b18 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -9,7 +9,7 @@ https://epics.anl.gov/ P4P includes API for a `clientapi` and `serverapi`, as well as a `gwpage` executable (``pvagw``) which can optionally enforce an access control policy. -Supports Linux, OSX, and Windows. With python 2.7 and >=3.5 (>=3.6 for asyncio support). +Supports Linux, OSX, and Windows. With python >=3.8. The recommended starting point is to install from pypi.org. :: diff --git a/documentation/values.rst b/documentation/values.rst index b2d5a1c0..f7d37c6e 100644 --- a/documentation/values.rst +++ b/documentation/values.rst @@ -91,7 +91,7 @@ Type specifier codes: Code Type ==== ======= ? bool -s unicode +s str b s8 B u8 h s16 @@ -175,7 +175,7 @@ Value Stores a structure int signed 32-bit (python 2.x only) long signed 64-bit float 64-bit floating -bytes|unicode string +bytes|str string ndarray array of integer or floating ============= ================================ diff --git a/makehelper.py b/makehelper.py index a6c60892..b5c32384 100644 --- a/makehelper.py +++ b/makehelper.py @@ -5,7 +5,7 @@ Emits something like the following PY_OK := YES # indicates success of this script -PY_VER := 2.6 +PY_VER := 3.8 PY_INCDIRS := /path ... PY_LIBDIRS := /path ... """ @@ -24,12 +24,7 @@ pass out = open(sys.argv[1], 'w') -try: - from sysconfig import get_config_var, get_path - def get_python_inc(): - return get_path('include') -except ImportError: - from distutils.sysconfig import get_config_var, get_python_inc +from sysconfig import get_config_var, get_path def gcv(name, *dflt): v = get_config_var(name) @@ -39,7 +34,7 @@ def gcv(name, *dflt): raise KeyError(name) return v -incdirs = [get_python_inc()] +incdirs = [get_path('include')] libdir = gcv('LIBDIR', '') or gcv('prefix') + '/libs' @@ -65,20 +60,6 @@ def get_numpy_include_dirs(): print('PY_INCDIRS :=',' '.join(incdirs), file=out) print('PY_LIBDIRS :=',libdir, 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() diff --git a/pyproject.toml b/pyproject.toml index bc702557..2da0330b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,8 @@ requires = [ "setuptools_dso>=2.7a1", "setuptools_dso>=2.11a2; python_version>='3.12'", "wheel", - "numpy>=1.7", - "Cython>0.29.32, <3.0.0a1 ; python_version<'3.0'", - "Cython>0.29.32, <3.1.0a1 ; python_version>='3.0' and python_version<'3.9'", + "numpy>=1.15", + "Cython>0.29.32, <3.1.0a1 ; python_version<'3.9'", "Cython>0.29.32 ; python_version>='3.9'", "epicscorelibs>=7.0.3.99.2.0a1", "epicscorelibs>=7.0.7.99.1.2a2; python_version>='3.12'", diff --git a/setup.py b/setup.py index a348dba8..6853065c 100755 --- a/setup.py +++ b/setup.py @@ -103,7 +103,7 @@ def get_numpy_include_dirs(): ] if hasattr(numpy.lib, "NumpyVersion") and numpy.lib.NumpyVersion(numpy.__version__) >= '2.0.0b1': - install_requires += ['numpy >= 1.7', 'numpy < 3'] + install_requires += ['numpy >= 1.15', 'numpy < 3'] else: # assume ABI forward compatibility as indicated by # https://github.com/numpy/numpy/blob/master/numpy/core/setup_common.py#L28 @@ -121,10 +121,8 @@ def get_numpy_include_dirs(): license='BSD', classifiers = [ 'Development Status :: 5 - Production/Stable', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: Implementation :: CPython', - 'License :: OSI Approved :: BSD License', 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering', 'Topic :: Software Development :: Libraries', @@ -134,7 +132,7 @@ def get_numpy_include_dirs(): 'Operating System :: Microsoft :: Windows', ], keywords='epics scada', - python_requires='>=2.7', + python_requires='>=3.8', packages=[ 'p4p', diff --git a/src/Makefile b/src/Makefile index 72708f4f..da0a6384 100644 --- a/src/Makefile +++ b/src/Makefile @@ -87,7 +87,6 @@ PY += p4p/test/test_qt.py PY += p4p/test/qttest.py PY += p4p/test/test_asyncio.py -PY += p4p/test/asynciotest.py PY += p4p/example.conf PY += p4p/pvagw@.service diff --git a/src/p4p.h b/src/p4p.h index 285a499b..c53f6f42 100644 --- a/src/p4p.h +++ b/src/p4p.h @@ -20,31 +20,18 @@ #include -#if PY_MAJOR_VERSION < 3 -// quiet some warnings about implicit const char* -> char* cast -// for API functions. These are corrected in py >= 3.x -#ifndef PyObject_CallFunction -# define PyObject_CallFunction(O, FMT, ...) PyObject_CallFunction(O, (char*)(FMT), ##__VA_ARGS__) -# define PyObject_CallMethod(O, METH, FMT, ...) PyObject_CallMethod(O, (char*)(METH), (char*)(FMT), ##__VA_ARGS__) -#endif -#endif - // handle -fvisibility=default // effects generated _p4p.cpp #if __GNUC__ >= 4 # undef PyMODINIT_FUNC -# if PY_MAJOR_VERSION < 3 -# define PyMODINIT_FUNC extern "C" __attribute__ ((visibility("default"))) void -# else -# define PyMODINIT_FUNC extern "C" __attribute__ ((visibility("default"))) PyObject* -# endif +# define PyMODINIT_FUNC extern "C" __attribute__ ((visibility("default"))) PyObject* #endif // this file must be included from all code using numpy APIs // include cython generated code. // minimum supported numpy version... -#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION -#define NPY_TARGET_VERSION NPY_1_7_API_VERSION +#define NPY_NO_DEPRECATED_API NPY_1_15_API_VERSION +#define NPY_TARGET_VERSION NPY_1_15_API_VERSION namespace p4p { diff --git a/src/p4p/_gw.pyx b/src/p4p/_gw.pyx index fc4e5383..af682d1f 100644 --- a/src/p4p/_gw.pyx +++ b/src/p4p/_gw.pyx @@ -167,7 +167,7 @@ cdef class Provider(_p4p.Source): self.BanPV = GWSearchBanPV self.BanHostPV = GWSearchBanHostPV - def __init__(self, unicode name, object client, object handler): + def __init__(self, str name, object client, object handler): cdef _p4p.ClientProvider prov = client._ctxt cdef string cname = name.encode('utf-8') self.name = cname diff --git a/src/p4p/_p4p.pyx b/src/p4p/_p4p.pyx index 348c7590..412f355f 100644 --- a/src/p4p/_p4p.pyx +++ b/src/p4p/_p4p.pyx @@ -118,7 +118,7 @@ cdef lookupMember(data.Value* dest, const data.Value& top, key, int err): if key is None: dest[0] = top return - elif isinstance(key, unicode): + elif isinstance(key, str): ckey = key.encode() else: ckey = key diff --git a/src/p4p/client/asyncio.py b/src/p4p/client/asyncio.py index e6d1d9ab..b40b6f16 100644 --- a/src/p4p/client/asyncio.py +++ b/src/p4p/client/asyncio.py @@ -21,23 +21,7 @@ 'timesout', ] -if hasattr(asyncio, 'get_running_loop'): # py >=3.7 - from asyncio import get_running_loop, create_task, all_tasks -else: - from asyncio import _get_running_loop - from asyncio.tasks import Task - - def get_running_loop(): - ret = _get_running_loop() - if ret is None: - raise RuntimeError('Thread has no running event loop') - return ret - - def create_task(coro, *, name=None): - return get_running_loop().create_task(coro) - - def all_tasks(): - return Task.all_tasks(loop=get_running_loop()) +from asyncio import get_running_loop, create_task, all_tasks def timesout(deftimeout=5.0): """Decorate a coroutine to implement an overall timeout. diff --git a/src/p4p/client/cli.py b/src/p4p/client/cli.py index 4c648326..90ed6719 100644 --- a/src/p4p/client/cli.py +++ b/src/p4p/client/cli.py @@ -7,24 +7,19 @@ import sys import time import json -try: - from itertools import izip -except ImportError: - izip = zip - import logging -_log = logging.getLogger(__name__) from .. import nt from .. import set_debug from . import thread +_log = logging.getLogger(__name__) def op_get(ctxt, args): requests = [args.request] * len(args.names) results = ctxt.get(args.names, requests, timeout=args.timeout, throw=False) ret = 0 - for name, val in izip(args.names, results): + for name, val in zip(args.names, results): if isinstance(val, Exception): ret = 1 print(name, 'Error:', val) diff --git a/src/p4p/client/cothread.py b/src/p4p/client/cothread.py index 7aeeb79b..c4d0caa9 100644 --- a/src/p4p/client/cothread.py +++ b/src/p4p/client/cothread.py @@ -23,9 +23,6 @@ 'RemoteError', ] -if sys.version_info >= (3, 0): - unicode = str - def cb2event(done, value): if isinstance(value, Cancelled): pass # can ignore cothreads can't be preempted @@ -56,7 +53,7 @@ def get(self, name, request=None, timeout=5.0, throw=True): >>> A, B = ctxt.get(['pv:1', 'pv:2']) >>> """ - singlepv = isinstance(name, (bytes, unicode)) + singlepv = isinstance(name, (bytes, str)) if singlepv: return self._get_one(name, request=request, timeout=timeout, throw=throw) @@ -124,7 +121,7 @@ def put(self, name, values, request=None, process=None, wait=None, timeout=5.0, Unless the provided value is a dict, it is assumed to be a plain value and an attempt is made to store it in '.value' field. """ - singlepv = isinstance(name, (bytes, unicode)) + singlepv = isinstance(name, (bytes, str)) if request and (process or wait is not None): raise ValueError("request= is mutually exclusive to process= or wait=") elif process or wait is not None: diff --git a/src/p4p/client/raw.py b/src/p4p/client/raw.py index 5ab0d021..52a3e434 100644 --- a/src/p4p/client/raw.py +++ b/src/p4p/client/raw.py @@ -7,10 +7,7 @@ import warnings import sys -try: - from Queue import Queue, Full, Empty -except ImportError: - from queue import Queue, Full, Empty +from queue import Queue, Full, Empty from .. import _p4p from .._p4p import Cancelled, Disconnected, Finished, RemoteError @@ -18,9 +15,6 @@ from ..wrapper import Value, Type from ..nt import buildNT -if sys.version_info >= (3, 0): - unicode = str - __all__ = ( 'Subscription', 'Context', @@ -40,10 +34,7 @@ def dounwrap(code, msg, val, handler=handler): handler(Cancelled()) elif code == 2: # exception during builder callback A, B, C = val - if unicode is str: - E = A(B).with_traceback(C) # py 3 - else: - E = A(B) # py 2 (bye bye traceback...) + E = A(B).with_traceback(C) handler(E) else: if val is not None: @@ -127,9 +118,8 @@ def __enter__(self): def __exit__(self, A, B, C): self.close() - if unicode is str: - def __del__(self): - self.close() + def __del__(self): + self.close() class Context(object): @@ -268,11 +258,9 @@ def _cleanup_contexts(): ctxt.close() class _ClientOperation(_p4p.ClientOperation): - if unicode is str: - def __del__(self): - self.close() + def __del__(self): + self.close() class _ClientProvider(_p4p.ClientProvider): - if unicode is str: - def __del__(self): - self.close() + def __del__(self): + self.close() diff --git a/src/p4p/client/thread.py b/src/p4p/client/thread.py index e70714be..ac7e1dc2 100644 --- a/src/p4p/client/thread.py +++ b/src/p4p/client/thread.py @@ -3,20 +3,11 @@ import logging import sys -_log = logging.getLogger(__name__) - -try: - from itertools import izip -except ImportError: - izip = zip from functools import partial import json import threading -try: - from Queue import Queue, Full, Empty -except ImportError: - from queue import Queue, Full, Empty +from queue import Queue, Full, Empty from . import raw from .raw import Disconnected, RemoteError, Cancelled, Finished @@ -35,16 +26,9 @@ 'TimeoutError', ] -if sys.version_info >= (3, 0): - unicode = str - TimeoutError = TimeoutError - -else: - class TimeoutError(RuntimeError): - "Local timeout has expired" - def __init__(self): - RuntimeError.__init__(self, 'Timeout') +_log = logging.getLogger(__name__) +TimeoutError = TimeoutError class Subscription(object): """An active subscription. @@ -229,7 +213,7 @@ def get(self, name, request=None, timeout=5.0, throw=True): >>> A, B = ctxt.get(['pv:1', 'pv:2']) >>> """ - singlepv = isinstance(name, (bytes, unicode)) + singlepv = isinstance(name, (bytes, str)) if singlepv: name = [name] request = [request] @@ -247,7 +231,7 @@ def get(self, name, request=None, timeout=5.0, throw=True): raw_get = super(Context, self).get try: - for i, (N, req) in enumerate(izip(name, request)): + for i, (N, req) in enumerate(zip(name, request)): def cb(value, i=i): try: if not isinstance(value, Cancelled): @@ -314,7 +298,7 @@ def put(self, name, values, request=None, timeout=5.0, throw=True, Unless the provided value is a dict, it is assumed to be a plain value and an attempt is made to store it in '.value' field. """ - singlepv = isinstance(name, (bytes, unicode)) + singlepv = isinstance(name, (bytes, str)) if request and (process or wait is not None): raise ValueError("request= is mutually exclusive to process= or wait=") elif process or wait is not None: @@ -341,8 +325,8 @@ def put(self, name, values, request=None, timeout=5.0, throw=True, raw_put = super(Context, self).put try: - for i, (n, value, req) in enumerate(izip(name, values, request)): - if isinstance(value, (bytes, unicode)) and value[:1] == '{': + for i, (n, value, req) in enumerate(zip(name, values, request)): + if isinstance(value, (bytes, str)) and value[:1] == '{': try: value = json.loads(value) except ValueError: diff --git a/src/p4p/disect.py b/src/p4p/disect.py index 3febb35d..c64590cf 100644 --- a/src/p4p/disect.py +++ b/src/p4p/disect.py @@ -7,10 +7,6 @@ import inspect import time from glob import fnmatch -try: - from types import InstanceType -except ImportError: # py3 - InstanceType = None class StatsDelta(object): @@ -89,9 +85,6 @@ def gcstats(): if K is StatsDelta: continue # avoid counting ourselves - elif K is InstanceType: # instance of an old-style class - K = getattr(obj, '__class__', K) - # Track types as strings to avoid holding references K = str(K) @@ -117,9 +110,6 @@ def gcfind(name): if K is gcfind: continue # avoid counting ourselves - if K is InstanceType: # instance of an old-style class - K = getattr(obj, '__class__', K) - if fnmatch(str(K), name): found.append(obj) diff --git a/src/p4p/gw.py b/src/p4p/gw.py index 14a682df..dc6a9747 100644 --- a/src/p4p/gw.py +++ b/src/p4p/gw.py @@ -24,9 +24,6 @@ from .asLib.pvlist import PVList from .test.utils import RegularNamedTemporaryFile as NamedTemporaryFile -if sys.version_info >= (3, 0): - unicode = str - _log = logging.getLogger(__name__) _log_audit = logging.getLogger(__name__+'.audit') @@ -241,21 +238,18 @@ def pokeStats(pv, op): self._pvs['StatsTime'] = self.statsTime = SharedPV(nt=NTScalar('d'), initial=0.0) - # faulthandler.dump_traceback() added w/ py 3.3 - # however, file= arg. added w/ 3.5 - if sys.version_info>=(3,5): - self._pvs['threads'] = stackstrace = SharedPV(nt=NTScalar('s'), initial='RPC only') - @stackstrace.rpc - def showStacks(pv, op): - import faulthandler - from tempfile import TemporaryFile - with TemporaryFile('r+') as F: - faulthandler.dump_traceback(file=F) - F.seek(0) - V = pv.current().raw - V.unmark() - V['value'] = F.read() - op.done(V) + self._pvs['threads'] = stackstrace = SharedPV(nt=NTScalar('s'), initial='RPC only') + @stackstrace.rpc + def showStacks(pv, op): + import faulthandler + from tempfile import TemporaryFile + with TemporaryFile('r+') as F: + faulthandler.dump_traceback(file=F) + F.seek(0) + V = pv.current().raw + V.unmark() + V['value'] = F.read() + op.done(V) # PVs for bandwidth usage statistics. # 2x tables: us, ds diff --git a/src/p4p/nt/__init__.py b/src/p4p/nt/__init__.py index e4af0489..81293ed2 100644 --- a/src/p4p/nt/__init__.py +++ b/src/p4p/nt/__init__.py @@ -1,12 +1,5 @@ import logging -_log = logging.getLogger(__name__) - -try: - from itertools import izip -except ImportError: - izip = zip - from collections import OrderedDict from ..wrapper import Type, Value from .common import timeStamp, alarm, NTBase @@ -23,6 +16,8 @@ 'defaultNT', ] +_log = logging.getLogger(__name__) + _default_nt = { "epics:nt/NTScalar:1.0": NTScalar, "epics:nt/NTScalarArray:1.0": NTScalar, @@ -228,7 +223,7 @@ def unwrap(value): cols.append(cval) # zip together column arrays to iterate over rows - for rval in izip(*cols): + for rval in zip(*cols): # zip together column names and row values ret.append(OrderedDict(zip(lbl, rval))) diff --git a/src/p4p/nt/enum.py b/src/p4p/nt/enum.py index 8a413f94..a2fe18a8 100644 --- a/src/p4p/nt/enum.py +++ b/src/p4p/nt/enum.py @@ -8,9 +8,6 @@ from .common import alarm, timeStamp, NTBase from .scalar import _metaHelper, ntwrappercommon -if sys.version_info >= (3, 0): - unicode = str - class ntenum(ntwrappercommon, int): """ @@ -110,7 +107,7 @@ def unwrap(self, value): def assign(self, V, py): """Store python value in Value """ - if isinstance(py, (bytes, unicode)): + if isinstance(py, (bytes, str)): for i,C in enumerate(V['value.choices'] or self._choices): if py==C: V['value.index'] = i diff --git a/src/p4p/nt/scalar.py b/src/p4p/nt/scalar.py index c5176e7d..2fe0d26f 100644 --- a/src/p4p/nt/scalar.py +++ b/src/p4p/nt/scalar.py @@ -6,10 +6,6 @@ from ..wrapper import Type, Value from .common import alarm, timeStamp, NTBase -if sys.version_info >= (3, 0): - unicode = str - - class ntwrappercommon(object): raw = timestamp = None @@ -70,7 +66,7 @@ def __repr__(self): return bool(self).__repr__().lower() -class ntstr(ntwrappercommon, unicode): +class ntstr(ntwrappercommon, str): """ Augmented string with additional attributes @@ -243,7 +239,7 @@ def wrap(self, value, **kws): bool: ntbool, int: ntint, float: ntfloat, - unicode: ntstr, + str: ntstr, numpy.ndarray: ntnumericarray.build, list: ntstringarray, } @@ -267,9 +263,3 @@ def assign(self, V, py): """Store python value in Value """ V.value = py - -if sys.version_info < (3, 0): - class ntlong(ntwrappercommon, long): - pass - - NTScalar.typeMap[long] = ntlong diff --git a/src/p4p/rpc.py b/src/p4p/rpc.py index d9b7d498..e94af6e1 100644 --- a/src/p4p/rpc.py +++ b/src/p4p/rpc.py @@ -254,12 +254,8 @@ class RPCProxyBase(object): def _wrapMethod(name, meth): pv, req = meth._call_PV, meth._call_Request - if sys.version_info >= (3, 0): - S = inspect.getfullargspec(meth) - keywords = S.varkw - else: - S = inspect.getargspec(meth) - keywords = S.keywords + S = inspect.getfullargspec(meth) + keywords = S.varkw defaults = S.defaults or [] # for getfullargspec().defaults can be None if S.varargs is not None or keywords is not None: diff --git a/src/p4p/server/__init__.py b/src/p4p/server/__init__.py index 3b47744d..65835e2d 100644 --- a/src/p4p/server/__init__.py +++ b/src/p4p/server/__init__.py @@ -15,9 +15,6 @@ ServerOperation, ) -if sys.version_info >= (3, 0): - unicode = str - _log = logging.getLogger(__name__) __all__ = ( @@ -74,7 +71,7 @@ class Server(object): def __init__(self, providers, isolate=False, **kws): self.__keep_alive = [] # ick... - if isinstance(providers, (bytes, unicode)): + if isinstance(providers, (bytes, str)): providers = providers.split() # split on space warnings.warn("Server providers list should be a list", DeprecationWarning) @@ -87,7 +84,7 @@ def __init__(self, providers, isolate=False, **kws): else: order = 0 - if isinstance(provider, (bytes, unicode)): + if isinstance(provider, (bytes, str)): if not re.match(r'^[^ \t\n\r]+$', provider): raise ValueError("Invalid provider name: '%s'"%provider) Ps.append((provider, order)) diff --git a/src/p4p/server/raw.py b/src/p4p/server/raw.py index 2629f780..0297340a 100644 --- a/src/p4p/server/raw.py +++ b/src/p4p/server/raw.py @@ -19,7 +19,7 @@ def value(self): V = self._op.value() try: return self._unwrap(V) - except: # py3 will chain automatically, py2 won't + except: raise ValueError("Unable to unwrap %r with %r"%(V, self._unwrap)) def done(self, value=None, error=None): diff --git a/src/p4p/test/asynciotest.py b/src/p4p/test/asynciotest.py deleted file mode 100644 index b52fe43a..00000000 --- a/src/p4p/test/asynciotest.py +++ /dev/null @@ -1,275 +0,0 @@ -# I'm imported from test_asyncio.y -# Add new TestCase s to __all__ - -import logging -_log = logging.getLogger(__name__) -import sys -from functools import wraps - -from .. import Value -from ..nt import NTScalar -from ..server import Server, StaticProvider -from .utils import RefTestCase - -import asyncio - -from ..client.asyncio import Context, Disconnected, timesout -from ..server.asyncio import SharedPV - -__all__ = ( - 'TestGPM', - 'TestTimeout', - 'TestFirstLast', -) - -if sys.version_info < (3, 14): - # we should never implicitly use the default loop. - asyncio.get_event_loop().close() - -class AsyncMeta(type): - """Automatically wrap and "async def test*():" methods for dispatch to self.loop - """ - def __new__(klass, name, bases, classdict): - for name, mem in classdict.items(): - if name.startswith('test') and asyncio.iscoroutinefunction(mem): - @wraps(mem) - def wrapper(self, mem=mem): - self.loop.run_until_complete(asyncio.wait_for(mem(self), self.timeout)) - classdict[name] = wrapper - - return type.__new__(klass, name, bases, classdict) - -class AsyncTest(RefTestCase, metaclass=AsyncMeta): - timeout = 1.0 - - def setUp(self): - super(AsyncTest, self).setUp() - self.loop = asyncio.new_event_loop() - self.loop.set_debug(True) - self.loop.run_until_complete(asyncio.wait_for(self.asyncSetUp(), self.timeout)) - - def tearDown(self): - self.loop.run_until_complete(asyncio.wait_for(self.asyncTearDown(), self.timeout)) - self.loop.close() - super(AsyncTest, self).tearDown() - - async def asyncSetUp(self): - pass - - async def asyncTearDown(self): - pass - -class TestGPM(AsyncTest): - timeout = 3 # overall timeout for each test method - - class Handler: - async def put(self, pv, op): - _log.debug("putting %s <- %s", op.name(), op.value()) - await asyncio.sleep(0) # prove that we can - pv.post(op.value() * 2) - op.done() - - async def asyncSetUp(self): - await super(TestGPM, self).asyncSetUp() - - self.pv = SharedPV(nt=NTScalar('i'), initial=0, handler=self.Handler()) - self.pv2 = SharedPV(handler=self.Handler(), nt=NTScalar('d'), initial=42.0) - self.provider = StaticProvider("serverend") - self.provider.add('foo', self.pv) - self.provider.add('bar', self.pv2) - - async def asyncTearDown(self): - del self.pv - del self.pv2 - del self.provider - await super(TestGPM, self).asyncTearDown() - - async def test_getput(self): - with Server(providers=[self.provider], isolate=True) as S: - with Context('pva', conf=S.conf(), useenv=False) as C: - self.assertEqual(0, (await C.get('foo'))) - - await C.put('foo', 5) - - self.assertEqual(5 * 2, (await C.get('foo'))) - - async def test_monitor(self): - with Server(providers=[self.provider], isolate=True) as S: - with Context('pva', conf=S.conf(), useenv=False) as C: - - Q = asyncio.Queue() - - sub = C.monitor('foo', Q.put, notify_disconnect=True) - try: - self.assertIsInstance((await Q.get()), Disconnected) - - self.assertEqual(0, (await Q.get())) - - await C.put('foo', 2) - - self.assertEqual(2 * 2, (await Q.get())) - - self.pv.close() - - self.assertIsInstance((await Q.get()), Disconnected) - - self.pv.open(3) - - self.assertEqual(3, (await Q.get())) - - finally: - sub.close() - await sub.wait_closed() - - async def test_put_noconvert(self): - with Server(providers=[self.provider], isolate=True) as S: - with Context('pva', conf=S.conf(), useenv=False) as C: - with self.assertRaisesRegex(ValueError, 'not_an_integer'): - await C.put('foo', 'not_an_integer') - - if hasattr(asyncio, 'coroutine'): - @asyncio.coroutine - def test_gen_coro(self): - # demonstrate interoperability w/ code using generated based coroutines - with Server(providers=[self.provider], isolate=True) as S: - with Context('pva', conf=S.conf(), useenv=False) as C: - self.assertEqual(0, (yield from C.get('foo'))) - - yield from C.put('foo', 5) - - self.assertEqual(5 * 2, (yield from C.get('foo'))) - - -class TestTimeout(AsyncTest): - - async def test_timeout(self): - done = None - - @timesout() - async def action(): - nonlocal done - done = False - await asyncio.sleep(5) - done = True - - with self.assertRaises(asyncio.TimeoutError): - await action(timeout=0.01) - -class TestFirstLast(AsyncTest): - maxDiff = 2000 - timeout = 5.0 - mode = 'Mask' - - class Handler: - def __init__(self): - self.evtC = asyncio.Event() - self.evtD = asyncio.Event() - self.conn = None - def onFirstConnect(self, pv): - _log.debug("onFirstConnect") - self.conn = True - self.evtC.set() - def onLastDisconnect(self, pv): - _log.debug("onLastDisconnect") - self.conn = False - self.evtD.set() - - async def asyncSetUp(self): - await super(TestFirstLast, self).asyncSetUp() - - self.H = self.Handler() - self.pv = SharedPV(handler=self.H, - nt=NTScalar('d'), - options={'mapperMode':self.mode}) - self.sprov = StaticProvider("serverend") - self.sprov.add('foo', self.pv) - - self.server = Server(providers=[self.sprov], isolate=True) - - async def asyncTearDown(self): - self.server.stop() - #_defaultWorkQueue.stop() - del self.server - del self.sprov - del self.pv - del self.H - await super(TestFirstLast, self).asyncTearDown() - - async def testClientDisconn(self): - self.pv.open(1.0) - - with Context('pva', conf=self.server.conf(), useenv=False, unwrap={}) as ctxt: - Q = asyncio.Queue(maxsize=4) - sub = ctxt.monitor('foo', Q.put, notify_disconnect=True) - try: - - _log.debug('Wait Disconnected') - E = await Q.get() # Disconnected - self.assertIsInstance(E, Disconnected) - _log.debug('Wait initial') - E = await Q.get() # initial update - self.assertIsInstance(E, Value) - - _log.debug('Wait onFirstConnect') - await self.H.evtC.wait() # onFirstConnect() - self.assertTrue(self.H.conn) - - except: - _log.exception('oops') - raise - finally: - _log.debug('sub close()') - sub.close() - await sub.wait_closed() - - _log.debug('Wait onLastDisconnect') - await self.H.evtD.wait() # onLastDisconnect() - _log.debug('SHUTDOWN') - self.assertFalse(self.H.conn) - - async def testServerShutdown(self): - self.pv.open(1.0) - - with Context('pva', conf=self.server.conf(), useenv=False, unwrap={}) as ctxt: - Q = asyncio.Queue(maxsize=4) - sub = ctxt.monitor('foo', Q.put, notify_disconnect=True) - try: - - await Q.get() # initial update - - _log.debug('TEST') - await self.H.evtC.wait() # onFirstConnect() - self.assertIs(self.H.conn, True) - - self.server.stop() - - await self.H.evtD.wait() # onLastDisconnect() - _log.debug('SHUTDOWN') - self.assertIs(self.H.conn, False) - - finally: - sub.close() - await sub.wait_closed() - - async def testPVClose(self): - self.pv.open(1.0) - - with Context('pva', conf=self.server.conf(), useenv=False, unwrap={}) as ctxt: - Q = asyncio.Queue(maxsize=4) - sub = ctxt.monitor('foo', Q.put, notify_disconnect=True) - try: - - await Q.get() # initial update - - _log.debug('TEST') - await self.H.evtC.wait() # onFirstConnect() - self.assertTrue(self.H.conn) - - await self.pv.close(destroy=True, sync=True) # onLastDisconnect() - - _log.debug('CLOSE') - self.assertFalse(self.H.conn) - - finally: - sub.close() - await sub.wait_closed() diff --git a/src/p4p/test/test_asyncio.py b/src/p4p/test/test_asyncio.py index fd3eefd4..7324848f 100644 --- a/src/p4p/test/test_asyncio.py +++ b/src/p4p/test/test_asyncio.py @@ -1,10 +1,267 @@ + +import logging +_log = logging.getLogger(__name__) import sys -import unittest +from functools import wraps + +from .. import Value +from ..nt import NTScalar +from ..server import Server, StaticProvider +from .utils import RefTestCase + +import asyncio + +from ..client.asyncio import Context, Disconnected, timesout +from ..server.asyncio import SharedPV + +if sys.version_info < (3, 14): + # we should never implicitly use the default loop. + asyncio.get_event_loop().close() + +class AsyncMeta(type): + """Automatically wrap and "async def test*():" methods for dispatch to self.loop + """ + def __new__(klass, name, bases, classdict): + for name, mem in classdict.items(): + if name.startswith('test') and asyncio.iscoroutinefunction(mem): + @wraps(mem) + def wrapper(self, mem=mem): + self.loop.run_until_complete(asyncio.wait_for(mem(self), self.timeout)) + classdict[name] = wrapper + + return type.__new__(klass, name, bases, classdict) + +class AsyncTest(RefTestCase, metaclass=AsyncMeta): + timeout = 1.0 + + def setUp(self): + super(AsyncTest, self).setUp() + self.loop = asyncio.new_event_loop() + self.loop.set_debug(True) + self.loop.run_until_complete(asyncio.wait_for(self.asyncSetUp(), self.timeout)) + + def tearDown(self): + self.loop.run_until_complete(asyncio.wait_for(self.asyncTearDown(), self.timeout)) + self.loop.close() + super(AsyncTest, self).tearDown() + + async def asyncSetUp(self): + pass + + async def asyncTearDown(self): + pass + +class TestGPM(AsyncTest): + timeout = 3 # overall timeout for each test method + + class Handler: + async def put(self, pv, op): + _log.debug("putting %s <- %s", op.name(), op.value()) + await asyncio.sleep(0) # prove that we can + pv.post(op.value() * 2) + op.done() + + async def asyncSetUp(self): + await super(TestGPM, self).asyncSetUp() + + self.pv = SharedPV(nt=NTScalar('i'), initial=0, handler=self.Handler()) + self.pv2 = SharedPV(handler=self.Handler(), nt=NTScalar('d'), initial=42.0) + self.provider = StaticProvider("serverend") + self.provider.add('foo', self.pv) + self.provider.add('bar', self.pv2) + + async def asyncTearDown(self): + del self.pv + del self.pv2 + del self.provider + await super(TestGPM, self).asyncTearDown() + + async def test_getput(self): + with Server(providers=[self.provider], isolate=True) as S: + with Context('pva', conf=S.conf(), useenv=False) as C: + self.assertEqual(0, (await C.get('foo'))) + + await C.put('foo', 5) + + self.assertEqual(5 * 2, (await C.get('foo'))) + + async def test_monitor(self): + with Server(providers=[self.provider], isolate=True) as S: + with Context('pva', conf=S.conf(), useenv=False) as C: + + Q = asyncio.Queue() + + sub = C.monitor('foo', Q.put, notify_disconnect=True) + try: + self.assertIsInstance((await Q.get()), Disconnected) + + self.assertEqual(0, (await Q.get())) + + await C.put('foo', 2) + + self.assertEqual(2 * 2, (await Q.get())) + + self.pv.close() + + self.assertIsInstance((await Q.get()), Disconnected) + + self.pv.open(3) + + self.assertEqual(3, (await Q.get())) + + finally: + sub.close() + await sub.wait_closed() + + async def test_put_noconvert(self): + with Server(providers=[self.provider], isolate=True) as S: + with Context('pva', conf=S.conf(), useenv=False) as C: + with self.assertRaisesRegex(ValueError, 'not_an_integer'): + await C.put('foo', 'not_an_integer') + + if hasattr(asyncio, 'coroutine'): + @asyncio.coroutine + def test_gen_coro(self): + # demonstrate interoperability w/ code using generated based coroutines + with Server(providers=[self.provider], isolate=True) as S: + with Context('pva', conf=S.conf(), useenv=False) as C: + self.assertEqual(0, (yield from C.get('foo'))) + + yield from C.put('foo', 5) + + self.assertEqual(5 * 2, (yield from C.get('foo'))) + + +class TestTimeout(AsyncTest): + + async def test_timeout(self): + done = None + + @timesout() + async def action(): + nonlocal done + done = False + await asyncio.sleep(5) + done = True + + with self.assertRaises(asyncio.TimeoutError): + await action(timeout=0.01) + +class TestFirstLast(AsyncTest): + maxDiff = 2000 + timeout = 5.0 + mode = 'Mask' + + class Handler: + def __init__(self): + self.evtC = asyncio.Event() + self.evtD = asyncio.Event() + self.conn = None + def onFirstConnect(self, pv): + _log.debug("onFirstConnect") + self.conn = True + self.evtC.set() + def onLastDisconnect(self, pv): + _log.debug("onLastDisconnect") + self.conn = False + self.evtD.set() + + async def asyncSetUp(self): + await super(TestFirstLast, self).asyncSetUp() + + self.H = self.Handler() + self.pv = SharedPV(handler=self.H, + nt=NTScalar('d'), + options={'mapperMode':self.mode}) + self.sprov = StaticProvider("serverend") + self.sprov.add('foo', self.pv) + + self.server = Server(providers=[self.sprov], isolate=True) + + async def asyncTearDown(self): + self.server.stop() + #_defaultWorkQueue.stop() + del self.server + del self.sprov + del self.pv + del self.H + await super(TestFirstLast, self).asyncTearDown() + + async def testClientDisconn(self): + self.pv.open(1.0) + + with Context('pva', conf=self.server.conf(), useenv=False, unwrap={}) as ctxt: + Q = asyncio.Queue(maxsize=4) + sub = ctxt.monitor('foo', Q.put, notify_disconnect=True) + try: + + _log.debug('Wait Disconnected') + E = await Q.get() # Disconnected + self.assertIsInstance(E, Disconnected) + _log.debug('Wait initial') + E = await Q.get() # initial update + self.assertIsInstance(E, Value) + + _log.debug('Wait onFirstConnect') + await self.H.evtC.wait() # onFirstConnect() + self.assertTrue(self.H.conn) + + except: + _log.exception('oops') + raise + finally: + _log.debug('sub close()') + sub.close() + await sub.wait_closed() + + _log.debug('Wait onLastDisconnect') + await self.H.evtD.wait() # onLastDisconnect() + _log.debug('SHUTDOWN') + self.assertFalse(self.H.conn) + + async def testServerShutdown(self): + self.pv.open(1.0) + + with Context('pva', conf=self.server.conf(), useenv=False, unwrap={}) as ctxt: + Q = asyncio.Queue(maxsize=4) + sub = ctxt.monitor('foo', Q.put, notify_disconnect=True) + try: + + await Q.get() # initial update + + _log.debug('TEST') + await self.H.evtC.wait() # onFirstConnect() + self.assertIs(self.H.conn, True) + + self.server.stop() + + await self.H.evtD.wait() # onLastDisconnect() + _log.debug('SHUTDOWN') + self.assertIs(self.H.conn, False) + + finally: + sub.close() + await sub.wait_closed() + + async def testPVClose(self): + self.pv.open(1.0) + + with Context('pva', conf=self.server.conf(), useenv=False, unwrap={}) as ctxt: + Q = asyncio.Queue(maxsize=4) + sub = ctxt.monitor('foo', Q.put, notify_disconnect=True) + try: + + await Q.get() # initial update + + _log.debug('TEST') + await self.H.evtC.wait() # onFirstConnect() + self.assertTrue(self.H.conn) + + await self.pv.close(destroy=True, sync=True) # onLastDisconnect() -if sys.version_info<(3,6): - class TestDummy(unittest.TestCase): - def test_asyncio(self): - raise unittest.SkipTest("asyncio needs Py >=3.6") + _log.debug('CLOSE') + self.assertFalse(self.H.conn) -else: - from .asynciotest import * + finally: + sub.close() + await sub.wait_closed() diff --git a/src/p4p/test/test_gw.py b/src/p4p/test/test_gw.py index 91b82137..cdf93d47 100644 --- a/src/p4p/test/test_gw.py +++ b/src/p4p/test/test_gw.py @@ -7,17 +7,10 @@ import weakref import threading -try: # 2.7 - from Queue import Queue, Full, Empty - from time import time as monotonic -except ImportError: # 3.x - from queue import Queue, Full, Empty - from time import monotonic - -try: - from io import StringIO -except ImportError: - from cString import StringIO +from queue import Queue, Full, Empty +from time import monotonic + +from io import StringIO from .utils import RefTestCase, RegularNamedTemporaryFile as NamedTemporaryFile from ..server import Server, StaticProvider diff --git a/src/p4p/test/test_nt.py b/src/p4p/test/test_nt.py index b85ea1f8..48ea2097 100644 --- a/src/p4p/test/test_nt.py +++ b/src/p4p/test/test_nt.py @@ -244,14 +244,13 @@ def fn(): self.assertEqual(V.value.index, 42) - if sys.version_info >= (3, 0): - V.value.choices = [] + V.value.choices = [] - def fn(): - V.value = '1' - self.assertWarns(UserWarning, fn) # warns of empty choices + def fn(): + V.value = '1' + self.assertWarns(UserWarning, fn) # warns of empty choices - self.assertEqual(V.value.index, 1) + self.assertEqual(V.value.index, 1) def testSubStore(self): V = Value(Type([ diff --git a/src/p4p/test/test_sharedpv.py b/src/p4p/test/test_sharedpv.py index a810ba32..c19f07de 100644 --- a/src/p4p/test/test_sharedpv.py +++ b/src/p4p/test/test_sharedpv.py @@ -9,10 +9,7 @@ import inspect import threading -try: - from Queue import Queue, Full, Empty -except ImportError: - from queue import Queue, Full, Empty +from queue import Queue, Full, Empty from ..wrapper import Value, Type from ..client.thread import Context, Disconnected, TimeoutError, RemoteError diff --git a/src/p4p/util.py b/src/p4p/util.py index 474dddcd..97cbdd1c 100644 --- a/src/p4p/util.py +++ b/src/p4p/util.py @@ -4,10 +4,7 @@ from functools import partial -try: - from Queue import Queue, Full, Empty -except ImportError: - from queue import Queue, Full, Empty +from queue import Queue, Full, Empty from threading import Thread, Event __all__ = [ diff --git a/src/p4p/wrapper.py b/src/p4p/wrapper.py index d823b9fd..3c22add9 100644 --- a/src/p4p/wrapper.py +++ b/src/p4p/wrapper.py @@ -62,7 +62,7 @@ class Type(_p4p._Type): Code Type ==== ======= ? bool - s unicode + s str b s8 B u8 h s16 diff --git a/src/pvxs_type.cpp b/src/pvxs_type.cpp index 8e4dfcd0..23498efc 100644 --- a/src/pvxs_type.cpp +++ b/src/pvxs_type.cpp @@ -69,11 +69,6 @@ void appendMembers(std::vector& members, PyObject* spec) members.push_back(TypeDef(prototype).as(key)); -#if PY_MAJOR_VERSION < 3 - } else if(PyBytes_Check(val)) { - const char *spec = PyBytes_AsString(val); - members.push_back(plainMember(key, spec)); -#endif } else if(PyUnicode_Check(val)) { PyRef str(PyUnicode_AsASCIIString(val)); const char *spec = PyBytes_AsString(str.obj); diff --git a/src/pvxs_value.cpp b/src/pvxs_value.cpp index 0052cadd..80c99022 100644 --- a/src/pvxs_value.cpp +++ b/src/pvxs_value.cpp @@ -44,11 +44,6 @@ PyObject* asPy(const Value& v, bool unpackstruct, bool unpackrecurse, PyObject* case StoreType::Real: return PyFloat_FromDouble(v.as()); case StoreType::Integer: -#if PY_MAJOR_VERSION < 3 - if(v.type()!=TypeCode::Int64) { - return PyInt_FromLong(v.as()); - } -#endif return PyLong_FromLongLong(v.as()); case StoreType::UInteger: return PyLong_FromUnsignedLongLong(v.as()); @@ -219,10 +214,6 @@ Value inferPy(PyObject* py) TypeCode code = TypeCode::Null; if(PyBool_Check(py)) code = TypeCode::Bool; -#if PY_MAJOR_VERSION < 3 - else if(PyInt_Check(py)) - code = TypeCode::Int32; -#endif else if(PyLong_Check(py)) code = TypeCode::Int64; else if(PyFloat_Check(py)) @@ -287,10 +278,6 @@ void storePy(Value& v, PyObject* py, bool forceCast) if(PyBool_Check(py)) { v.from(py==Py_True); -#if PY_MAJOR_VERSION < 3 - } else if(PyInt_Check(py)) { - v.from(PyInt_AsLong(py)); -#endif } else if(PyLong_Check(py) || PyArray_IsScalar(py, Integer)) { int oflow = 0; long long temp = PyLong_AsLongLongAndOverflow(py, &oflow);