From b43886c93a4ce21bcd0dc0892eb0de9a09f832d9 Mon Sep 17 00:00:00 2001 From: Michael Howitz Date: Mon, 28 Sep 2020 16:08:03 +0200 Subject: [PATCH] Drop support for Python 2. --- .coveragerc | 7 ++- .travis.yml | 5 -- CHANGES.rst | 2 +- appveyor.yml | 2 - setup.py | 5 +- src/Acquisition/_Acquisition.c | 110 --------------------------------- src/Acquisition/__init__.py | 71 +++++++-------------- src/Acquisition/tests.py | 110 ++++++++++++++------------------- tox.ini | 12 ++-- 9 files changed, 87 insertions(+), 237 deletions(-) diff --git a/.coveragerc b/.coveragerc index 913da64..981c411 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,11 +1,16 @@ [run] branch = True source = Acquisition -plugins = coverage_python_version [report] precision = 2 show_missing = True +exclude_lines = + pragma: no cover + if __name__ == '__main__': + raise NotImplementedError + raise AssertionError + self.fail [paths] source = diff --git a/.travis.yml b/.travis.yml index e3aeb03..d40bb8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,12 @@ language: python python: - - 2.7 - 3.6 - 3.7 - 3.8 - 3.9-dev - - pypy - pypy3 matrix: include: - - name: "2.7-pure" - python: "2.7" - env: PURE_PYTHON=1 - name: "3.8-pure" python: "3.8" env: PURE_PYTHON=1 diff --git a/CHANGES.rst b/CHANGES.rst index 863194d..3ffe369 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,7 +6,7 @@ Changelog - Add support for Python 3.8 and 3.9. -- Drop support for Python 3.5. +- Drop support for Python 2 and 3.5. 4.6 (2019-04-24) diff --git a/appveyor.yml b/appveyor.yml index 68e39e0..9a5ae55 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,8 +5,6 @@ environment: secure: y/k8TP312tLRATkXU5dq+g== matrix: - - python: 27 - - python: 27-x64 - python: 36 - python: 36-x64 - python: 37 diff --git a/setup.py b/setup.py index 853032a..824d21e 100644 --- a/setup.py +++ b/setup.py @@ -57,9 +57,8 @@ "License :: OSI Approved :: Zope Public License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", @@ -68,7 +67,7 @@ "Programming Language :: Python :: Implementation :: PyPy", ], ext_modules=ext_modules, - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*', + python_requires='>=3.6, <4', install_requires=[ 'ExtensionClass >= 4.2.0', 'zope.interface', diff --git a/src/Acquisition/_Acquisition.c b/src/Acquisition/_Acquisition.c index aab684f..4b32da9 100644 --- a/src/Acquisition/_Acquisition.c +++ b/src/Acquisition/_Acquisition.c @@ -20,18 +20,6 @@ static ACQUISITIONCAPI AcquisitionCAPI; -// Py_XSETREF is undefined in Python 3.4 only, it's present in 2.7 and 3.5 -#ifndef Py_XSETREF - -#define Py_XSETREF(op, op2) \ - do { \ - PyObject *_py_tmp = (PyObject *)(op); \ - (op) = (op2); \ - Py_XDECREF(_py_tmp); \ - } while (0) - -#endif - #define ASSIGN(dst, src) Py_XSETREF(dst, src) #define OBJECT(O) ((PyObject*)(O)) @@ -982,11 +970,7 @@ Wrapper_bytes(Wrapper *self) return r; } else { PyErr_Clear(); -#ifdef PY3K return PyBytes_FromObject(self->obj); -#else - return Wrapper_str(self); -#endif } } @@ -1217,11 +1201,6 @@ static PyMappingMethods Wrapper_as_mapping = { WRAP_BINOP(sub); WRAP_BINOP(mul); - -#ifndef PY3K -WRAP_BINOP(div); -#endif - WRAP_BINOP(mod); WRAP_BINOP(divmod); WRAP_TERNARYOP(pow); @@ -1236,26 +1215,11 @@ WRAP_BINOP(xor); WRAP_BINOP(or); WRAP_UNARYOP(int); - -#ifndef PY3K -WRAP_UNARYOP(long); -#endif - WRAP_UNARYOP(float); -#ifndef PY3K -WRAP_UNARYOP(oct); -WRAP_UNARYOP(hex); -#endif - WRAP_BINOP(iadd); WRAP_BINOP(isub); WRAP_BINOP(imul); - -#ifndef PY3K -WRAP_BINOP(idiv); -#endif - WRAP_BINOP(imod); WRAP_TERNARYOP(ipow); WRAP_BINOP(ilshift); @@ -1281,11 +1245,7 @@ Wrapper_nonzero(PyObject *self) PyObject* result = NULL; PyObject* callable = NULL; -#ifdef PY3K callable = PyObject_GetAttr(self, py__bool__); -#else - callable = PyObject_GetAttr(self, py__nonzero__); -#endif if (callable == NULL) { PyErr_Clear(); @@ -1311,43 +1271,11 @@ Wrapper_nonzero(PyObject *self) } -#ifndef PY3K -static int -Wrapper_coerce(PyObject **self, PyObject **o) -{ - PyObject *m; - - if ((m=PyObject_GetAttr(*self, py__coerce__)) == NULL) { - PyErr_Clear(); - Py_INCREF(*self); - Py_INCREF(*o); - return 0; - } - - ASSIGN(m, PyObject_CallFunction(m, "O", *o)); - if (m == NULL) { - return -1; - } - - if (!PyArg_ParseTuple(m, "OO", self, o)) { - Py_DECREF(m); - return -1; - } - - Py_INCREF(*self); - Py_INCREF(*o); - Py_DECREF(m); - return 0; -} -#endif static PyNumberMethods Wrapper_as_number = { Wrapper_add, /* nb_add */ Wrapper_sub, /* nb_subtract */ Wrapper_mul, /* nb_multiply */ -#ifndef PY3K - Wrapper_div, /* nb_divide */ -#endif Wrapper_mod, /* nb_remainder */ Wrapper_divmod, /* nb_divmod */ Wrapper_pow, /* nb_power */ @@ -1361,34 +1289,12 @@ static PyNumberMethods Wrapper_as_number = { Wrapper_and, /* nb_and */ Wrapper_xor, /* nb_xor */ Wrapper_or, /* nb_or */ - -#ifndef PY3K - Wrapper_coerce, /* nb_coerce */ -#endif - Wrapper_int, /* nb_int */ - -#ifdef PY3K NULL, -#else - Wrapper_long, /* nb_long */ -#endif - Wrapper_float, /* nb_float */ - -#ifndef PY3K - Wrapper_oct, /* nb_oct*/ - Wrapper_hex, /* nb_hex*/ -#endif - Wrapper_iadd, /* nb_inplace_add */ Wrapper_isub, /* nb_inplace_subtract */ Wrapper_imul, /* nb_inplace_multiply */ - -#ifndef PY3K - Wrapper_idiv, /* nb_inplace_divide */ -#endif - Wrapper_imod, /* nb_inplace_remainder */ Wrapper_ipow, /* nb_inplace_power */ Wrapper_ilshift, /* nb_inplace_lshift */ @@ -2003,7 +1909,6 @@ static struct PyMethodDef methods[] = { {NULL, NULL} }; -#ifdef PY3K static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, @@ -2016,7 +1921,6 @@ static struct PyModuleDef moduledef = NULL, /* m_clear */ NULL, /* m_free */ }; -#endif static PyObject* @@ -2044,14 +1948,7 @@ module_init(void) return NULL; } -#ifdef PY3K m = PyModule_Create(&moduledef); -#else - m = Py_InitModule3("_Acquisition", - methods, - "Provide base classes for acquiring objects\n\n"); -#endif - d = PyModule_GetDict(m); init_py_names(); PyExtensionClass_Export(d,"Acquirer", AcquirerType); @@ -2081,14 +1978,7 @@ module_init(void) return m; } -#ifdef PY3K PyMODINIT_FUNC PyInit__Acquisition(void) { return module_init(); } -#else -PyMODINIT_FUNC init_Acquisition(void) -{ - module_init(); -} -#endif diff --git a/src/Acquisition/__init__.py b/src/Acquisition/__init__.py index 34e7003..6df0e0e 100644 --- a/src/Acquisition/__init__.py +++ b/src/Acquisition/__init__.py @@ -1,8 +1,7 @@ -from __future__ import absolute_import, print_function - # pylint:disable=W0212,R0911,R0912 +import copyreg import os import operator import platform @@ -21,7 +20,7 @@ IS_PURE = 'PURE_PYTHON' in os.environ -class Acquired(object): +class Acquired: "Marker for explicit acquisition" @@ -47,32 +46,19 @@ def _apply_filter(predicate, inst, name, result, extra, orig): return predicate(orig, inst, name, result, extra) -if sys.version_info < (3,): # pragma: PY2 - import copy_reg - - def _rebound_method(method, wrapper): - """Returns a version of the method with self bound to `wrapper`""" - if isinstance(method, types.MethodType): - method = types.MethodType(method.im_func, wrapper, method.im_class) - return method - exec("""def _reraise(tp, value, tb=None): - raise tp, value, tb -""") -else: # pragma: PY3 - import copyreg as copy_reg - - def _rebound_method(method, wrapper): - """Returns a version of the method with self bound to `wrapper`""" - if isinstance(method, types.MethodType): - method = types.MethodType(method.__func__, wrapper) - return method - - def _reraise(tp, value, tb=None): - if value is None: - value = tp() - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value +def _rebound_method(method, wrapper): + """Returns a version of the method with self bound to `wrapper`""" + if isinstance(method, types.MethodType): + method = types.MethodType(method.__func__, wrapper) + return method + + +def _reraise(tp, value, tb=None): + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value ### # Wrapper object protocol, mostly ported from C directly @@ -338,7 +324,7 @@ def _make_wrapper_subclass_if_needed(cls, obj, container): type_obj = type(obj) wrapper_subclass = _wrapper_subclass_cache.get(type_obj, _NOT_GIVEN) if wrapper_subclass is _NOT_GIVEN: - slotnames = copy_reg._slotnames(type_obj) + slotnames = copyreg._slotnames(type_obj) if slotnames and not isinstance(obj, _Wrapper): new_type_dict = {'_Wrapper__DERIVED': True} @@ -367,7 +353,7 @@ def __new__(cls, obj, container): if wrapper_subclass: inst = wrapper_subclass(obj, container) else: - inst = super(_Wrapper, cls).__new__(cls) + inst = super().__new__(cls) inst._obj = obj inst._container = container if hasattr(obj, '__dict__') and not isinstance(obj, _Wrapper): @@ -379,7 +365,7 @@ def __new__(cls, obj, container): return inst def __init__(self, obj, container): - super(_Wrapper, self).__init__() + super().__init__() self._obj = obj self._container = container @@ -536,7 +522,6 @@ def __nonzero__(self): type_aq_self = type(aq_self) nonzero = getattr(type_aq_self, '__nonzero__', None) if nonzero is None: - # Py3 bool? nonzero = getattr(type_aq_self, '__bool__', None) if nonzero is None: # a len? @@ -563,14 +548,14 @@ def __str__(self): aq_self = self._obj try: return _rebound_method(aq_self.__str__, self)() - except (AttributeError, TypeError): # pragma: PY3 + except (AttributeError, TypeError): return str(aq_self) def __bytes__(self): aq_self = self._obj try: return _rebound_method(aq_self.__bytes__, self)() - except (AttributeError, TypeError): # pragma: PY3 + except (AttributeError, TypeError): return bytes(aq_self) __binary_special_methods__ = [ @@ -678,8 +663,7 @@ def op(self): def __len__(self): # if len is missing, it should raise TypeError - # (AttributeError is acceptable under Py2, but Py3 - # breaks list conversion if AttributeError is raised) + # (AttributeError breaks list conversion if it is raised) try: l = getattr(type(self._obj), '__len__') except AttributeError: @@ -698,7 +682,7 @@ def __iter__(self): # complains: # (TypeError: 'sequenceiterator' expected, got 'Wrapper' instead) - class WrapperIter(object): + class WrapperIter: __slots__ = ('_wrapper',) def __init__(self, o): @@ -733,15 +717,6 @@ def __setitem__(self, key, value): setter(self, key, value) def __getitem__(self, key): - if isinstance(key, slice) and hasattr(operator, 'getslice'): - # Only on Python 2 - # XXX: This is probably not proxying correctly, but the existing - # tests pass with this behaviour - return operator.getslice( - self._obj, - key.start if key.start is not None else 0, - key.stop if key.stop is not None else sys.maxint) - aq_self = self._obj try: getter = type(aq_self).__getitem__ @@ -783,7 +758,7 @@ class _Acquirer(ExtensionClass.Base): def __getattribute__(self, name): try: - return super(_Acquirer, self).__getattribute__(name) + return super().__getattribute__(name) except AttributeError: # the doctests have very specific error message # requirements (but at least we can preserve the traceback) diff --git a/src/Acquisition/tests.py b/src/Acquisition/tests.py index 33bf354..ed85e63 100644 --- a/src/Acquisition/tests.py +++ b/src/Acquisition/tests.py @@ -14,7 +14,6 @@ """Acquisition test cases (and useful examples) """ -from __future__ import print_function import gc import unittest import sys @@ -39,21 +38,15 @@ IS_PURE, ) -if sys.version_info >= (3,): - PY3 = True - PY2 = False - def unicode(self): - # For test purposes, redirect the unicode - # to the type of the object, just like Py2 did - try: - return type(self).__unicode__(self) - except AttributeError as e: - return type(self).__str__(self) - long = int -else: - PY2 = True - PY3 = False +def unicode(self): + # For test purposes, redirect the unicode + # to the type of the object, just like Py2 did + try: + return type(self).__unicode__(self) + except AttributeError: + return type(self).__str__(self) + if 'Acquisition._Acquisition' not in sys.modules: CAPI = False @@ -62,17 +55,17 @@ def unicode(self): MIXIN_POST_CLASS_DEFINITION = True try: - class Plain(object): + class Plain: pass Plain.__bases__ = (ExtensionClass.Base, ) except TypeError: # Not supported MIXIN_POST_CLASS_DEFINITION = False -AQ_PARENT = unicode('aq_parent') -UNICODE_WAS_CALLED = unicode('unicode was called') -STR_WAS_CALLED = unicode('str was called') -TRUE = unicode('True') +AQ_PARENT = 'aq_parent' +UNICODE_WAS_CALLED = 'unicode was called' +STR_WAS_CALLED = 'str was called' +TRUE = 'True' class I(Implicit): @@ -93,7 +86,7 @@ def __repr__(self): return self.id -class Location(object): +class Location: __parent__ = None @@ -296,12 +289,12 @@ class I(Implicit): # returned, otherwise, the acquisition search continues. # For example, in: - class HandyForTesting(object): + class HandyForTesting: def __init__(self, name): self.name = name def __str__(self): - return "%s(%s)" % (self.name, self.__class__.__name__) + return "{}({})".format(self.name, self.__class__.__name__) __repr__ = __str__ @@ -355,7 +348,7 @@ def __init__(self, name): self.name = name def __str__(self): - return "%s(%s)" % (self.name, self.__class__.__name__) + return "{}({})".format(self.name, self.__class__.__name__) __repr__ = __str__ @@ -1394,7 +1387,7 @@ class TestPickle(unittest.TestCase): def test_cant_pickle_acquisition_wrappers_classic(self): import pickle - class X(object): + class X: def __getstate__(self): return 1 @@ -1431,7 +1424,7 @@ def __getstate__(self): def test_cant_pickle_acquisition_wrappers_newstyle(self): import pickle - class X(object): + class X: def __getstate__(self): return 1 @@ -1471,7 +1464,7 @@ def test_cant_persist_acquisition_wrappers_classic(self): except ImportError: import pickle as cPickle - class X(object): + class X: _p_oid = '1234' def __getstate__(self): @@ -1519,7 +1512,7 @@ def persistent_id(obj): pickler.inst_persistent_id = persistent_id except AttributeError: pass - pickler.persistent_id = persistent_id # PyPy and Py3k + pickler.persistent_id = persistent_id pickler.dump(w) state = file.getvalue() self.assertTrue(b'1234' in state) @@ -1531,7 +1524,7 @@ def test_cant_persist_acquisition_wrappers_newstyle(self): except ImportError: import pickle as cPickle - class X(object): + class X: _p_oid = '1234' def __getstate__(self): @@ -1578,7 +1571,7 @@ def persistent_id(obj): except AttributeError: pass - pickler.persistent_id = persistent_id # PyPy and Py3k + pickler.persistent_id = persistent_id pickler.dump(w) state = file.getvalue() self.assertTrue(b'1234' in state) @@ -1624,7 +1617,7 @@ def test_mixin_post_class_definition(self): # but also doesn't result in any wrappers. from ExtensionClass import Base - class Plain(object): + class Plain: pass self.assertEqual(Plain.__bases__, (object, )) @@ -1653,7 +1646,7 @@ def test_mixin_base(self): # We can mix-in Base as part of multiple inheritance. from ExtensionClass import Base - class MyBase(object): + class MyBase: pass class MixedIn(Base, MyBase): @@ -1727,7 +1720,7 @@ def test_Wrapper_gc(self): for B in I, E: counter = [0] - class C(object): + class C: def __del__(self, counter=counter): counter[0] += 1 @@ -1784,7 +1777,7 @@ def test_container_proxying(): >>> c[5:10] slicing... (5, 10) - >>> c[5:] == (5, sys.maxsize if PY2 else None) + >>> c[5:] == (5, None) slicing... True @@ -1807,7 +1800,7 @@ def test_container_proxying(): >>> i.c[5:10] slicing... (5, 10) - >>> i.c[5:] == (5, sys.maxsize if PY2 else None) + >>> i.c[5:] == (5, None) slicing... True @@ -1850,7 +1843,7 @@ def test_container_proxying(): >>> c[5:10] slicing... (5, 10) - >>> c[5:] == (5, sys.maxsize if PY2 else None) + >>> c[5:] == (5, None) slicing... True @@ -1873,7 +1866,7 @@ def test_container_proxying(): >>> i.c[5:10] slicing... (5, 10) - >>> i.c[5:] == (5, sys.maxsize if PY2 else None) + >>> i.c[5:] == (5, None) slicing... True @@ -2460,8 +2453,7 @@ def __bytes__(self): wrapper = Acquisition.ImplicitAcquisitionWrapper(a.b, a) self.assertEqual(b'my bytes', wrapper.__bytes__()) - if PY3: # pragma: PY3 - self.assertEqual(b'my bytes', bytes(wrapper)) + self.assertEqual(b'my bytes', bytes(wrapper)) def test_AttributeError_if_object_has_no__bytes__(self): class A(Implicit): @@ -2473,9 +2465,8 @@ class A(Implicit): with self.assertRaises(AttributeError): wrapper.__bytes__() - if PY3: # pragma: PY3 - with self.assertRaises(TypeError): - bytes(wrapper) + with self.assertRaises(TypeError): + bytes(wrapper) class TestOf(unittest.TestCase): @@ -2544,7 +2535,7 @@ class A(Implicit): def hi(self): return self.color - class Location(object): + class Location: __parent__ = None b = B() @@ -2637,7 +2628,7 @@ def test_aq_inContextOf_odd_cases(self): Acquisition.ImplicitAcquisitionWrapper) # Following parent pointers in weird circumstances works too: - class WithParent(object): + class WithParent: __parent__ = None self.assertEqual(aq_inContextOf(WithParent(), root), 0) @@ -2665,7 +2656,7 @@ def test_search_repeated_objects(self): from Acquisition import _Wrapper as Wrapper from Acquisition import _Wrapper_acquire - class Repeated(object): + class Repeated: hello = "world" def __repr__(self): @@ -2793,7 +2784,7 @@ def test_wrapper_falls_back_to_default(self): self.assertEqual(aq_acquire(self.a.b.c, 'nonesuch', default=4), 4) def test_no_wrapper_but___parent___falls_back_to_default(self): - class NotWrapped(object): + class NotWrapped: pass child = NotWrapped() child.__parent__ = NotWrapped() @@ -2817,7 +2808,7 @@ class ExtendsBase(Base): def __getattribute__(self, name): if name == 'magic': return 42 - return super(ExtendsBase, self).__getattribute__(name) + return super().__getattribute__(name) class Acquirer(kind, ExtendsBase): pass @@ -2848,7 +2839,7 @@ class TestImplicitWrappingGetattribute(unittest.TestCase): @unittest.skipIf(CAPI, 'Pure Python test.') def test_object_getattribute_in_rebound_method_with_slots(self): - class Persistent(object): + class Persistent: __slots__ = ('__flags',) def __init__(self): @@ -2874,7 +2865,7 @@ def get_flags(self): @unittest.skipIf(CAPI, 'Pure Python test.') def test_type_with_slots_reused(self): - class Persistent(object): + class Persistent: __slots__ = ('__flags',) def __init__(self): @@ -2892,7 +2883,7 @@ def get_flags(self): @unittest.skipIf(CAPI, 'Pure Python test.') def test_object_getattribute_in_rebound_method_with_dict(self): - class Persistent(object): + class Persistent: def __init__(self): self.__flags = 42 @@ -2916,7 +2907,7 @@ def get_flags(self): @unittest.skipIf(CAPI, 'Pure Python test.') def test_object_getattribute_in_rebound_method_with_slots_and_dict(self): - class Persistent(object): + class Persistent: __slots__ = ('__flags', '__dict__') def __init__(self): @@ -2992,6 +2983,7 @@ class TestProxying(unittest.TestCase): __binary_numeric_methods__ = [ '__add__', '__sub__', + '__matmul__', '__mul__', # '__floordiv__', # not implemented in C '__mod__', @@ -3023,6 +3015,7 @@ class TestProxying(unittest.TestCase): # in place '__iadd__', '__isub__', + '__imatmul__', '__imul__', '__idiv__', '__itruediv__', @@ -3040,12 +3033,6 @@ class TestProxying(unittest.TestCase): # '__coerce__', ] - if PY3 and sys.version_info.minor >= 5: - __binary_numeric_methods__.extend([ - '__matmul__', - '__imatmul__' - ]) - __unary_special_methods__ = [ # arithmetic '__neg__', @@ -3058,7 +3045,7 @@ class TestProxying(unittest.TestCase): # conversion '__complex__': complex, '__int__': int, - '__long__': long, + '__long__': int, '__float__': float, '__oct__': oct, '__hex__': hex, @@ -3090,10 +3077,7 @@ def converter(self, *args): acquire_meths[k] = make_converter(convert) acquire_meths['__len__'] = lambda self: self.value - - if PY3: - # Under Python 3, oct() and hex() call __index__ directly - acquire_meths['__index__'] = acquire_meths['__int__'] + acquire_meths['__index__'] = acquire_meths['__int__'] if base_class == Explicit: acquire_meths['value'] = Acquisition.Acquired @@ -3282,7 +3266,7 @@ class B(Implicit): '__ne__', '__ge__', '__le__'] def _never_called(self, other): - raise RuntimeError("This should never be called") + raise AssertionError("This should never be called") class RichCmpNeverCalled(base_class): for _name in rich_cmp_methods: diff --git a/tox.ini b/tox.ini index 2c2b6ff..efb7b45 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,11 @@ [tox] +# If adding or removing envs here please update the testenv:coverage-report +# section as well: envlist = - py27,py27-pure, py36,py36-pure, py37,py37-pure, py38,py38-pure, py39,py39-pure, - pypy, pypy3, coverage @@ -16,7 +16,6 @@ commands = deps = zope.testrunner coverage - coverage-python-version setenv = PIP_NO_CACHE = 1 COVERAGE_FILE=.coverage.{envname} @@ -34,4 +33,9 @@ commands = coverage report -i --fail-under=97.5 deps = coverage - coverage_python_version +depends = + py36,py36-pure, + py37,py37-pure, + py38,py38-pure, + py39,py39-pure, + pypy3,