Skip to content

Commit

Permalink
Make provided/implementedBy and adapter registries respect super().
Browse files Browse the repository at this point in the history
The query functions now start by looking at the next class in the MRO (interfaces directly provided by the underlying object are not found).

Adapter registries automatically pick up providedBy change to start finding the correct implementations of adapters, but to make that really useful they needed to change to unpack super() arguments and pass __self__ to the factory.

Fixes #11

Unfortunately, this makes PyPy unable to build the C extensions.
  • Loading branch information
jamadden committed Mar 5, 2020
1 parent c931999 commit 1c80f26
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 17 deletions.
4 changes: 0 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ jobs:
- sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest
after_success:

- name: PyPy C Extensions
env: PURE_PYTHON=0
python: pypy

- name: CPython No C Extension
env: PURE_PYTHON=1
python: 3.8
Expand Down
14 changes: 14 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@
`dolmen.builtins <https://pypi.org/project/dolmen.builtins/>`_.
See `issue 138 <https://github.com/zopefoundation/zope.interface/issues/138>`_.

- Make ``providedBy()`` and ``implementedBy()`` respect ``super``
objects. For instance, if class ``Derived`` implements ``IDerived``
and extends ``Base`` which in turn implements ``IBase``, then
``providedBy(super(Derived, derived))`` will return ``[IBase]``.
Previously it would have returned ``[IDerived]`` (in general, it
would previously have returned whatever would have been returned
without ``super``).

Along with this change, adapter registries will unpack ``super``
objects into their ``__self___`` before passing it to the factory.
Together, this means that ``component.getAdapter(super(Derived,
self), ITarget)`` is now meaningful.

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

4.7.1 (2019-11-11)
==================
Expand Down
27 changes: 25 additions & 2 deletions docs/adapter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,11 @@ factories:
... pass

>>> @zope.interface.implementer(IR)
... class X:
... class X(object):
... pass

>>> @zope.interface.implementer(IProvide1)
... class Y:
... class Y(object):
... def __init__(self, context):
... self.context = context

Expand Down Expand Up @@ -238,6 +238,29 @@ We can register and lookup by name too:
>>> y.context is x
True

Passing ``super`` objects works as expected to find less specific adapters:

.. doctest::

>>> class IDerived(IR):
... pass
>>> @zope.interface.implementer(IDerived)
... class Derived(X):
... pass
>>> class DerivedAdapter(Y):
... def query_next(self):
... return registry.queryAdapter(
... super(type(self.context), self.context),
... IProvide1)
>>> registry.register([IDerived], IProvide1, '', DerivedAdapter)
>>> derived = Derived()
>>> adapter = registry.queryAdapter(derived, IProvide1)
>>> adapter.__class__.__name__
'DerivedAdapter'
>>> adapter = adapter.query_next()
>>> adapter.__class__.__name__
'Y'

When the adapter factory produces ``None``, then this is treated as if no
adapter has been found. This allows us to prevent adaptation (when desired)
and let the adapter factory determine whether adaptation is possible based on
Expand Down
4 changes: 3 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import pkg_resources
sys.path.append(os.path.abspath('../src'))
rqmt = pkg_resources.require('zope.interface')[0]

# Import and document the pure-python versions of things; they tend to have better
# docstrings and signatures.
os.environ['PURE_PYTHON'] = '1'
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
Expand Down
8 changes: 5 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ def _unavailable(self, e):
]

is_jython = 'java' in sys.platform
is_pypy = hasattr(sys, 'pypy_version_info')

# Jython cannot build the C optimizations. Everywhere else,
# including PyPy, defer the decision to runtime.
if is_jython:
# Jython cannot build the C optimizations. Nor, as of 7.3, can PyPy (
# it doesn't have PySuper_Type) Everywhere else, defer the decision to
# runtime.
if is_jython or is_pypy:
ext_modules = []
else:
ext_modules = codeoptimization
Expand Down
102 changes: 99 additions & 3 deletions src/zope/interface/_zope_interface_coptimizations.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ static PyObject *str__conform__, *str_call_conform, *adapter_hooks;
static PyObject *str_uncached_lookup, *str_uncached_lookupAll;
static PyObject *str_uncached_subscriptions;
static PyObject *str_registry, *strro, *str_generation, *strchanged;
static PyObject *str__get__;
static PyObject *str__self__;
static PyObject *str__thisclass__;
static PyObject *strmro;

static PyTypeObject *Implements;

Expand Down Expand Up @@ -91,6 +95,41 @@ import_declarations(void)
return 0;
}

static PyObject*
_next_super_class(PyObject* ob)
{
// Given an *ob* of type ``super``, return the next
// class in the MRO to examine, starting from where the ``super``
// was invoked.
// Returns a BORROWED reference.

PyObject* result;
PyObject* class_that_invoked_super;
PyObject* mro;
Py_ssize_t index;

result = class_that_invoked_super = mro = NULL;
#define _NSCHECK(ob, bad_val) do { if (ob == bad_val) { goto EXIT; } } while (0)

class_that_invoked_super = PyObject_GetAttr(ob, str__thisclass__);
_NSCHECK(class_that_invoked_super, NULL);

mro = PyObject_CallMethodObjArgs(class_that_invoked_super, strmro, NULL);
_NSCHECK(mro, NULL);

index = PySequence_Index(mro, class_that_invoked_super);
_NSCHECK(index, -1);

result = PyList_GetItem(mro, index + 1); // borrow

#undef _NSCHECK
EXIT:
Py_XDECREF(class_that_invoked_super);
Py_XDECREF(mro);
return result;
}


static PyTypeObject SpecType; /* Forward */

static PyObject *
Expand All @@ -111,6 +150,20 @@ implementedBy(PyObject *ignored, PyObject *cls)

PyObject *dict = NULL, *spec;

if (PyObject_TypeCheck(cls, &PySuper_Type))
{
PyObject* next = _next_super_class(cls);
if (next == NULL)
{
PyErr_Clear();
}
else
{
// next and cls are both borrowed references
cls = next;
}
}

if (PyType_Check(cls))
{
dict = TYPE(cls)->tp_dict;
Expand Down Expand Up @@ -192,7 +245,35 @@ providedBy(PyObject *ignored, PyObject *ob)
{
PyObject *result, *cls, *cp;

result = PyObject_GetAttr(ob, str__providedBy__);
result = NULL;

if (PyObject_TypeCheck(ob, &PySuper_Type))
{
#define _PVCHECK(ob) do { if (ob == NULL) {PyErr_Clear(); goto NO_PROVIDED; }} while (0)

PyObject* next_class = NULL;
PyObject* super_self = NULL;
PyObject* provided_descr = NULL;
next_class = _next_super_class(ob); // borrowed
_PVCHECK(next_class);
provided_descr = PyObject_GetAttr(next_class, str__providedBy__);
_PVCHECK(provided_descr);
super_self = PyObject_GetAttr(ob, str__self__);
_PVCHECK(super_self);

result = PyObject_CallMethodObjArgs(provided_descr, str__get__,
super_self, next_class, NULL);

NO_PROVIDED:
Py_CLEAR(super_self);
Py_CLEAR(provided_descr);
#undef _PVCHECK
}
else
{
result = PyObject_GetAttr(ob, str__providedBy__);
}

if (result == NULL)
{
PyErr_Clear();
Expand Down Expand Up @@ -1154,12 +1235,23 @@ _adapter_hook(lookup *self,
return NULL;

if (factory != Py_None)
{
{
if (PyObject_TypeCheck(object, &PySuper_Type)) {
PyObject* self = PyObject_GetAttr(object, str__self__);
if (self == NULL)
{
Py_DECREF(factory);
return NULL;
}
// Borrow the reference to self
Py_DECREF(self);
object = self;
}
result = PyObject_CallFunctionObjArgs(factory, object, NULL);
Py_DECREF(factory);
if (result == NULL || result != Py_None)
return result;
}
}
else
result = factory; /* None */

Expand Down Expand Up @@ -1736,6 +1828,10 @@ init(void)
DEFINE_STRING(_generation);
DEFINE_STRING(ro);
DEFINE_STRING(changed);
DEFINE_STRING(__self__);
DEFINE_STRING(__get__);
DEFINE_STRING(__thisclass__);
DEFINE_STRING(mro);
#undef DEFINE_STRING
adapter_hooks = PyList_New(0);
if (adapter_hooks == NULL)
Expand Down
4 changes: 3 additions & 1 deletion src/zope/interface/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ def adapter_hook(self, provided, object, name=u'', default=None):
factory = self.lookup((required, ), provided, name)

if factory is not None:
if isinstance(object, super):
object = object.__self__
result = factory(object)
if result is not None:
return result
Expand Down Expand Up @@ -543,7 +545,7 @@ def queryMultiAdapter(self, objects, provided, name=u'', default=None):
if factory is None:
return default

result = factory(*objects)
result = factory(*[o.__self__ if isinstance(o, super) else o for o in objects])
if result is None:
return default

Expand Down
25 changes: 24 additions & 1 deletion src/zope/interface/declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ class implements (that instances of the class provides).
_ADVICE_WARNING = ('The %s API is deprecated, and will not work in Python3 '
'Use the @%s class decorator instead.')

def _next_super_class(ob):
# When ``ob`` is an instance of ``super``, return
# the next class in the MRO that we should actually be
# looking at.
class_that_invoked_super = ob.__thisclass__
mro = class_that_invoked_super.mro()
next_class = mro[mro.index(class_that_invoked_super) + 1]
return next_class

class named(object):

def __init__(self, name):
Expand Down Expand Up @@ -283,6 +292,9 @@ def implementedBy(cls):
The value returned is an `~zope.interface.interfaces.IDeclaration`.
"""
try:
if isinstance(cls, super):
cls = _next_super_class(cls)

spec = cls.__dict__.get('__implemented__')
except AttributeError:

Expand Down Expand Up @@ -890,13 +902,24 @@ def getObjectSpecification(ob):

@_use_c_impl
def providedBy(ob):
"""
Return the interfaces provided by *ob*.
If *ob* is a :class:`super` object, then only interfaces implemented
by the remainder of the classes in the method resolution order are
considered. Interfaces directly provided by the object underlying *ob*
are not.
"""
# Here we have either a special object, an old-style declaration
# or a descriptor

# Try to get __providedBy__
try:
r = ob.__providedBy__
if isinstance(ob, super):
next_class = _next_super_class(ob)
r = next_class.__providedBy__.__get__(ob.__self__, next_class)
else:
r = ob.__providedBy__
except:
# Not set yet. Fall back to lower-level thing that computes it
return getObjectSpecification(ob)
Expand Down
Loading

0 comments on commit 1c80f26

Please sign in to comment.