Skip to content

Commit

Permalink
Add collections.IByteString and refactor to avoid one-to-one assumpti…
Browse files Browse the repository at this point in the history
…on about ABCs and builtins.

bytearray turns out to violate that.
  • Loading branch information
jamadden committed Feb 11, 2020
1 parent bef8384 commit b27ff65
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 45 deletions.
120 changes: 94 additions & 26 deletions 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
Expand All @@ -11,22 +22,6 @@
# 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
Expand All @@ -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.
Expand Down Expand Up @@ -100,26 +159,35 @@ def __method_from_function(self, function, name):
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)
Expand Down
39 changes: 34 additions & 5 deletions src/zope/interface/common/collections.py
Expand Up @@ -33,24 +33,43 @@

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',
Expand Down Expand Up @@ -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__():
Expand All @@ -111,7 +130,7 @@ def __reversed__():

class IGenerator(IIterator):
# New in 3.5
abc = _new_in_ver('Generator', PY35)
abc = _new_in_ver('Generator', PY35, (IIterator.getABC(),))


class ISized(ABCInterface):
Expand All @@ -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,
Expand All @@ -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

Expand Down
47 changes: 33 additions & 14 deletions src/zope/interface/common/tests/test_collections.py
Expand Up @@ -25,13 +25,33 @@

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)
Expand All @@ -50,12 +70,10 @@ def verify(self, iface, klass, **kwargs):
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,
Expand Down Expand Up @@ -107,17 +125,18 @@ def test_list(self):

@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()

Expand Down

0 comments on commit b27ff65

Please sign in to comment.