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