summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2020-03-16 17:24:29 -0500
committerJason Madden <jamadden@gmail.com>2020-03-18 12:06:17 -0500
commit4c4e1c985ffa710482cfeebaf1cb28b470bab47e (patch)
tree833b671abd678213c77b9e072c9b7808e813a08d /src
parentbd5a749b5b050b422940193d8d2935e7c88aa7cc (diff)
downloadzope-interface-4c4e1c985ffa710482cfeebaf1cb28b470bab47e.tar.gz
Ensure Interface is the last item in the __sro__.
None of the elegant solutions mentioned in the issue worked out, so I had to brute force it. Fixes #8
Diffstat (limited to 'src')
-rw-r--r--src/zope/interface/common/tests/__init__.py16
-rw-r--r--src/zope/interface/interface.py94
-rw-r--r--src/zope/interface/tests/test_interface.py42
3 files changed, 131 insertions, 21 deletions
diff --git a/src/zope/interface/common/tests/__init__.py b/src/zope/interface/common/tests/__init__.py
index ade2bf3..b285ad7 100644
--- a/src/zope/interface/common/tests/__init__.py
+++ b/src/zope/interface/common/tests/__init__.py
@@ -73,14 +73,24 @@ def add_verify_tests(cls, iface_classes_iter):
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
diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py
index f9fb333..34f54ba 100644
--- a/src/zope/interface/interface.py
+++ b/src/zope/interface/interface.py
@@ -22,6 +22,7 @@ import weakref
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
@@ -226,6 +227,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
@@ -274,7 +279,7 @@ class Specification(SpecificationBase):
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)
@@ -286,29 +291,76 @@ class Specification(SpecificationBase):
__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)
@@ -318,7 +370,8 @@ class Specification(SpecificationBase):
# 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)
@@ -647,6 +700,11 @@ class InterfaceClass(Element, InterfaceBase, Specification):
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):
diff --git a/src/zope/interface/tests/test_interface.py b/src/zope/interface/tests/test_interface.py
index 70e5d64..da7eb8c 100644
--- a/src/zope/interface/tests/test_interface.py
+++ b/src/zope/interface/tests/test_interface.py
@@ -438,6 +438,48 @@ class SpecificationTests(unittest.TestCase):
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):