diff --git a/docs/api/common.rst b/docs/api/common.rst index b04f0675..a83731b1 100644 --- a/docs/api/common.rst +++ b/docs/api/common.rst @@ -26,3 +26,8 @@ implement the correct interface. ================================== .. automodule:: zope.interface.common.sequence + +zope.interface.common.collections +================================= + +.. automodule:: zope.interface.common.collections diff --git a/src/zope/interface/common/__init__.py b/src/zope/interface/common/__init__.py index b711d360..49b22414 100644 --- a/src/zope/interface/common/__init__.py +++ b/src/zope/interface/common/__init__.py @@ -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,), {}) diff --git a/src/zope/interface/common/collections.py b/src/zope/interface/common/collections.py new file mode 100644 index 00000000..a56b913d --- /dev/null +++ b/src/zope/interface/common/collections.py @@ -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) diff --git a/src/zope/interface/common/tests/test_collections.py b/src/zope/interface/common/tests/test_collections.py new file mode 100644 index 00000000..24157315 --- /dev/null +++ b/src/zope/interface/common/tests/test_collections.py @@ -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))()