Add z.i.common.collections to parallel the collections.abc classes.

Register implemented standard library types on import.

Derive the interface methods and documentation from the ABC automatically. I hope to use this for numbers too.

Part of #138
This commit is contained in:
Jason Madden 2020-02-10 15:37:25 -06:00
parent 7f6f60e824
commit e819c75e60
No known key found for this signature in database
GPG Key ID: 349F84431A08B99E
4 changed files with 487 additions and 2 deletions

View File

@ -26,3 +26,8 @@ implement the correct interface.
==================================
.. automodule:: zope.interface.common.sequence
zope.interface.common.collections
=================================
.. automodule:: zope.interface.common.collections

View File

@ -1,2 +1,126 @@
#
# This file is necessary to make this directory a package.
from weakref import WeakKeyDictionary
from types import FunctionType
from zope.interface import classImplements
from zope.interface import Interface
from zope.interface.interface import fromFunction
from zope.interface.interface import InterfaceClass
from zope.interface.interface import _decorator_non_return
__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
# pylint:disable=unexpected-special-method-signature
def optional(meth):
# Apply this decorator to a method definition to make it
# optional (remove it from the list of required names), overriding
# the definition inherited from the ABC.
return _decorator_non_return
class ABCInterfaceClass(InterfaceClass):
def __init__(self, name, bases, attrs):
# go ahead and give us a name to ease debugging.
self.__name__ = name
based_on = attrs.pop('abc')
if based_on is None:
# An ABC from the future, not available to us.
methods = {
'__doc__': 'This ABC is not available.'
}
else:
assert name[1:] == based_on.__name__, (name, based_on)
methods = {
# Passing the name is important in case of aliases,
# e.g., ``__ror__ = __or__``.
k: self.__method_from_function(v, k)
for k, v in vars(based_on).items()
if isinstance(v, FunctionType) and not self.__is_private_name(k)
and not self.__is_reverse_protocol_name(k)
}
methods['__doc__'] = "See `%s.%s`" % (
based_on.__module__,
based_on.__name__,
)
# Anything specified in the body takes precedence.
# This lets us remove things that are rarely, if ever,
# actually implemented. For example, ``tuple`` is registered
# as an Sequence, but doesn't implement the required ``__reversed__``
# method, but that's OK, it still works with the ``reversed()`` builtin
# because it has ``__len__`` and ``__getitem__``.
methods.update(attrs)
InterfaceClass.__init__(self, name, bases, methods)
self.__abc = based_on
self.__register_classes()
@staticmethod
def __is_private_name(name):
if name.startswith('__') and name.endswith('__'):
return False
return name.startswith('_')
@staticmethod
def __is_reverse_protocol_name(name):
# The reverse names, like __rand__,
# aren't really part of the protocol. The interpreter has
# very complex behaviour around invoking those. PyPy
# doesn't always even expose them as attributes.
return name.startswith('__r') and name.endswith('__')
def __method_from_function(self, function, name):
method = fromFunction(function, self, name=name)
# Eliminate the leading *self*, which is implied in
# an interface, but explicit in an ABC.
method.positional = method.positional[1:]
return method
def __register_classes(self):
# Make the concrete classes already present in our ABC's registry
# declare that they implement this interface.
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.?.
# 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
ABCInterface = ABCInterfaceClass.__new__(ABCInterfaceClass, None, None, None)
InterfaceClass.__init__(ABCInterface, 'ABCInterface', (Interface,), {})

View File

@ -0,0 +1,211 @@
##############################################################################
# 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.
##############################################################################
"""
Interface definitions paralleling the abstract base classes defined in
:mod:`collections.abc`.
After this module is imported, the standard library types will declare
that they implement the appropriate interface. While most standard
library types will properly implement that interface (that
is, ``verifyObject(ISequence, list()))`` will pass, for example), a few might not:
- `memoryview` doesn't feature all the defined methods of
``ISequence`` such as ``count``; it is still declared to provide
``ISequence`` though.
- `collections.deque.pop` doesn't accept the ``index`` argument of
`collections.abc.MutableSequence.pop`
- `range.index` does not accept the ``start`` and ``stop`` arguments.
.. versionadded:: 5.0.0
"""
from __future__ import absolute_import
import sys
try:
from collections import abc
except ImportError:
import collections as abc
from zope.interface._compat import PYTHON2 as PY2
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
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
__all__ = [
'IAsyncGenerator',
'IAsyncIterable',
'IAsyncIterator',
'IAwaitable',
'ICollection',
'IContainer',
'ICoroutine',
'IGenerator',
'IHashable',
'IItemsView',
'IIterable',
'IIterator',
'IKeysView',
'IMapping',
'IMappingView',
'IMutableMapping',
'IMutableSequence',
'IMutableSet',
'IReversible',
'ISequence',
'ISet',
'ISized',
'IValuesView',
]
class IContainer(ABCInterface):
abc = abc.Container
@optional
def __contains__(other):
"""
Optional method. If not provided, the interpreter will use
``__iter__`` or the old ``__len__`` and ``__getitem__`` protocol
to implement ``in``.
"""
class IHashable(ABCInterface):
abc = abc.Hashable
class IIterable(ABCInterface):
abc = abc.Iterable
class IIterator(IIterable):
abc = abc.Iterator
class IReversible(IIterable):
abc = _new_in_ver('Reversible', PY36)
@optional
def __reversed__():
"""
Optional method. If this isn't present, the interpreter
will use ``__len__`` and ``__getitem__`` to implement the
`reversed` builtin.`
"""
class IGenerator(IIterator):
# New in 3.5
abc = _new_in_ver('Generator', PY35)
class ISized(ABCInterface):
abc = abc.Sized
# ICallable is not defined because there's no standard signature.
class ICollection(ISized,
IIterable,
IContainer):
abc = _new_in_ver('Collection', PY36)
class ISequence(IReversible,
ICollection):
abc = abc.Sequence
@optional
def __reversed__():
"""
Optional method. If this isn't present, the interpreter
will use ``__len__`` and ``__getitem__`` to implement the
`reversed` builtin.`
"""
class IMutableSequence(ISequence):
abc = abc.MutableSequence
class ISet(ICollection):
abc = abc.Set
class IMutableSet(ISet):
abc = abc.MutableSet
class IMapping(ICollection):
abc = abc.Mapping
if PY2:
@optional
def __eq__(other):
"""
The interpreter will supply one.
"""
__ne__ = __eq__
class IMutableMapping(IMapping):
abc = abc.MutableMapping
class IMappingView(ISized):
abc = abc.MappingView
class IItemsView(IMappingView, ISet):
abc = abc.ItemsView
class IKeysView(IMappingView, ISet):
abc = abc.KeysView
class IValuesView(IMappingView, ICollection):
abc = abc.ValuesView
@optional
def __contains__(other):
"""
Optional method. If not provided, the interpreter will use
``__iter__`` or the old ``__len__`` and ``__getitem__`` protocol
to implement ``in``.
"""
class IAwaitable(ABCInterface):
abc = _new_in_ver('Awaitable', PY35)
class ICoroutine(IAwaitable):
abc = _new_in_ver('Coroutine', PY35)
class IAsyncIterable(ABCInterface):
abc = _new_in_ver('AsyncIterable', PY35)
class IAsyncIterator(IAsyncIterable):
abc = _new_in_ver('AsyncIterator', PY35)
class IAsyncGenerator(IAsyncIterator):
abc = _new_in_ver('AsyncGenerator', PY36)

View File

@ -0,0 +1,145 @@
##############################################################################
# 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.
##############################################################################
import unittest
try:
import collections.abc as abc
except ImportError:
import collections as abc
from collections import deque
try:
from types import MappingProxyType
except ImportError:
MappingProxyType = object()
from zope.interface.verify import verifyClass
from zope.interface.verify import verifyObject
# 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
class TestVerifyClass(unittest.TestCase):
verifier = staticmethod(verifyClass)
def _adjust_object_before_verify(self, iface, x):
return x
def verify(self, iface, klass, **kwargs):
return self.verifier(iface,
self._adjust_object_before_verify(iface, klass),
**kwargs)
# Here we test some known builtin classes that are defined to implement
# various collection interfaces as a quick sanity test.
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,
# it could contain things from outside of there too. We aren't concerned
# about third-party code here, just standard library types. We start with a
# blacklist of things to exclude, but if that gets out of hand we can figure
# out a better whitelisting.
_UNVERIFIABLE = {
# This is declared to be an ISequence, but is missing lots of methods,
# including some that aren't part of a language protocol, such as
# ``index`` and ``count``.
memoryview,
# 'pkg_resources._vendor.pyparsing.ParseResults' is registered as a
# MutableMapping but is missing methods like ``popitem`` and ``setdefault``.
# It's imported due to namespace packages.
'ParseResults',
# sqlite3.Row claims ISequence but also misses ``index`` and ``count``.
# It's imported because...? Coverage imports it, but why do we have it without
# coverage?
'Row',
}
if PYPY:
_UNVERIFIABLE.update({
# collections.deque.pop() doesn't support the index= argument to
# MutableSequence.pop(). We can't verify this on CPython because we can't
# get the signature, but on PyPy we /can/ get the signature, and of course
# it doesn't match.
deque,
# Likewise for index
range,
})
if PY2:
# pylint:disable=undefined-variable,no-member
# There are a lot more types that are fundamentally unverifiable on Python 2.
_UNVERIFIABLE.update({
# Missing several key methods like __getitem__
basestring,
# Missing __iter__ and __contains__, hard to construct.
buffer,
# Missing ``__contains__``, ``count`` and ``index``.
xrange,
# These two are missing Set.isdisjoint()
type({}.viewitems()),
type({}.viewkeys()),
# str is missing __iter__!
str,
})
@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)
TestVerifyClass.gen_tests()
class TestVerifyObject(TestVerifyClass):
verifier = staticmethod(verifyObject)
_CONSTRUCTORS = {
collections.IValuesView: {}.values,
collections.IItemsView: {}.items,
collections.IKeysView: {}.keys,
memoryview: lambda: memoryview(b'abc'),
range: lambda: range(10),
MappingProxyType: lambda: MappingProxyType({})
}
if PY2:
# pylint:disable=undefined-variable,no-member
_CONSTRUCTORS.update({
collections.IValuesView: {}.viewvalues,
})
def _adjust_object_before_verify(self, iface, x):
return self._CONSTRUCTORS.get(iface,
self._CONSTRUCTORS.get(x, x))()