Skip to content

Commit

Permalink
Ensure Interface is the last item in the __sro__.
Browse files Browse the repository at this point in the history
None of the elegant solutions mentioned in the issue worked out, so I had to brute force it.

Fixes #8
  • Loading branch information
jamadden committed Mar 17, 2020
1 parent d0c6a59 commit 11a1d01
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 25 deletions.
8 changes: 7 additions & 1 deletion CHANGES.rst
Expand Up @@ -163,7 +163,7 @@
registry lookup functions. See issue 11.

- Use Python's standard C3 resolution order to compute the
``__iro___`` and ``__sro___`` of interfaces. Previously, an ad-hoc
``__iro__`` and ``__sro__`` of interfaces. Previously, an ad-hoc
ordering that made no particular guarantees was used.

This has many beneficial properties, including the fact that base
Expand Down Expand Up @@ -195,6 +195,12 @@
the future). For details, see the documentation for
``zope.interface.ro``.

- Ensure that ``Interface`` is always the last item in the ``__iro__``
and ``__sro__``. This is usually the case, but if classes that do
not implement any interfaces are part of a class inheritance
hierarchy, ``Interface`` could be assigned too high a priority.
See `issue 8 <https://github.com/zopefoundation/zope.interface/issues/8>`_.

4.7.2 (2020-03-10)
==================

Expand Down
19 changes: 16 additions & 3 deletions docs/README.rst
Expand Up @@ -689,9 +689,22 @@ that lists the specification and all of it's ancestors:
<InterfaceClass builtins.IBaz>,
<InterfaceClass builtins.IFoo>,
<InterfaceClass builtins.IBlat>,
<InterfaceClass zope.interface.Interface>,
<implementedBy ...object>)

<implementedBy ...object>,
<InterfaceClass zope.interface.Interface>)
>>> class IBiz(zope.interface.Interface):
... pass
>>> @zope.interface.implementer(IBiz)
... class Biz(Baz):
... pass
>>> pprint(zope.interface.implementedBy(Biz).__sro__)
(<implementedBy builtins.Biz>,
<InterfaceClass builtins.IBiz>,
<implementedBy builtins.Baz>,
<InterfaceClass builtins.IBaz>,
<InterfaceClass builtins.IFoo>,
<InterfaceClass builtins.IBlat>,
<implementedBy ...object>,
<InterfaceClass zope.interface.Interface>)

Tagged Values
=============
Expand Down
16 changes: 13 additions & 3 deletions src/zope/interface/common/tests/__init__.py
Expand Up @@ -73,14 +73,24 @@ def test(self, stdlib_class=stdlib_class, iface=iface):
def test_ro(self, stdlib_class=stdlib_class, iface=iface):
from zope.interface import ro
from zope.interface import implementedBy
from zope.interface import Interface
self.assertEqual(
tuple(ro.ro(iface, strict=True)),
iface.__sro__)
implements = implementedBy(stdlib_class)
sro = implements.__sro__
self.assertIs(sro[-1], Interface)

# Check that we got the strict C3 resolution order, unless we
# know we cannot. Note that 'Interface' is virtual base that doesn't
# necessarily appear at the same place in the calculated SRO as in the
# final SRO.
strict = stdlib_class not in self.NON_STRICT_RO
self.assertEqual(
tuple(ro.ro(implements, strict=strict)),
implements.__sro__)
isro = ro.ro(implements, strict=strict)
isro.remove(Interface)
isro.append(Interface)

self.assertEqual(tuple(isro), sro)

name = 'test_auto_ro_' + suffix
test_ro.__name__ = name
Expand Down
94 changes: 76 additions & 18 deletions src/zope/interface/interface.py
Expand Up @@ -22,6 +22,7 @@
from zope.interface._compat import _use_c_impl
from zope.interface.exceptions import Invalid
from zope.interface.ro import ro
from zope.interface.ro import C3

__all__ = [
# Most of the public API from this module is directly exported
Expand Down Expand Up @@ -215,6 +216,10 @@ class Specification(SpecificationBase):
"""
__slots__ = ()

# The root of all Specifications. This will be assigned `Interface`,
# once it is defined.
_ROOT = None

# Copy some base class methods for speed
isOrExtends = SpecificationBase.isOrExtends
providedBy = SpecificationBase.providedBy
Expand Down Expand Up @@ -263,7 +268,7 @@ def __setBases(self, bases):
for b in self.__bases__:
b.unsubscribe(self)

# Register ourselves as a dependent of our bases
# Register ourselves as a dependent of our new bases
self._bases = bases
for b in bases:
b.subscribe(self)
Expand All @@ -275,29 +280,76 @@ def __setBases(self, bases):
__setBases,
)

def changed(self, originally_changed):
"""We, or something we depend on, have changed
def _calculate_sro(self):
"""
self._v_attrs = None

implied = self._implied
implied.clear()
Calculate and return the resolution order for this object, using its ``__bases__``.
if len(self.__bases__) == 1:
Ensures that ``Interface`` is always the last (lowest priority) element.
"""
# We'd like to make Interface the lowest priority as a
# property of the resolution order algorithm. That almost
# works out naturally, but it fails when class inheritance has
# some bases that DO implement an interface, and some that DO
# NOT. In such a mixed scenario, you wind up with a set of
# bases to consider that look like this: [[..., Interface],
# [..., object], ...]. Depending on the order if inheritance,
# Interface can wind up before or after object, and that can
# happen at any point in the tree, meaning Interface can wind
# up somewhere in the middle of the order. Since Interface is
# treated as something that everything winds up implementing
# anyway (a catch-all for things like adapters), having it high up
# the order is bad. It's also bad to have it at the end, just before
# some concrete class: concrete classes should be HIGHER priority than
# interfaces (because there's only one class, but many implementations).
#
# One technically nice way to fix this would be to have
# ``implementedBy(object).__bases__ = (Interface,)``
#
# But: (1) That fails for old-style classes and (2) that causes
# everything to appear to *explicitly* implement Interface, when up
# to this point it's been an implicit virtual sort of relationship.
#
# So we force the issue by mutating the resolution order.

# TODO: Caching. Perhaps make ro.C3 able to re-use the computed ``__sro__``
# instead of re-doing it for the entire tree.
base_count = len(self._bases)

if base_count == 1:
# Fast path: One base makes it trivial to calculate
# the MRO.
sro = self.__bases__[0].__sro__
ancestors = [self]
ancestors.extend(sro)
sro = [self]
sro.extend(self.__bases__[0].__sro__)
else:
ancestors = ro(self)
sro = ro(self)
if self._ROOT is not None:
# Once we don't have to deal with old-style classes,
# we can add a check and only do this if base_count > 1,
# if we tweak the bootstrapping for ``<implementedBy object>``
root = self._ROOT
sro = [
x
for x in sro
if x is not root
]
sro.append(root)
assert sro[-1] is root, sro

return sro

try:
if Interface not in ancestors:
ancestors.append(Interface)
except NameError:
pass # defining Interface itself
def changed(self, originally_changed):
"""
We, or something we depend on, have changed.
By the time this is called, the things we depend on,
such as our bases, should themselves be stable.
"""
self._v_attrs = None

implied = self._implied
implied.clear()

ancestors = self._calculate_sro()
self.__sro__ = tuple(ancestors)
self.__iro__ = tuple([ancestor for ancestor in ancestors
if isinstance(ancestor, InterfaceClass)
Expand All @@ -307,7 +359,8 @@ def changed(self, originally_changed):
# We directly imply our ancestors:
implied[ancestor] = ()

# Now, advise our dependents of change:
# Now, advise our dependents of change
# (being careful not to create the WeakKeyDictionary if not needed):
for dependent in tuple(self._dependents.keys() if self._dependents else ()):
dependent.changed(originally_changed)

Expand Down Expand Up @@ -636,6 +689,11 @@ def __ge__(self, other):


Interface = InterfaceClass("Interface", __module__='zope.interface')
# Interface is the only member of its own SRO.
Interface._calculate_sro = lambda: (Interface,)
Interface.changed(Interface)
assert Interface.__sro__ == (Interface,)
Specification._ROOT = Interface


class Attribute(Element):
Expand Down
42 changes: 42 additions & 0 deletions src/zope/interface/tests/test_interface.py
Expand Up @@ -412,6 +412,48 @@ class IBar(Interface):
self.assertTrue(spec.get('foo') is IFoo.get('foo'))
self.assertTrue(spec.get('bar') is IBar.get('bar'))

def test_multiple_inheritance_no_interfaces(self):
# If we extend an object that implements interfaces,
# plus ane that doesn't, we do not interject `Interface`
# early in the resolution order. It stays at the end,
# like it should.
# See https://github.com/zopefoundation/zope.interface/issues/8
from zope.interface.interface import Interface
from zope.interface.declarations import implementer
from zope.interface.declarations import implementedBy

class IDefaultViewName(Interface):
pass

class Context(object):
pass

class RDBModel(Context):
pass

class IOther(Interface):
pass

@implementer(IOther)
class OtherBase(object):
pass

class Model(OtherBase, Context):
pass

self.assertEqual(
implementedBy(Model).__sro__,
(
implementedBy(Model),
implementedBy(OtherBase),
IOther,
implementedBy(Context),
implementedBy(object),
Interface, # This used to be wrong, it used to be 2 too high.
)
)


class InterfaceClassTests(unittest.TestCase):

def _getTargetClass(self):
Expand Down

0 comments on commit 11a1d01

Please sign in to comment.