Skip to content

Commit

Permalink
Let interface 'subclasses' override __adapt__.
Browse files Browse the repository at this point in the history
Cooperate with InterfaceClass to ensure there is no performance penalty for this. Fixes #3

+-------------------------------------------------------------+----------------+------------------------------+------------------------------+
| Benchmark                                                   | bench_master38 | bench_issue3                 | bench_issue3_opt             |
+=============================================================+================+==============================+==============================+
| call interface (provides; deep)                             | 369 ns         | 454 ns: 1.23x slower (+23%)  | not significant              |
+-------------------------------------------------------------+----------------+------------------------------+------------------------------+
| call interface (provides; wide)                             | 373 ns         | 457 ns: 1.22x slower (+22%)  | 365 ns: 1.02x faster (-2%)   |
+-------------------------------------------------------------+----------------+------------------------------+------------------------------+
| call interface (no alternate, no conform, not provided)     | 671 ns         | 760 ns: 1.13x slower (+13%)  | 636 ns: 1.06x faster (-5%)   |
+-------------------------------------------------------------+----------------+------------------------------+------------------------------+
| call interface (alternate, no conform, not provided)        | 395 ns         | 494 ns: 1.25x slower (+25%)  | not significant              |
+-------------------------------------------------------------+----------------+------------------------------+------------------------------+
| call interface (no alternate, valid conform, not provided)  | 250 ns         | not significant              | 227 ns: 1.10x faster (-9%)   |
+-------------------------------------------------------------+----------------+------------------------------+------------------------------+
| call interface (alternate, invalid conform, not provided)   | 348 ns         | 424 ns: 1.22x slower (+22%)  | not significant              |
+-------------------------------------------------------------+----------------+------------------------------+------------------------------+
  • Loading branch information
jamadden committed Apr 3, 2020
1 parent 1f38cab commit 6385812
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 65 deletions.
17 changes: 17 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@

See `issue 200 <https://github.com/zopefoundation/zope.interface/issues/200>`_.

- Require that the second argument (*bases*) to ``InterfaceClass`` is
a tuple. This only matters when directly using ``InterfaceClass`` to
create new interfaces dynamically. Previously, an individual
interface was allowed, but did not work correctly. Now it is
consistent with ``type`` and requires a tuple.

- Let interfaces define custom ``__adapt__`` methods. This implements
the other side of the :pep:`246` adaptation protocol: objects being
adapted could already implement ``__conform__`` if they know about
the interface, and now interfaces can implement ``__adapt__`` if
they know about particular objects. There is no performance penalty
for interfaces that do not supply custom ``__adapt__`` methods.

This includes the ability to add new methods, or override existing
interface methods using the new ``@interfacemethod`` decorator.

See `issue 3 <https://github.com/zopefoundation/zope.interface/issues/3>`_.

5.0.2 (2020-03-30)
==================
Expand Down
49 changes: 42 additions & 7 deletions docs/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -863,7 +863,8 @@ Adaptation

Interfaces can be called to perform adaptation.

The semantics are based on those of the PEP 246 ``adapt`` function.
The semantics are based on those of the :pep:`246` ``adapt``
function.

If an object cannot be adapted, then a ``TypeError`` is raised:

Expand Down Expand Up @@ -897,7 +898,13 @@ If an object already implements the interface, then it will be returned:
>>> I(obj) is obj
True

If an object implements ``__conform__``, then it will be used:
:pep:`246` outlines a requirement:

When the object knows about the [interface], and either considers
itself compliant, or knows how to wrap itself suitably.

This is handled with ``__conform__``. If an object implements
``__conform__``, then it will be used:

.. doctest::

Expand Down Expand Up @@ -936,21 +943,27 @@ Adapter hooks (see ``__adapt__``) will also be used, if present:
>>> class I(zope.interface.Interface):
... pass

Interfaces implement the PEP 246 ``__adapt__`` method.
Interfaces implement the :pep:`246` ``__adapt__`` method to satisfy
the requirement:

When the [interface] knows about the object, and either the object
already complies or the [interface] knows how to suitably wrap the
object.

This method is normally not called directly. It is called by the PEP
246 adapt framework and by the interface ``__call__`` operator.
This method is normally not called directly. It is called by the
:pep:`246` adapt framework and by the interface ``__call__`` operator.

The ``adapt`` method is responsible for adapting an object to the
reciever.

The default version returns ``None``:
The default version returns ``None`` (because by default no interface
"knows how to suitably wrap the object"):

.. doctest::

>>> I.__adapt__(0)

unless the object given provides the interface:
unless the object given provides the interface ("the object already complies"):

.. doctest::

Expand Down Expand Up @@ -989,6 +1002,28 @@ Hooks can be uninstalled by removing them from the list:
>>> I.__adapt__(0)


It is possible to replace or customize the ``__adapt___``
functionality for particular interfaces.

.. doctest::

>>> class ICustomAdapt(zope.interface.Interface):
... @zope.interface.interfacemethod
... def __adapt__(self, obj):
... if isinstance(obj, str):
... return obj
... return super(type(ICustomAdapt), self).__adapt__(obj)

>>> @zope.interface.implementer(ICustomAdapt)
... class CustomAdapt(object):
... pass
>>> ICustomAdapt('a string')
'a string'
>>> ICustomAdapt(CustomAdapt())
<CustomAdapt object at ...>

.. seealso:: :func:`zope.interface.interfacemethod`

.. [#create] The main reason we subclass ``Interface`` is to cause the
Python class statement to create an interface, rather
than a class.
Expand Down
2 changes: 2 additions & 0 deletions docs/api/declarations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ To declare an interface itself, extend the ``Interface`` base class.

.. documented in README.rst
.. autofunction:: interfacemethod

Declaring The Interfaces of Objects
===================================

Expand Down
5 changes: 4 additions & 1 deletion src/zope/interface/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def meth(arg1, arg2):
See the module doc strings for more information.
"""
__docformat__ = 'restructuredtext'

# pylint:disable=wrong-import-position,unused-import
from zope.interface.interface import Interface
from zope.interface.interface import _wire

Expand All @@ -75,8 +75,11 @@ def meth(arg1, arg2):
from zope.interface.declarations import noLongerProvides
from zope.interface.declarations import providedBy
from zope.interface.declarations import provider

from zope.interface.exceptions import Invalid

from zope.interface.interface import Attribute
from zope.interface.interface import interfacemethod
from zope.interface.interface import invariant
from zope.interface.interface import taggedValue

Expand Down
26 changes: 23 additions & 3 deletions src/zope/interface/_zope_interface_coptimizations.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ static PyObject *str_registry, *strro, *str_generation, *strchanged;
static PyObject *str__self__;
static PyObject *str__module__;
static PyObject *str__name__;
static PyObject *str__adapt__;
static PyObject *str_CALL_CUSTOM_ADAPT;

static PyTypeObject *Implements;

Expand Down Expand Up @@ -796,7 +798,7 @@ static struct PyMethodDef ib_methods[] = {
*/
static PyObject *
ib_call(PyObject *self, PyObject *args, PyObject *kwargs)
IB_call(PyObject *self, PyObject *args, PyObject *kwargs)
{
PyObject *conform, *obj, *alternate, *adapter;
static char *kwlist[] = {"obj", "alternate", NULL};
Expand Down Expand Up @@ -835,7 +837,23 @@ ib_call(PyObject *self, PyObject *args, PyObject *kwargs)
Py_DECREF(conform);
}

adapter = __adapt__(self, obj); // XXX: should be self.__adapt__.
/* We differ from the Python code here. For speed, instead of always calling
self.__adapt__(), we check to see if the type has defined it. Checking in
the dict for __adapt__ isn't sufficient because there's no cheap way to
tell if it's the __adapt__ that InterfaceBase itself defines (our type
will *never* be InterfaceBase, we're always subclassed by
InterfaceClass). Instead, we cooperate with InterfaceClass in Python to
set a flag in a new subclass when this is necessary. */
if (PyDict_GetItem(self->ob_type->tp_dict, str_CALL_CUSTOM_ADAPT))
{
/* Doesn't matter what the value is. Simply being present is enough. */
adapter = PyObject_CallMethodObjArgs(self, str__adapt__, obj, NULL);
}
else
{
adapter = __adapt__(self, obj);
}

if (adapter == NULL || adapter != Py_None)
{
return adapter;
Expand Down Expand Up @@ -1045,7 +1063,7 @@ static PyTypeObject InterfaceBaseType = {
/* tp_as_sequence */ 0,
/* tp_as_mapping */ 0,
/* tp_hash */ (hashfunc)IB_hash,
/* tp_call */ (ternaryfunc)ib_call,
/* tp_call */ (ternaryfunc)IB_call,
/* tp_str */ (reprfunc)0,
/* tp_getattro */ (getattrofunc)0,
/* tp_setattro */ (setattrofunc)0,
Expand Down Expand Up @@ -2023,6 +2041,8 @@ init(void)
DEFINE_STRING(__self__);
DEFINE_STRING(__name__);
DEFINE_STRING(__module__);
DEFINE_STRING(__adapt__);
DEFINE_STRING(_CALL_CUSTOM_ADAPT);
#undef DEFINE_STRING
adapter_hooks = PyList_New(0);
if (adapter_hooks == NULL)
Expand Down
2 changes: 1 addition & 1 deletion src/zope/interface/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,5 +259,5 @@ def getRegisteredConformers(self):
return set(itertools.chain(registered, self.__extra_classes))


ABCInterface = ABCInterfaceClass.__new__(ABCInterfaceClass, None, None, None)
ABCInterface = ABCInterfaceClass.__new__(ABCInterfaceClass, 'ABCInterfaceClass', (), {})
InterfaceClass.__init__(ABCInterface, 'ABCInterface', (Interface,), {})
77 changes: 76 additions & 1 deletion src/zope/interface/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import weakref

from zope.interface._compat import _use_c_impl
from zope.interface._compat import PYTHON2 as PY2
from zope.interface.exceptions import Invalid
from zope.interface.ro import ro as calculate_ro
from zope.interface import ro
Expand All @@ -36,7 +37,10 @@

CO_VARARGS = 4
CO_VARKEYWORDS = 8
# Put in the attrs dict of an interface by ``taggedValue`` and ``invariants``
TAGGED_DATA = '__interface_tagged_values__'
# Put in the attrs dict of an interface by ``interfacemethod``
INTERFACE_METHODS = '__interface_methods__'

_decorator_non_return = object()
_marker = object()
Expand Down Expand Up @@ -651,6 +655,21 @@ def __repr__(cls):
)


def interfacemethod(func):
"""
Convert a method specification to an actual method of the interface.
This is a decorator that functions like `staticmethod` et al.
The primary use of this decorator is to allow interface definitions to
define the ``__adapt__`` method.
"""
f_locals = sys._getframe(1).f_locals
methods = f_locals.setdefault(INTERFACE_METHODS, {})
methods[func.__name__] = func
return _decorator_non_return


class InterfaceClass(_InterfaceClassBase):
"""
Prototype (scarecrow) Interfaces Implementation.
Expand All @@ -664,6 +683,57 @@ class InterfaceClass(_InterfaceClassBase):
#
#implements(IInterface)

def __new__(cls, name=None, bases=(), attrs=None, __doc__=None, # pylint:disable=redefined-builtin
__module__=None):
assert isinstance(bases, tuple)
attrs = attrs or {}
needs_custom_class = attrs.pop(INTERFACE_METHODS, None)
if needs_custom_class:
needs_custom_class.update(
{'__classcell__': attrs.pop('__classcell__')}
if '__classcell__' in attrs
else {}
)
if '__adapt__' in needs_custom_class:
# We need to tell the C code to call this.
needs_custom_class['_CALL_CUSTOM_ADAPT'] = 1

if issubclass(cls, _InterfaceClassWithCustomMethods):
cls_bases = (cls,)
elif cls is InterfaceClass:
cls_bases = (_InterfaceClassWithCustomMethods,)
else:
cls_bases = (cls, _InterfaceClassWithCustomMethods)

cls = type(cls)( # pylint:disable=self-cls-assignment
name + "<WithCustomMethods>",
cls_bases,
needs_custom_class
)
elif PY2 and bases and len(bases) > 1:
bases_with_custom_methods = tuple(
type(b)
for b in bases
if issubclass(type(b), _InterfaceClassWithCustomMethods)
)

# If we have a subclass of InterfaceClass in *bases*,
# Python 3 is smart enough to pass that as *cls*, but Python
# 2 just passes whatever the first base in *bases* is. This means that if
# we have multiple inheritance, and one of our bases has already defined
# a custom method like ``__adapt__``, we do the right thing automatically
# and extend it on Python 3, but not necessarily on Python 2. To fix this, we need
# to run the MRO algorithm and get the most derived base manually.
# Note that this only works for consistent resolution orders
if bases_with_custom_methods:
cls = type( # pylint:disable=self-cls-assignment
name + "<WithCustomMethods>",
bases_with_custom_methods,
{}
).__mro__[1] # Not the class we created, the most derived.

return _InterfaceClassBase.__new__(cls)

def __init__(self, name, bases=(), attrs=None, __doc__=None, # pylint:disable=redefined-builtin
__module__=None):
# We don't call our metaclass parent directly
Expand Down Expand Up @@ -738,7 +808,7 @@ def update_value(aname, aval):
# __qualname__: PEP 3155 (Python 3.3+)
'__qualname__',
# __annotations__: PEP 3107 (Python 3.0+)
'__annotations__'
'__annotations__',
)
and aval is not _decorator_non_return
}
Expand Down Expand Up @@ -889,6 +959,11 @@ def __reduce__(self):
Specification._ROOT = Interface
ro._ROOT = Interface

class _InterfaceClassWithCustomMethods(InterfaceClass):
"""
Marker class for interfaces with custom methods that override InterfaceClass methods.
"""


class Attribute(Element):
"""Attribute descriptions
Expand Down
29 changes: 29 additions & 0 deletions src/zope/interface/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,35 @@ class IRange(Interface):
.. seealso:: `zope.interface.invariant`
"""

def interfacemethod(method):
"""
A decorator that transforms a method specification into an
implementation method.
This is used to override methods of ``Interface`` or provide new methods.
Definitions using this decorator will not appear in :meth:`IInterface.names()`.
It is possible to have an implementation method and a method specification
of the same name.
For example::
class IRange(Interface):
@interfacemethod
def __adapt__(self, obj):
if isinstance(obj, range):
# Return the builtin ``range`` as-is
return obj
return super(type(IRange), self).__adapt__(obj)
You can use ``super`` to call the parent class functionality. Note that
the zero-argument version (``super().__adapt__``) works on Python 3.6 and above, but
prior to that the two-argument version must be used, and the class must be explicitly
passed as the first argument.
.. versionadded:: 5.1.0
.. seealso:: `zope.interface.interfacemethod`
"""

###
# Querying interfaces
###
Expand Down
Loading

0 comments on commit 6385812

Please sign in to comment.