Skip to content

Commit

Permalink
Add z.i.common.collections to parallel the collections.abc classes.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jamadden committed Feb 17, 2020
1 parent 7f6f60e commit e819c75
Show file tree
Hide file tree
Showing 4 changed files with 487 additions and 2 deletions.
5 changes: 5 additions & 0 deletions docs/api/common.rst
Expand Up @@ -26,3 +26,8 @@ implement the correct interface.
==================================

.. automodule:: zope.interface.common.sequence

zope.interface.common.collections
=================================

.. automodule:: zope.interface.common.collections
128 changes: 126 additions & 2 deletions 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,), {})
211 changes: 211 additions & 0 deletions 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)

0 comments on commit e819c75

Please sign in to comment.