Skip to content

Commit

Permalink
Merge pull request #175 from zopefoundation/issue138
Browse files Browse the repository at this point in the history
Add more common interfaces
  • Loading branch information
jamadden authored Feb 21, 2020
2 parents 7f6f60e + 2b49157 commit c931999
Show file tree
Hide file tree
Showing 15 changed files with 1,338 additions and 39 deletions.
18 changes: 18 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,24 @@
Like the above, this will break consumers depending on the exact
output of error messages if more than one error is present.

- Add ``zope.interface.common.collections``,
``zope.interface.common.numbers``, and ``zope.interface.common.io``.
These modules define interfaces based on the ABCs defined in the
standard library ``collections.abc``, ``numbers`` and ``io``
modules, respectively. Importing these modules will make the
standard library concrete classes that are registered with those
ABCs declare the appropriate interface. See `issue 138
<https://github.com/zopefoundation/zope.interface/issues/138>`_.

- Add ``zope.interface.common.builtins``. This module defines
interfaces of common builtin types, such as ``ITextString`` and
``IByteString``, ``IDict``, etc. These interfaces extend the
appropriate interfaces from ``collections`` and ``numbers``, and the
standard library classes implement them after importing this module.
This is intended as a replacement for third-party packages like
`dolmen.builtins <https://pypi.org/project/dolmen.builtins/>`_.
See `issue 138 <https://github.com/zopefoundation/zope.interface/issues/138>`_.


4.7.1 (2019-11-11)
==================
Expand Down
38 changes: 30 additions & 8 deletions docs/api/common.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,47 @@

The ``zope.interface.common`` package provides interfaces for objects
distributed as part of the Python standard library. Importing these
modules has the effect of making the standard library objects
modules (usually) has the effect of making the standard library objects
implement the correct interface.

``zope.interface.common.interfaces``
====================================
zope.interface.common.interface
===============================

.. automodule:: zope.interface.common.interfaces

``zope.interface.common.idatetime``
===================================
zope.interface.common.idatetime
===============================

.. automodule:: zope.interface.common.idatetime

``zope.interface.common.mapping``
zope.interface.common.collections
=================================

.. automodule:: zope.interface.common.collections

zope.interface.common.numbers
=============================

.. automodule:: zope.interface.common.numbers

zope.interface.common.builtins
==============================

.. automodule:: zope.interface.common.builtins

zope.interface.common.io
========================

.. automodule:: zope.interface.common.io

.. Deprecated or discouraged modules below this
zope.interface.common.mapping
=============================

.. automodule:: zope.interface.common.mapping

``zope.interface.common.sequence``
==================================
zope.interface.common.sequence
==============================

.. automodule:: zope.interface.common.sequence
259 changes: 258 additions & 1 deletion src/zope/interface/common/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,259 @@
##############################################################################
# Copyright (c) 2020 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This file is necessary to make this directory a package.
# 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 itertools
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.
]


# pylint:disable=inherit-non-class,
# pylint:disable=no-self-argument,no-method-argument
# pylint:disable=unexpected-special-method-signature

class optional(object):
# 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.
def __init__(self, method):
self.__doc__ = method.__doc__


class ABCInterfaceClass(InterfaceClass):
"""
An interface that is automatically derived from a
:class:`abc.ABCMeta` type.
Internal use only.
The body of the interface definition *must* define
a property ``abc`` that is the ABC to base the interface on.
If ``abc`` is *not* in the interface definition, a regular
interface will be defined instead (but ``extra_classes`` is still
respected).
Use the ``@optional`` decorator on method definitions if
the ABC defines methods that are not actually required in all cases
because the Python language has multiple ways to implement a protocol.
For example, the ``iter()`` protocol can be implemented with
``__iter__`` or the pair ``__len__`` and ``__getitem__``.
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. If the body of the
interface definition defines ``extra_classes``, it should be a
tuple giving additional classes to declare implement the interface.
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.
.. versionadded:: 5.0.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.
self.__name__ = name
extra_classes = attrs.pop('extra_classes', ())

if 'abc' not in attrs:
# Something like ``IList(ISequence)``: We're extending
# abc interfaces but not an ABC interface ourself.
self.__class__ = InterfaceClass
InterfaceClass.__init__(self, name, bases, attrs)
for cls in extra_classes:
classImplements(cls, self)
return

based_on = attrs.pop('abc')
self.__abc = based_on
self.__extra_classes = tuple(extra_classes)

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__'] = self.__create_class_doc(attrs)
# Anything specified in the body takes precedence.
methods.update(attrs)
InterfaceClass.__init__(self, name, bases, methods)
self.__register_classes()

@staticmethod
def __optional_methods_to_docs(attrs):
optionals = {k: v for k, v in attrs.items() if isinstance(v, optional)}
for k in optionals:
attrs[k] = _decorator_non_return

if not optionals:
return ''

docs = "\n\nThe following methods are optional:\n - " + "\n-".join(
"%s\n%s" % (k, v.__doc__) for k, v in optionals.items()
)
return docs

def __create_class_doc(self, attrs):
based_on = self.__abc
def ref(c):
mod = c.__module__
name = c.__name__
if mod == str.__module__:
return "`%s`" % name
if mod == '_io':
mod = 'io'
return "`%s.%s`" % (mod, name)
implementations_doc = "\n - ".join(
ref(c)
for c in sorted(self.getRegisteredConformers(), key=ref)
)
if implementations_doc:
implementations_doc = "\n\nKnown implementations are:\n\n - " + implementations_doc

based_on_doc = (based_on.__doc__ or '')
based_on_doc = based_on_doc.splitlines()
based_on_doc = based_on_doc[0] if based_on_doc else ''

doc = """Interface for the ABC `%s.%s`.\n\n%s%s%s""" % (
based_on.__module__, based_on.__name__,
attrs.get('__doc__', based_on_doc),
self.__optional_methods_to_docs(attrs),
implementations_doc
)
return doc


@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.

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 known to conform to
the ABC this interface parallels.
"""
based_on = self.__abc

# The registry only contains things that aren't already
# known to be subclasses of the ABC. But the ABC is in charge
# of checking that, so its quite possible that registrations
# are in fact ignored, winding up just in the _abc_cache.
try:
registered = list(based_on._abc_registry) + list(based_on._abc_cache)
except AttributeError:
# Rewritten in C in CPython 3.7.
# These expose the underlying weakref.
from abc import _get_dump
data = _get_dump(based_on)
registry = data[0]
cache = data[1]
registered = [x() for x in itertools.chain(registry, cache)]
registered = [x for x in registered if x is not None]

return set(itertools.chain(registered, self.__extra_classes))


ABCInterface = ABCInterfaceClass.__new__(ABCInterfaceClass, None, None, None)
InterfaceClass.__init__(ABCInterface, 'ABCInterface', (Interface,), {})
Loading

0 comments on commit c931999

Please sign in to comment.