diff options
| author | Jason Madden <jamadden@gmail.com> | 2020-02-11 06:44:46 -0600 |
|---|---|---|
| committer | Jason Madden <jamadden@gmail.com> | 2020-02-17 07:01:03 -0600 |
| commit | 653e24f53650810bb6a8ff401477e0e03ab84aa0 (patch) | |
| tree | c471f8cfff4b8dcc84f966d1ec7fad06d2981075 /src | |
| parent | e819c75e609781848d0ba3c7301b352e0cb93d88 (diff) | |
| download | zope-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__.py | 120 | ||||
| -rw-r--r-- | src/zope/interface/common/collections.py | 39 | ||||
| -rw-r--r-- | src/zope/interface/common/tests/test_collections.py | 47 |
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() |
