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:
parent
7f6f60e824
commit
e819c75e60
|
@ -26,3 +26,8 @@ implement the correct interface.
|
|||
==================================
|
||||
|
||||
.. automodule:: zope.interface.common.sequence
|
||||
|
||||
zope.interface.common.collections
|
||||
=================================
|
||||
|
||||
.. automodule:: zope.interface.common.collections
|
||||
|
|
|
@ -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,), {})
|
||||
|
|
|
@ -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)
|
|
@ -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))()
|
Loading…
Reference in New Issue