summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2020-02-11 06:44:46 -0600
committerJason Madden <jamadden@gmail.com>2020-02-17 07:01:03 -0600
commit653e24f53650810bb6a8ff401477e0e03ab84aa0 (patch)
treec471f8cfff4b8dcc84f966d1ec7fad06d2981075 /src
parente819c75e609781848d0ba3c7301b352e0cb93d88 (diff)
downloadzope-interface-653e24f53650810bb6a8ff401477e0e03ab84aa0.tar.gz
Add collections.IByteString and refactor to avoid one-to-one assumption about ABCs and builtins.
bytearray turns out to violate that.
Diffstat (limited to 'src')
-rw-r--r--src/zope/interface/common/__init__.py120
-rw-r--r--src/zope/interface/common/collections.py39
-rw-r--r--src/zope/interface/common/tests/test_collections.py47
3 files changed, 161 insertions, 45 deletions
diff --git a/src/zope/interface/common/__init__.py b/src/zope/interface/common/__init__.py
index 49b2241..d4c41ee 100644
--- a/src/zope/interface/common/__init__.py
+++ b/src/zope/interface/common/__init__.py
@@ -1,4 +1,15 @@
-from weakref import WeakKeyDictionary
+##############################################################################
+# Copyright (c) 2020 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+##############################################################################
+
from types import FunctionType
from zope.interface import classImplements
@@ -11,22 +22,6 @@ __all__ = [
# Nothing public here.
]
-# Map of standard library class to its primary
-# interface. We assume there's a simple linearization
-# so that each standard library class can be represented
-# by a single interface.
-# TODO: Maybe store this in the individual interfaces? We're
-# only really keeping this around for test purposes.
-stdlib_class_registry = WeakKeyDictionary()
-
-def stdlib_classImplements(cls, iface):
- # Execute ``classImplements(cls, iface)`` and record
- # that in the registry for validation by tests.
- if cls in stdlib_class_registry:
- raise KeyError(cls)
- stdlib_class_registry[cls] = iface
- classImplements(cls, iface)
-
# pylint:disable=inherit-non-class,
# pylint:disable=no-self-argument,no-method-argument
@@ -40,6 +35,70 @@ def optional(meth):
class ABCInterfaceClass(InterfaceClass):
+ """
+ An interface that is automatically derived from a
+ :class:`abc.ABCMeta` type.
+
+ Internal use only.
+
+ When created, any existing classes that are registered to conform
+ to the ABC are declared to implement this interface. This is *not*
+ automatically updated as the ABC registry changes.
+
+ Note that this is not fully symmetric. For example, it is usually
+ the case that a subclass relationship carries the interface
+ declarations over::
+
+ >>> from zope.interface import Interface
+ >>> class I1(Interface):
+ ... pass
+ ...
+ >>> from zope.interface import implementer
+ >>> @implementer(I1)
+ ... class Root(object):
+ ... pass
+ ...
+ >>> class Child(Root):
+ ... pass
+ ...
+ >>> child = Child()
+ >>> isinstance(child, Root)
+ True
+ >>> from zope.interface import providedBy
+ >>> list(providedBy(child))
+ [<InterfaceClass __main__.I1>]
+
+ However, that's not the case with ABCs and ABC interfaces. Just
+ because ``isinstance(A(), AnABC)`` and ``isinstance(B(), AnABC)``
+ are both true, that doesn't mean there's any class hierarchy
+ relationship between ``A`` and ``B``, or between either of them
+ and ``AnABC``. Thus, if ``AnABC`` implemented ``IAnABC``, it would
+ not follow that either ``A`` or ``B`` implements ``IAnABC`` (nor
+ their instances provide it)::
+
+ >>> class SizedClass(object):
+ ... def __len__(self): return 1
+ ...
+ >>> from collections.abc import Sized
+ >>> isinstance(SizedClass(), Sized)
+ True
+ >>> from zope.interface import classImplements
+ >>> classImplements(Sized, I1)
+ None
+ >>> list(providedBy(SizedClass()))
+ []
+
+ Thus, to avoid conflicting assumptions, ABCs should not be
+ declared to implement their parallel ABC interface. Only concrete
+ classes specifically registered with the ABC should be declared to
+ do so.
+
+ .. verisonadded:: 5.0
+ """
+
+ # If we could figure out invalidation, and used some special
+ # Specification/Declaration instances, and override the method ``providedBy`` here,
+ # perhaps we could more closely integrate with ABC virtual inheritance?
def __init__(self, name, bases, attrs):
# go ahead and give us a name to ease debugging.
@@ -100,26 +159,35 @@ class ABCInterfaceClass(InterfaceClass):
def __register_classes(self):
# Make the concrete classes already present in our ABC's registry
# declare that they implement this interface.
+
+ for cls in self.getRegisteredConformers():
+ classImplements(cls, self)
+
+ def getABC(self):
+ """
+ Return the ABC this interface represents.
+ """
+ return self.__abc
+
+ def getRegisteredConformers(self):
+ """
+ Return an iterable of the classes that are directly
+ registered to conform to the ABC this interface
+ parallels.
+ """
based_on = self.__abc
- if based_on is None:
- return
try:
registered = list(based_on._abc_registry)
except AttributeError:
- # Rewritten in C in Python 3.?.
+ # Rewritten in C in CPython 3.7.
# These expose the underlying weakref.
from abc import _get_dump
registry = _get_dump(based_on)[0]
registered = [x() for x in registry]
registered = [x for x in registered if x is not None]
- for cls in registered:
- stdlib_classImplements(cls, self)
-
- def getABC(self):
- """Return the ABC this interface represents."""
- return self.__abc
+ return registered
ABCInterface = ABCInterfaceClass.__new__(ABCInterfaceClass, None, None, None)
diff --git a/src/zope/interface/common/collections.py b/src/zope/interface/common/collections.py
index a56b913..6f5969d 100644
--- a/src/zope/interface/common/collections.py
+++ b/src/zope/interface/common/collections.py
@@ -33,24 +33,43 @@ from __future__ import absolute_import
import sys
+from abc import ABCMeta
try:
from collections import abc
except ImportError:
import collections as abc
from zope.interface._compat import PYTHON2 as PY2
+from zope.interface._compat import PYTHON3 as PY3
from zope.interface.common import ABCInterface
from zope.interface.common import optional
# pylint:disable=inherit-non-class,
# pylint:disable=no-self-argument,no-method-argument
# pylint:disable=unexpected-special-method-signature
+# pylint:disable=no-value-for-parameter
PY35 = sys.version_info[:2] >= (3, 5)
PY36 = sys.version_info[:2] >= (3, 6)
-def _new_in_ver(name, ver):
- return getattr(abc, name) if ver else None
+def _new_in_ver(name, ver,
+ bases_if_missing=(ABCMeta,),
+ register_if_missing=()):
+ if ver:
+ return getattr(abc, name)
+
+ # TODO: It's a shame to have to repeat the bases when
+ # the ABC is missing. Can we DRY that?
+ missing = ABCMeta(name, bases_if_missing, {
+ '__doc__': "The ABC %s is not defined in this version of Python." % (
+ name
+ ),
+ })
+
+ for c in register_if_missing:
+ missing.register(c)
+
+ return missing
__all__ = [
'IAsyncGenerator',
@@ -99,7 +118,7 @@ class IIterator(IIterable):
abc = abc.Iterator
class IReversible(IIterable):
- abc = _new_in_ver('Reversible', PY36)
+ abc = _new_in_ver('Reversible', PY36, (IIterable.getABC(),))
@optional
def __reversed__():
@@ -111,7 +130,7 @@ class IReversible(IIterable):
class IGenerator(IIterator):
# New in 3.5
- abc = _new_in_ver('Generator', PY35)
+ abc = _new_in_ver('Generator', PY35, (IIterator.getABC(),))
class ISized(ABCInterface):
@@ -124,7 +143,8 @@ class ISized(ABCInterface):
class ICollection(ISized,
IIterable,
IContainer):
- abc = _new_in_ver('Collection', PY36)
+ abc = _new_in_ver('Collection', PY36,
+ (ISized.getABC(), IIterable.getABC(), IContainer.getABC()))
class ISequence(IReversible,
@@ -144,6 +164,15 @@ class IMutableSequence(ISequence):
abc = abc.MutableSequence
+class IByteString(ISequence):
+ """
+ This unifies `bytes` and `bytearray`.
+ """
+ abc = _new_in_ver('ByteString', PY3,
+ (ISequence.getABC(),),
+ (bytes, bytearray))
+
+
class ISet(ICollection):
abc = abc.Set
diff --git a/src/zope/interface/common/tests/test_collections.py b/src/zope/interface/common/tests/test_collections.py
index 2415731..8fd1d0c 100644
--- a/src/zope/interface/common/tests/test_collections.py
+++ b/src/zope/interface/common/tests/test_collections.py
@@ -25,13 +25,33 @@ except ImportError:
from zope.interface.verify import verifyClass
from zope.interface.verify import verifyObject
+from zope.interface.common import ABCInterface
+from zope.interface.common import ABCInterfaceClass
# Note that importing z.i.c.collections does work on import.
from zope.interface.common import collections
-from zope.interface.common import stdlib_class_registry
+
from zope.interface._compat import PYPY
from zope.interface._compat import PYTHON2 as PY2
+def walk_abc_interfaces():
+ # Note that some builtin classes are registered for two distinct
+ # parts of the ABC/interface tree. For example, bytearray is both ByteString
+ # and MutableSequence.
+ seen = set()
+ stack = list(ABCInterface.dependents) # subclasses, but also implementedBy objects
+ while stack:
+ iface = stack.pop(0)
+ if iface in seen or not isinstance(iface, ABCInterfaceClass):
+ continue
+ seen.add(iface)
+ stack.extend(list(iface.dependents))
+
+ registered = list(iface.getRegisteredConformers())
+ if registered:
+ yield iface, registered
+
+
class TestVerifyClass(unittest.TestCase):
verifier = staticmethod(verifyClass)
@@ -50,12 +70,10 @@ class TestVerifyClass(unittest.TestCase):
def test_frozenset(self):
self.assertIsInstance(frozenset(), abc.Set)
self.assertTrue(self.verify(collections.ISet, frozenset))
- self.assertIn(frozenset, stdlib_class_registry)
def test_list(self):
self.assertIsInstance(list(), abc.MutableSequence)
self.assertTrue(self.verify(collections.IMutableSequence, list))
- self.assertIn(list, stdlib_class_registry)
# Now we go through the registry, which should have several things,
# mostly builtins, but if we've imported other libraries already,
@@ -107,17 +125,18 @@ class TestVerifyClass(unittest.TestCase):
@classmethod
def gen_tests(cls):
- for stdlib_class, iface in stdlib_class_registry.items():
- if stdlib_class in cls._UNVERIFIABLE or stdlib_class.__name__ in cls._UNVERIFIABLE:
- continue
-
- def test(self, stdlib_class=stdlib_class, iface=iface):
- self.assertTrue(self.verify(iface, stdlib_class))
-
- name = 'test_auto_' + stdlib_class.__name__
- test.__name__ = name
- assert not hasattr(cls, name)
- setattr(cls, name, test)
+ for iface, registered_classes in walk_abc_interfaces():
+ for stdlib_class in registered_classes:
+ if stdlib_class in cls._UNVERIFIABLE or stdlib_class.__name__ in cls._UNVERIFIABLE:
+ continue
+
+ def test(self, stdlib_class=stdlib_class, iface=iface):
+ self.assertTrue(self.verify(iface, stdlib_class))
+
+ name = 'test_auto_' + stdlib_class.__name__ + '_' + iface.__name__
+ test.__name__ = name
+ assert not hasattr(cls, name)
+ setattr(cls, name, test)
TestVerifyClass.gen_tests()