diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 89802b1..4d5caba 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,12 +14,13 @@ jobs: strategy: matrix: python-version: - - "pypy-3.10" + - "pypy-3.11" - "3.11" - "3.12" - "3.13" + - "3.14" extras: - - "[test,docs]" + - "[test,docs,zodb]" # include: # - python-version: "3.13" # extras: "[test,docs,gevent,pyramid]" @@ -46,10 +47,14 @@ jobs: coverage combine || true coverage report -i || true - name: Lint - if: matrix.python-version == '3.12' + if: matrix.python-version == '3.14' run: | python -m pip install -U pylint pylint src + - name: Test without ZODB + run: | + python -m pip uninstall -y ZODB zope.dublincore persistent zope.container BTrees + PURE_PYTHON=1 coverage run -a -m zope.testrunner --test-path=src --auto-color --auto-progress - name: Submit to Coveralls uses: coverallsapp/github-action@v2 with: @@ -74,12 +79,11 @@ jobs: python-version: [3.12] image: - manylinux_2_28_x86_64 - - manylinux2014_aarch64 - - manylinux2014_ppc64le - - manylinux2014_s390x - - manylinux2014_x86_64 - - musllinux_1_1_x86_64 - - musllinux_1_1_aarch64 + - manylinux_2_28_aarch64 + - manylinux_2_28_ppc64le + - manylinux_2_28_s390x + - musllinux_1_2_x86_64 + - musllinux_1_2_aarch64 name: ${{ matrix.image }} steps: diff --git a/CHANGES.rst b/CHANGES.rst index 698bab2..23b7ac2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,12 +3,18 @@ ========= -2.4.1 (unreleased) +2.5.0 (unreleased) ================== - Reduce the logging level for recursive invocations. We handle this case correctly, it did not need to be a warning. - +- Add support for Python 3.14. +- Add the new 'zodb' extra, which installs optional dependencies that + use the ZODB ecosystem: persistent, BTrees, zope.intid, + zope.container, etc. These dependencies are no longer installed by + default. +- No longer build binary wheels for the legacy 'manylinux2014' + standard, only 2_28. Similarly, switch from musllinux_1_1 to 1_2. 2.4.0 (2024-11-11) ================== diff --git a/make-manylinux b/make-manylinux index c6af63b..4837235 100755 --- a/make-manylinux +++ b/make-manylinux @@ -44,13 +44,10 @@ if [ -d /project ] && [ -d /opt/python ]; then OPATH="$PATH" which auditwheel echo @@@@@@@@@@@@@@@@@@@@@@ - echo Will build /opt/python/cp{39,310,311}* /opt/python/pp{39,}* + echo Will build /opt/python/cp{310,311,312,313,314}* + + for variant in `ls -d /opt/python/cp{314,313,310,311,312}*`; do - for variant in `ls -d /opt/python/cp{313,310,311,312}*`; do - if [ "$variant" = "/opt/python/cp313-cp313t" ]; then - echo "Skipping no-gil build. The GIL is required." - continue - fi export PATH="$variant/bin:$OPATH" echo "Building $variant $(python --version)" @@ -64,7 +61,7 @@ if [ -d /project ] && [ -d /opt/python ]; then # XXX: The name of the wheel doesn't match the name of the project PATH="$OPATH" auditwheel repair $WHL - WHL=$(ls wheelhouse/nti.externalization*whl) + WHL=$(ls wheelhouse/*externalization*whl) cp $WHL /project/wheelhouse ls -l /project/wheelhouse @@ -79,5 +76,5 @@ fi # Mount the current directory as /project # Can't use -i on Travis with arm64, "input device not a tty" sname=$(basename "$0") -docker run --rm -e PIP_INDEX_URL -v "$(pwd)/:/project" "${DOCKER_IMAGE:-quay.io/pypa/manylinux2014_x86_64}" /project/"$sname" +docker run --rm -e PIP_INDEX_URL -v "$(pwd)/:/project" "${DOCKER_IMAGE:-quay.io/pypa/manylinux_2_28_x86_64}" /project/"$sname" ls -l wheelhouse diff --git a/pyproject.toml b/pyproject.toml index 9e093fa..da0b6db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ requires = [ # failing in Python 2 (https://travis-ci.org/github/gevent/gevent/jobs/683782800); # This was fixed in 3.0a5 (https://github.com/cython/cython/issues/3578) # 3.0a6 fixes an issue cythonizing source on 32-bit platforms - "Cython >= 3.0.11", + "Cython >= 3.2.1", ] [tool.check-manifest] diff --git a/setup.py b/setup.py index a03f3b5..83e442a 100755 --- a/setup.py +++ b/setup.py @@ -18,10 +18,11 @@ } TESTS_REQUIRE = [ - 'nti.testing', + 'nti.testing >= 4.4.0', 'zope.testrunner', 'manuel', 'pyperf', + 'transaction >= 5.0', ] @@ -152,20 +153,19 @@ def _c(m): author_email='jason@nextthought.com', description="NTI Externalization", long_description=(_read('README.rst') + '\n\n' + _read('CHANGES.rst')), - license='Apache', + license='Apache-2.0', keywords='externalization', classifiers=[ - 'License :: OSI Approved :: Apache Software License', 'Intended Audience :: Developers', 'Natural Language :: English', 'Operating System :: OS Independent', - 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], @@ -179,30 +179,32 @@ def _c(m): 'nti.schema >= 1.17.0', 'PyYAML >= 5.1', - 'ZODB >= 5.5.1', 'isodate', - 'persistent >= 4.7.0', 'pytz', 'simplejson >= 3.19', - 'transaction >= 2.2', + 'transaction', 'zope.component >= 4.6.1', 'zope.configuration >= 4.4.0', - 'zope.container >= 4.4.0', 'zope.dottedname >= 4.3.0', - 'zope.dublincore >= 4.2.0', 'zope.event >= 4.4.0', 'zope.hookable >= 5.0.1', 'zope.interface >= 5.0.1', # getDirectTaggedValue - 'zope.intid >= 4.3.0', 'zope.lifecycleevent >= 4.3.0', 'zope.location >= 4.2.0', 'zope.mimetype >= 2.5.0', 'zope.proxy >= 4.3.5', 'zope.schema >= 6.0.0', 'zope.security >= 5.1.1', - 'BTrees >= 4.8.0', # Registers BTrees as Mapping automatically. ], extras_require={ + 'zodb': [ + 'ZODB >= 5.5.1', + 'persistent >= 4.7.0', + 'zope.container >= 4.4.0', + 'zope.dublincore >= 4.2.0', + 'zope.intid >= 4.3.0', + 'BTrees >= 4.8.0', # Registers BTrees as Mapping automatically. + ], 'test': TESTS_REQUIRE, 'docs': [ 'Sphinx', diff --git a/src/nti/externalization/_compat.py b/src/nti/externalization/_compat.py index a70e9ba..6606310 100644 --- a/src/nti/externalization/_compat.py +++ b/src/nti/externalization/_compat.py @@ -4,12 +4,9 @@ System spanning utilities. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - import os import sys +import logging text_type = str @@ -24,6 +21,19 @@ PURE_PYTHON = PYPY or os.getenv('PURE_PYTHON') or os.getenv("NTI_EXT_PURE_PYTHON") +try: + from zope.dublincore.interfaces import IDCTimes # pylint: disable=unused-import +except ModuleNotFoundError: + from zope.interface import Interface + class IDCTimes(Interface): # pylint: disable=inherit-non-class + """Mock""" + +try: + from ZODB.loglevels import TRACE +except ModuleNotFoundError: + TRACE = 5 + logging.addLevelName(TRACE, "TRACE") + def to_unicode(s, encoding='utf-8', err='strict'): """ Decode a byte sequence and unicode result diff --git a/src/nti/externalization/autopackage.py b/src/nti/externalization/autopackage.py index 0951411..68aff56 100644 --- a/src/nti/externalization/autopackage.py +++ b/src/nti/externalization/autopackage.py @@ -5,21 +5,19 @@ typically via a ZCML directive. """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +import logging -from ZODB.loglevels import TRACE from zope import interface from zope.dottedname import resolve as dottedname from zope.mimetype.interfaces import IContentTypeAware from nti.schema.interfaces import find_most_derived_interface -from nti.externalization.datastructures import ModuleScopedInterfaceObjectIO +from ._compat import TRACE +from .datastructures import ModuleScopedInterfaceObjectIO -logger = __import__('logging').getLogger(__name__) +logger = logging.getLogger(__name__) # If we extend ExtensionClass.Base, __class_init__ is called automatically # for each subclass. But we also start participating in acquisition, which diff --git a/src/nti/externalization/configure.zcml b/src/nti/externalization/configure.zcml index 784c856..ba1d962 100644 --- a/src/nti/externalization/configure.zcml +++ b/src/nti/externalization/configure.zcml @@ -13,12 +13,14 @@ --> - - diff --git a/src/nti/externalization/datastructures.py b/src/nti/externalization/datastructures.py index 94e3491..9efcca1 100644 --- a/src/nti/externalization/datastructures.py +++ b/src/nti/externalization/datastructures.py @@ -543,7 +543,7 @@ class InterfaceObjectIO(AbstractDynamicObjectIO): """ _ext_iface_upper_bound = None - _iface = None + def __init__(self, context, iface_upper_bound=None, validate_after_update=True): """ diff --git a/src/nti/externalization/dublincore.py b/src/nti/externalization/dublincore.py index af8a61d..0b00648 100644 --- a/src/nti/externalization/dublincore.py +++ b/src/nti/externalization/dublincore.py @@ -20,8 +20,15 @@ from zope import component from zope import interface -from zope.dublincore.interfaces import IDCDescriptiveProperties -from zope.dublincore.interfaces import IDCExtended +try: + from zope.dublincore.interfaces import IDCDescriptiveProperties + from zope.dublincore.interfaces import IDCExtended +except ModuleNotFoundError: + # pylint:disable=inherit-non-class + class IDCDescriptiveProperties(interface.Interface): + """Mock""" + class IDCExtended(interface.Interface): + """Mock""" from nti.externalization.interfaces import IExternalStandardDictionaryDecorator from nti.externalization.interfaces import StandardExternalFields diff --git a/src/nti/externalization/externalization/externalizer.py b/src/nti/externalization/externalization/externalizer.py index 66f3814..7fac493 100644 --- a/src/nti/externalization/externalization/externalizer.py +++ b/src/nti/externalization/externalization/externalizer.py @@ -15,18 +15,17 @@ # stdlib imports import warnings -try: - from collections.abc import Set -except ImportError: # Python 2 - # pylint:disable=deprecated-class - from collections import Set - from collections import Mapping -else: # pragma: no cover - from collections.abc import Mapping +from collections.abc import Set +from collections.abc import Mapping from collections import defaultdict from weakref import WeakKeyDictionary -import persistent + +try: + from persistent.list import PersistentList + _PL = (PersistentList,) +except ModuleNotFoundError: + _PL = () from zope.component import queryAdapter from zope.component import getUtility @@ -73,11 +72,10 @@ #: by iterating it and mapping onto a list. This allows :class:`~z3c.batching.interfaces.IBatch` #: to be directly externalized. SEQUENCE_TYPES = ( - persistent.list.PersistentList, Set, list, tuple, -) +) + _PL #: The types that we will treat as mappings for externalization purposes. These #: all map onto a dict. diff --git a/src/nti/externalization/externalization/standard_fields.py b/src/nti/externalization/externalization/standard_fields.py index 6375d31..dd9b899 100644 --- a/src/nti/externalization/externalization/standard_fields.py +++ b/src/nti/externalization/externalization/standard_fields.py @@ -14,10 +14,10 @@ from calendar import timegm as dt_tuple_to_unix -from zope.dublincore.interfaces import IDCTimes from zope.security.management import system_user from zope.security.interfaces import IPrincipal +from nti.externalization._compat import IDCTimes from nti.externalization._base_interfaces import get_standard_external_fields from nti.externalization._base_interfaces import get_standard_internal_fields from nti.externalization._base_interfaces import get_default_externalization_policy diff --git a/src/nti/externalization/internalization/updater.py b/src/nti/externalization/internalization/updater.py index a0b1da6..244c221 100644 --- a/src/nti/externalization/internalization/updater.py +++ b/src/nti/externalization/internalization/updater.py @@ -11,20 +11,18 @@ # stdlib imports -try: - from collections.abc import MutableSequence -except ImportError: # Python 2 - # pylint:disable=deprecated-class - from collections import MutableSequence - from collections import MutableMapping -else: # pragma: no cover - from collections.abc import MutableMapping +from collections.abc import MutableSequence +from collections.abc import MutableMapping import inspect import warnings - -from persistent.interfaces import IPersistent from zope import interface +try: + from persistent.interfaces import IPersistent +except ModuleNotFoundError: + class IPersistent(interface.Interface): # pylint: disable=inherit-non-class + """Mock""" + from zope.event import notify as notify_event from nti.externalization._base_interfaces import PRIMITIVES diff --git a/src/nti/externalization/oids.py b/src/nti/externalization/oids.py index 81cca3f..688cbcc 100644 --- a/src/nti/externalization/oids.py +++ b/src/nti/externalization/oids.py @@ -12,11 +12,19 @@ import binascii import collections - -from ZODB.interfaces import IConnection +try: + from ZODB.interfaces import IConnection +except ModuleNotFoundError: + def IConnection(_): + raise TypeError from zope import component -from zope.intid.interfaces import IIntIds +try: + from zope.intid.interfaces import IIntIds +except ModuleNotFoundError: + from zope.interface import Interface + class IIntIds(Interface): # pylint: disable=inherit-non-class + """Mock""" from nti.externalization._compat import bytes_ diff --git a/src/nti/externalization/persistence.py b/src/nti/externalization/persistence.py index 576ece8..a74ea76 100644 --- a/src/nti/externalization/persistence.py +++ b/src/nti/externalization/persistence.py @@ -22,10 +22,24 @@ import warnings -import persistent -from persistent.list import PersistentList -from persistent.mapping import PersistentMapping -from persistent.wref import WeakRef as PWeakRef +try: + from persistent import UPTODATE + from persistent import CHANGED + from persistent import Persistent + from persistent.list import PersistentList + from persistent.mapping import PersistentMapping + from persistent.wref import WeakRef as PWeakRef +except ModuleNotFoundError as ex: + assert ex.name == 'persistent' + UPTODATE = None + CHANGED = 'Fake Changed' + class Persistent: + """Mock""" + PersistentList = list + PersistentMapping = dict + from weakref import ref + class PWeakRef(ref): + __slots__ = () from zope import interface @@ -68,15 +82,15 @@ def getPersistentState(obj): # Trust the changed value ahead of the state value, # because it is settable from python but the state # is more implicit. - return persistent.CHANGED if obj._p_changed else persistent.UPTODATE + return CHANGED if obj._p_changed else UPTODATE except AttributeError: pass try: - if obj._p_state == persistent.UPTODATE and obj._p_jar is None: + if obj._p_state == UPTODATE and obj._p_jar is None: # In keeping with the pessimistic theme, if it claims to be uptodate, but has never # been saved, we consider that the same as changed - return persistent.CHANGED + return CHANGED except AttributeError: pass @@ -90,7 +104,7 @@ def getPersistentState(obj): try: return obj.getPersistentState() except AttributeError: - return persistent.CHANGED + return CHANGED def setPersistentStateChanged(obj): @@ -187,12 +201,13 @@ class PersistentExternalizableWeakList(PersistentExternalizableList): # pylint:d def __init__(self, initlist=None): if initlist is not None: initlist = [self.__wrap(x) for x in initlist] - super().__init__(initlist) + super().__init__(initlist or ()) def __getitem__(self, i): return super().__getitem__(i)() - # NOTE: __iter__ is implemented with __getitem__ so we don't reimplement. + # NOTE: __iter__ is implemented with __getitem__ so we don't reimplement + # (unless we're subclassing stdlib list) # However, __eq__ isn't, it wants to directly compare lists def __eq__(self, other): # If we just compare lists, weak refs will fail badly @@ -205,6 +220,17 @@ def __eq__(self, other): return all(obj1 == obj2 for obj1, obj2 in izip(self, other)) + if PersistentList is list: + def __iter__(self): + for i in super().__iter__(): + yield i() + + def __mul__(self, n): + # Returns a plain list object. + plain = super().__mul__(n) + return self.__class__(plain) + + __hash__ = None def __wrap(self, obj): @@ -292,7 +318,7 @@ def __reduce_ex__(self, protocol=0): stacklevel=2) setattr(cls, meth, __reduce_ex__) - if issubclass(cls, persistent.Persistent): + if issubclass(cls, Persistent): warnings.warn(RuntimeWarning("Using @NoPickle an a Persistent subclass"), stacklevel=2) diff --git a/src/nti/externalization/representation.py b/src/nti/externalization/representation.py index 47a710c..2b8cf7c 100644 --- a/src/nti/externalization/representation.py +++ b/src/nti/externalization/representation.py @@ -16,8 +16,15 @@ import decimal import warnings -from persistent import Persistent -from ZODB.POSException import POSError +try: + from persistent import Persistent +except ModuleNotFoundError: + class Persistent: + """Mock""" + class POSError(Exception): + """Mock""" +else: + from ZODB.POSException import POSError import simplejson import yaml from zope import component @@ -163,6 +170,8 @@ def _yaml_represent_decimal(dumper, data): pass else: return dumper.represent_int(data) + # TODO: Try replacing these with math.nan and math.inf + # pylint: disable=consider-math-not-float if data.is_nan(): return dumper.represent_float(float('nan')) if data.is_infinite(): diff --git a/src/nti/externalization/tests/test_datastructures.py b/src/nti/externalization/tests/test_datastructures.py index 733a643..d251707 100644 --- a/src/nti/externalization/tests/test_datastructures.py +++ b/src/nti/externalization/tests/test_datastructures.py @@ -659,7 +659,10 @@ def _ext_replacement(self): def test_can_also_subclass_persistent(self): - from persistent import Persistent + try: + from persistent import Persistent + except ModuleNotFoundError: + self.skipTest('persistent not installed') class Base(self._getTargetClass()): pass @@ -727,7 +730,10 @@ def test_unicode_strs_in_dict(self): # Seen in the wild with legacy data. # The unicode path is bad on Python 2, # the bytes path is bad on Python 3. - from persistent import Persistent + try: + from persistent import Persistent + except ModuleNotFoundError: + self.skipTest('persistent not installed') class MappingIO(self._getTargetClass(), Persistent): pass diff --git a/src/nti/externalization/tests/test_dublincore.py b/src/nti/externalization/tests/test_dublincore.py index 9409abe..99f6fb6 100644 --- a/src/nti/externalization/tests/test_dublincore.py +++ b/src/nti/externalization/tests/test_dublincore.py @@ -104,8 +104,9 @@ class Original(object): class TestConfigured(ExternalizationLayerTest): def test_decorate(self): - from zope.dublincore.interfaces import IDCDescriptiveProperties - from zope.dublincore.interfaces import IDCExtended + from ..dublincore import IDCDescriptiveProperties + from ..dublincore import IDCExtended + from zope import interface from nti.externalization.externalization import decorate_external_mapping diff --git a/src/nti/externalization/tests/test_externalization.py b/src/nti/externalization/tests/test_externalization.py index cdf2ed1..fce29eb 100644 --- a/src/nti/externalization/tests/test_externalization.py +++ b/src/nti/externalization/tests/test_externalization.py @@ -12,11 +12,20 @@ import unittest -from ZODB.broken import Broken -import persistent +try: + from ZODB.broken import Broken + from persistent import CHANGED + from persistent import UPTODATE + from zope.dublincore import interfaces as dub_interfaces +except ModuleNotFoundError: + Broken = None + dub_interfaces = None + from ..persistence import CHANGED + from ..persistence import UPTODATE + from zope import component from zope import interface -from zope.dublincore import interfaces as dub_interfaces + from zope.testing.cleanup import CleanUp from nti.externalization.externalization import stripSyntheticKeysFromExternalDictionary @@ -65,10 +74,7 @@ from hamcrest import same_instance from hamcrest import has_property as has_attr -try: - from collections import UserDict -except ImportError: # Python 2 - from UserDict import UserDict +from collections import UserDict # disable: accessing protected members, too many methods @@ -83,20 +89,20 @@ class TestFunctions(ExternalizationLayerTest): def test_getPersistentState(self): # Non-persistent objects are changed - assert_that(getPersistentState(None), is_(persistent.CHANGED)) - assert_that(getPersistentState(object()), is_(persistent.CHANGED)) + assert_that(getPersistentState(None), is_(CHANGED)) + assert_that(getPersistentState(object()), is_(CHANGED)) # Object with _p_changed are that class T(object): _p_changed = True - assert_that(getPersistentState(T()), is_(persistent.CHANGED)) + assert_that(getPersistentState(T()), is_(CHANGED)) T._p_changed = False - assert_that(getPersistentState(T()), is_(persistent.UPTODATE)) + assert_that(getPersistentState(T()), is_(UPTODATE)) # _p_state is trumped by _p_changed T._p_state = None - assert_that(getPersistentState(T()), is_(persistent.UPTODATE)) + assert_that(getPersistentState(T()), is_(UPTODATE)) # _p_state is used if _p_changed isn't del T._p_changed @@ -173,7 +179,10 @@ class C(UserDict, ExternalizableDictionaryMixin): assert_that(toExternalObject(C()), has_entry('Class', 'ExternalC')) def test_broken(self): + if Broken is None: + self.skipTest('ZODb not installed') # Without the devmode hooks + # XXX: Global side effects! This is not safe! gsm = component.getGlobalSiteManager() gsm.unregisterAdapter(factory=DevmodeNonExternalizableObjectReplacementFactory, required=()) @@ -237,7 +246,10 @@ def test_isSyntheticKey(self): assert_that(isSyntheticKey('key'), is_false()) def test_choose_field_POSKeyError_not_ignored(self): - from ZODB.POSException import POSKeyError + try: + from ZODB.POSException import POSKeyError + except ModuleNotFoundError: + self.skipTest('ZODB not installed') class Raises(object): def __getattr__(self, name): raise POSKeyError(name) @@ -362,7 +374,12 @@ def decorateExternalObject(self, *args): class TestPersistentExternalizableWeakList(unittest.TestCase): def test_plus_extend(self): - class C(persistent.Persistent): + try: + from persistent import Persistent + except ModuleNotFoundError: + self.skipTest('Persistent not installed') + + class C(Persistent): pass c1 = C() c2 = C() @@ -603,7 +620,8 @@ def toExternalObject(self, **unused_kwargs): is_not(same_instance(ext_val[1]))) def test_to_stand_dict_uses_dubcore(self): - + if dub_interfaces is None: + self.skipTest('zope.dublincore not installed') @interface.implementer(dub_interfaces.IDCTimes) class X(object): created = datetime.datetime.now() @@ -618,6 +636,8 @@ class X(object): has_entry(StandardExternalFields.CREATED_TIME, is_(Number))) def test_to_stand_dict_uses_dubcore_iso8601(self): + if dub_interfaces is None: + self.skipTest('zope.dublincore not installed') from ..interfaces import ExternalizationPolicy from ..datetime import datetime_to_string policy = ExternalizationPolicy(use_iso8601_for_unix_timestamp=True) @@ -637,6 +657,8 @@ class X(object): has_entry(StandardExternalFields.CREATED_TIME, is_(expected_string))) def test_to_stand_dict_prefers_direct_fields_iso8601(self): + if dub_interfaces is None: + self.skipTest('zope.dublincore not installed') from ..interfaces import ExternalizationPolicy from ..datetime import datetime_to_string policy = ExternalizationPolicy(use_iso8601_for_unix_timestamp=True) @@ -657,6 +679,8 @@ class X(object): has_entry(StandardExternalFields.CREATED_TIME, is_(expected_string))) def test_to_stand_dict_prefers_direct_fields(self): + if dub_interfaces is None: + self.skipTest('zope.dublincore not installed') @interface.implementer(dub_interfaces.IDCTimes) class X(object): created = datetime.datetime.now() @@ -698,6 +722,13 @@ class O(object): assert_that(result, is_({'abc': 42, 'Class': 'O', 'MimeType': 'application/thing'})) def test_name_falls_back_to_standard_name(self): + # Without the devmode hooks + # XXX: Global side effects, this is not safe. + gsm = component.getGlobalSiteManager() + gsm.unregisterAdapter(factory=DevmodeNonExternalizableObjectReplacementFactory, + required=()) + gsm.unregisterAdapter(factory=DevmodeNonExternalizableObjectReplacementFactory, + required=(interface.Interface,)) toExternalObject(self, name='a name') def test_toExternalList(self): @@ -822,7 +853,10 @@ def toExternalObject(self, *_args, **_kwargs): assert_that(s, is_('{"Class": "O", "Creator": "creator"}')) def test_externalize_OOBTree(self): - from BTrees import family64 + try: + from BTrees import family64 + except ModuleNotFoundError: + self.skipTest('BTrees not installed') bt = family64.OO.BTree() bt['key'] = 'value' result = toExternalObject(bt) @@ -830,7 +864,10 @@ def test_externalize_OOBTree(self): assert_that(result, is_({'Class': 'OOBTree', 'key': 'value'})) def test_externalize_PersistentMapping(self): - from persistent.mapping import PersistentMapping + try: + from persistent.mapping import PersistentMapping + except ModuleNotFoundError: + self.skipTest("persistent not installed") pm = PersistentMapping() pm['key'] = 'value' result = toExternalObject(pm) @@ -838,7 +875,10 @@ def test_externalize_PersistentMapping(self): assert_that(result, is_({'Class': 'PersistentMapping', 'key': 'value'})) def test_externalize_IIBTree(self): - from BTrees import family64 + try: + from BTrees import family64 + except ModuleNotFoundError: + self.skipTest('BTrees not installed') bt = family64.II.BTree() bt[1] = 2 result = toExternalObject(bt) diff --git a/src/nti/externalization/tests/test_internalization.py b/src/nti/externalization/tests/test_internalization.py index 14061e0..6cb8046 100644 --- a/src/nti/externalization/tests/test_internalization.py +++ b/src/nti/externalization/tests/test_internalization.py @@ -366,7 +366,10 @@ def test_update_sequence_of_primitives(self): assert_that(result, is_([1, 2, 3])) def test_update_sequence_of_primitives_persistent_contained(self): - from persistent import Persistent + try: + from persistent import Persistent + except ModuleNotFoundError: + self.skipTest('persistent not installed') ext = [1, 2, 3] class O(Persistent): pass @@ -410,7 +413,10 @@ def updateFromExternalObject(self, ext): assert_that(contained, has_property('updated', True)) def test_update_persistent_object(self): - from persistent import Persistent + try: + from persistent import Persistent + except ModuleNotFoundError: + self.skipTest('persistent not installed') external = {} class Obj(Persistent): diff --git a/src/nti/externalization/tests/test_oids.py b/src/nti/externalization/tests/test_oids.py index d6fa4ff..6cb93b5 100644 --- a/src/nti/externalization/tests/test_oids.py +++ b/src/nti/externalization/tests/test_oids.py @@ -25,7 +25,10 @@ class TestToExternalOID(CleanUp, def test_add_to_connection(self): from zope.interface import implementer - from ZODB.interfaces import IConnection + try: + from ZODB.interfaces import IConnection + except ModuleNotFoundError: + self.skipTest('ZODB not installed') @implementer(IConnection) class Persistent(object): @@ -51,8 +54,11 @@ class Persistent(object): def test_intid(self): from zope.interface import implementer - from zope.intid.interfaces import IIntIds from zope import component + try: + from zope.intid.interfaces import IIntIds + except ModuleNotFoundError: + self.skipTest('Intids not installed') @implementer(IIntIds) class IntIds(object): diff --git a/src/nti/externalization/tests/test_persistence.py b/src/nti/externalization/tests/test_persistence.py index ef40c2b..dff8252 100644 --- a/src/nti/externalization/tests/test_persistence.py +++ b/src/nti/externalization/tests/test_persistence.py @@ -9,17 +9,26 @@ import unittest import warnings -import persistent -from persistent import Persistent -from persistent.wref import WeakRef as PWeakRef - -from nti.externalization.persistence import PersistentExternalizableList -from nti.externalization.persistence import PersistentExternalizableDictionary -from nti.externalization.persistence import PersistentExternalizableWeakList -from nti.externalization.persistence import getPersistentState -from nti.externalization.persistence import setPersistentStateChanged -from nti.externalization.persistence import NoPickle -from nti.externalization.tests import ExternalizationLayerTest +try: + from persistent import Persistent + from persistent.wref import WeakRef as PWeakRef + from persistent import UPTODATE + from persistent import CHANGED +except ModuleNotFoundError: + class Persistent: + """Mock""" + MOCK = True + from ..persistence import PWeakRef + from ..persistence import UPTODATE + from ..persistence import CHANGED + +from ..persistence import PersistentExternalizableList +from ..persistence import PersistentExternalizableDictionary +from ..persistence import PersistentExternalizableWeakList +from ..persistence import getPersistentState +from ..persistence import setPersistentStateChanged +from ..persistence import NoPickle +from . import ExternalizationLayerTest from hamcrest import assert_that from hamcrest import calling @@ -55,7 +64,7 @@ def test_mutate(self): # Cannot set non-persistent objects assert_that(calling(obj.append).with_args(object()), - raises(AttributeError)) + raises((AttributeError, TypeError))) pers = Persistent() obj.append(pers) @@ -125,16 +134,17 @@ def toExternalObject(self, **_kw): class TestGetPersistentState(unittest.TestCase): def test_without_jar(self): + class P(object): - _p_state = persistent.UPTODATE + _p_state = UPTODATE _p_jar = None - assert_that(getPersistentState(P), is_(persistent.CHANGED)) + assert_that(getPersistentState(P), is_(CHANGED)) def test_with_proxy_p_changed(self): from zope.proxy import ProxyBase class P(object): - _p_state = persistent.UPTODATE + _p_state = UPTODATE _p_jar = None class MyProxy(ProxyBase): @@ -146,14 +156,14 @@ def _p_changed(self): _p_state = _p_changed proxy = MyProxy(P()) - assert_that(getPersistentState(proxy), is_(persistent.CHANGED)) + assert_that(getPersistentState(proxy), is_(CHANGED)) setPersistentStateChanged(proxy) # Does nothing def test_with_proxy_p_state(self): from zope.proxy import ProxyBase class P(object): - _p_state = persistent.CHANGED + _p_state = CHANGED _p_jar = None class MyProxy(ProxyBase): @@ -163,7 +173,7 @@ def _p_state(self): raise AttributeError() proxy = MyProxy(P()) - assert_that(getPersistentState(proxy), is_(persistent.CHANGED)) + assert_that(getPersistentState(proxy), is_(CHANGED)) setPersistentStateChanged(proxy) # Does nothing @@ -171,7 +181,8 @@ def _p_state(self): class TestWeakRef(unittest.TestCase): def test_to_externalObject(self): - + if hasattr(Persistent, 'MOCK'): + self.skipTest('persistent not installed') class P(Persistent): def toExternalObject(self, **_kwargs): return {'a': 42} @@ -181,7 +192,8 @@ def toExternalObject(self, **_kwargs): assert_that(wref.toExternalObject(), is_({'a': 42})) def test_to_externalOID(self): - + if hasattr(Persistent, 'MOCK'): + self.skipTest('persistent not installed') class P(Persistent): def toExternalOID(self, **_kwargs): return b'abc' @@ -201,7 +213,7 @@ class GlobalSubclassPersistentNoPickle(GlobalPersistentNoPickle): pass @NoPickle -class GlobalNoPickle(object): +class GlobalNoPickle: pass class GlobalSubclassNoPickle(GlobalNoPickle): @@ -222,8 +234,11 @@ class GlobalNoPicklePersistentMixin3(GlobalSubclassNoPickle, class TestNoPickle(unittest.TestCase): def _persist_zodb(self, obj): - from ZODB import DB - from ZODB.MappingStorage import MappingStorage + try: + from ZODB import DB + from ZODB.MappingStorage import MappingStorage + except ModuleNotFoundError: + self.skipTest('ZODB not installed') import transaction db = DB(MappingStorage()) @@ -246,8 +261,12 @@ def _all_persists_fail(self, factory): for meth in (self._persist_zodb, self._persist_pickle,): __traceback_info__ = meth - assert_that(calling(meth).with_args(factory()), - raises(TypeError, "Not allowed to pickle")) + # Got to allow SkipTest through + try: + meth(factory()) + self.fail('Should raise TypeError') + except TypeError as ex: + self.assertTrue('Not allowed to pickle' in str(ex)) def test_plain_object(self): self._all_persists_fail(GlobalNoPickle) @@ -277,6 +296,8 @@ def test_persistent_mixin3(self): self._all_persists_fail(GlobalNoPicklePersistentMixin3) def _check_emits_warning(self, kind): + if hasattr(Persistent, 'MOCK'): + self.skipTest('persistent not installed') with warnings.catch_warnings(record=True) as w: NoPickle(kind) diff --git a/src/nti/externalization/tests/test_proxy.py b/src/nti/externalization/tests/test_proxy.py index c5992cb..bab5e17 100644 --- a/src/nti/externalization/tests/test_proxy.py +++ b/src/nti/externalization/tests/test_proxy.py @@ -10,9 +10,14 @@ from Acquisition import Implicit from ExtensionClass import Base -from zope.container.contained import ContainedProxy from zope.proxy import ProxyBase +try: + from zope.container.contained import ContainedProxy +except ModuleNotFoundError: + ContainedProxy = ProxyBase + + from nti.externalization.proxy import removeAllProxies from nti.testing.matchers import aq_inContextOf @@ -80,7 +85,7 @@ def test_suite(): from unittest import defaultTestLoader suite = defaultTestLoader.loadTestsFromName(__name__) - return unittest.TestSuite([ - suite, - doctest.DocTestSuite('nti.externalization.proxy'), - ]) + suites = [suite] + if ProxyBase is not ContainedProxy: + suites.append(doctest.DocTestSuite('nti.externalization.proxy')) + return unittest.TestSuite(suites) diff --git a/src/nti/externalization/tests/test_representation.py b/src/nti/externalization/tests/test_representation.py index 76e1286..db270fd 100644 --- a/src/nti/externalization/tests/test_representation.py +++ b/src/nti/externalization/tests/test_representation.py @@ -12,7 +12,10 @@ import unittest from unittest.mock import patch as Patch -from persistent import Persistent +try: + from persistent import Persistent +except ModuleNotFoundError: + Persistent = None from . import ExternalizationLayerTest @@ -25,9 +28,15 @@ # disable: accessing protected members, too many methods # pylint: disable=W0212,R0904 # pylint:disable=attribute-defined-outside-init, useless-object-inheritance +# pylint:disable=consider-math-not-float class TestWithRepr(unittest.TestCase): + def setUp(self): + super().setUp() + if Persistent is None: + self.skipTest('Persistent not installed') + def test_default(self): @representation.WithRepr @@ -59,7 +68,7 @@ class Foo(object): def test_raises_POSError(self): def raise_(unused_instance): - from ZODB.POSException import ConnectionStateError + from ZODB.POSException import ConnectionStateError # pylint:disable=import-error raise ConnectionStateError() @representation.WithRepr(raise_) diff --git a/src/nti/externalization/zcml.py b/src/nti/externalization/zcml.py index 7cdf333..b3defd1 100644 --- a/src/nti/externalization/zcml.py +++ b/src/nti/externalization/zcml.py @@ -37,11 +37,8 @@ """ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function +import logging -from ZODB import loglevels from zope import interface from zope.component import zcml as component_zcml from zope.configuration.fields import Bool @@ -51,6 +48,7 @@ from zope.configuration.fields import MessageID from zope.configuration.fields import PythonIdentifier +from ._compat import TRACE from .interfaces import _ILegacySearchModuleFactory from .autopackage import AutoPackageSearchingScopedInterfaceObjectIO from .factory import MimeObjectFactory @@ -63,7 +61,7 @@ __docformat__ = "restructuredtext en" -logger = __import__('logging').getLogger(__name__) +logger = logging.getLogger(__name__) # pylint: disable=protected-access,inherit-non-class @@ -113,7 +111,7 @@ def registerMimeFactories(_context, module): continue if mime_type: - logger.log(loglevels.TRACE, + logger.log(TRACE, "Registered mime factory utility %s = %s (%s)", object_name, value, mime_type) factory = MimeObjectFactory(value, @@ -226,7 +224,7 @@ def _ap_find_package_name(_cls): cls_iio.__module__ = _context.package.__name__ if _context.package else '__dynamic__' for iface in root_interfaces: - logger.log(loglevels.TRACE, + logger.log(TRACE, "Registering ObjectIO for %s as %s", iface, cls_iio) component_zcml.adapter(_context, factory=(cls_iio,), for_=(iface,)) @@ -250,7 +248,7 @@ def _ap_find_package_name(_cls): # Now that it's initted, register the factories for module in (factory_modules or modules): - logger.log(loglevels.TRACE, + logger.log(TRACE, "Examining module %s for mime factories", module) registerMimeFactories(_context, module) diff --git a/tox.ini b/tox.ini index 8837018..c51243a 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ commands = zope-testrunner --test-path=src [] extras = test + zodb setenv = pure: PURE_PYTHON=1 ZOPE_INTERFACE_STRICT_IRO=1 @@ -25,6 +26,10 @@ deps = setenv = PURE_PYTHON = 1 +[testenv:py314] +extras = + test + [testenv:docs] commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html