Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Provides and ClassProvides ignore redundant interfaces like @implementer #235

Merged
merged 1 commit into from
Mar 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@
object itself (this might happen to persistent ``Components``
implementations in the face of bugs).

- Fix the ``Provides`` and ``ClassProvides`` descriptors to stop
allowing redundant interfaces (those already implemented by the
underlying class or meta class) to produce an inconsistent
resolution order. This is similar to the change in ``@implementer``
in 5.1.0, and resolves inconsistent resolution orders with
``zope.proxy`` and ``zope.location``. See `issue 207
<https://github.com/zopefoundation/zope.interface/issues/207>`_.

5.2.0 (2020-11-05)
==================

Expand Down
17 changes: 15 additions & 2 deletions src/zope/interface/declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ def __add__(self, other):

__radd__ = __add__

@staticmethod
def _add_interfaces_to_cls(interfaces, cls):
# Strip redundant interfaces already provided
# by the cls so we don't produce invalid
# resolution orders.
implemented_by_cls = implementedBy(cls)
interfaces = tuple([
iface
for iface in interfaces
if not implemented_by_cls.isOrExtends(iface)
])
return interfaces + (implemented_by_cls,)


class _ImmutableDeclaration(Declaration):
# A Declaration that is immutable. Used as a singleton to
Expand Down Expand Up @@ -747,7 +760,7 @@ class Provides(Declaration): # Really named ProvidesClass
def __init__(self, cls, *interfaces):
self.__args = (cls, ) + interfaces
self._cls = cls
Declaration.__init__(self, *(interfaces + (implementedBy(cls), )))
Declaration.__init__(self, *self._add_interfaces_to_cls(interfaces, cls))

def __repr__(self):
return "<%s.%s for %s>" % (
Expand Down Expand Up @@ -890,7 +903,7 @@ def __init__(self, cls, metacls, *interfaces):
self._cls = cls
self._implements = implementedBy(cls)
self.__args = (cls, metacls, ) + interfaces
Declaration.__init__(self, *(interfaces + (implementedBy(metacls), )))
Declaration.__init__(self, *self._add_interfaces_to_cls(interfaces, metacls))

def __repr__(self):
return "<%s.%s for %s>" % (
Expand Down
9 changes: 8 additions & 1 deletion src/zope/interface/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,13 @@ def __setBases(self, bases):
__setBases,
)

# This method exists for tests to override the way we call
# ro.calculate_ro(), usually by adding extra kwargs. We don't
# want to have a mutable dictionary as a class member that we pass
# ourself because mutability is bad, and passing **kw is slower than
# calling the bound function.
_do_calculate_ro = calculate_ro

def _calculate_sro(self):
"""
Calculate and return the resolution order for this object, using its ``__bases__``.
Expand Down Expand Up @@ -448,7 +455,7 @@ def _calculate_sro(self):
# This requires that by the time this method is invoked, our bases
# have settled their SROs. Thus, ``changed()`` must first
# update itself before telling its descendents of changes.
sro = calculate_ro(self, base_mros={
sro = self._do_calculate_ro(base_mros={
b: b.__sro__
for b in self.__bases__
})
Expand Down
87 changes: 84 additions & 3 deletions src/zope/interface/tests/test_declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1280,7 +1280,7 @@ class Foo(object):
pass
spec = self._makeOne(Foo, IFoo)
klass, args = spec.__reduce__()
self.assertTrue(klass is Provides)
self.assertIs(klass, Provides)
self.assertEqual(args, (Foo, IFoo))

def test___get___class(self):
Expand All @@ -1290,7 +1290,7 @@ class Foo(object):
pass
spec = self._makeOne(Foo, IFoo)
Foo.__provides__ = spec
self.assertTrue(Foo.__provides__ is spec)
self.assertIs(Foo.__provides__, spec)

def test___get___instance(self):
from zope.interface.interface import InterfaceClass
Expand All @@ -1312,6 +1312,43 @@ def test__repr__(self):
)


class ProvidesClassStrictTests(ProvidesClassTests):
# Tests that require the strict C3 resolution order.

def _getTargetClass(self):
ProvidesClass = super(ProvidesClassStrictTests, self)._getTargetClass()
class StrictProvides(ProvidesClass):
def _do_calculate_ro(self, base_mros):
return ProvidesClass._do_calculate_ro(self, base_mros=base_mros, strict=True)
return StrictProvides

def test__repr__(self):
self.skipTest("Not useful for the subclass.")

def test_overlapping_interfaces_corrected(self):
# Giving Provides(cls, IFace), where IFace is already
# provided by cls, doesn't produce invalid resolution orders.
from zope.interface import implementedBy
from zope.interface import Interface
from zope.interface import implementer

class IBase(Interface):
pass

@implementer(IBase)
class Base(object):
pass

spec = self._makeOne(Base, IBase)
self.assertEqual(spec.__sro__, (
spec,
implementedBy(Base),
IBase,
implementedBy(object),
Interface
))


class Test_Provides(unittest.TestCase):

def _callFUT(self, *args, **kw):
Expand Down Expand Up @@ -1580,7 +1617,7 @@ class Foo(object):
pass
cp = Foo.__provides__ = self._makeOne(Foo, type(Foo), IBar)
self.assertEqual(cp.__reduce__(),
(self._getTargetClass(), (Foo, type(Foo), IBar)))
(type(cp), (Foo, type(Foo), IBar)))

def test__repr__(self):
inst = self._makeOne(type(self), type)
Expand All @@ -1589,6 +1626,50 @@ def test__repr__(self):
"<zope.interface.declarations.ClassProvides for %r>" % type(self)
)


class ClassProvidesStrictTests(ClassProvidesTests):
# Tests that require the strict C3 resolution order.

def _getTargetClass(self):
ClassProvides = super(ClassProvidesStrictTests, self)._getTargetClass()
class StrictClassProvides(ClassProvides):
def _do_calculate_ro(self, base_mros):
return ClassProvides._do_calculate_ro(self, base_mros=base_mros, strict=True)
return StrictClassProvides

def test__repr__(self):
self.skipTest("Not useful for the subclass.")

def test_overlapping_interfaces_corrected(self):
# Giving ClassProvides(cls, metaclass, IFace), where IFace is already
# provided by metacls, doesn't produce invalid resolution orders.
from zope.interface import implementedBy
from zope.interface import Interface
from zope.interface import implementer

class IBase(Interface):
pass

@implementer(IBase)
class metaclass(type):
pass

cls = metaclass(
'cls',
(object,),
{}
)

spec = self._makeOne(cls, metaclass, IBase)
self.assertEqual(spec.__sro__, (
spec,
implementedBy(metaclass),
IBase,
implementedBy(type),
implementedBy(object),
Interface
))

class Test_directlyProvidedBy(unittest.TestCase):

def _callFUT(self, *args, **kw):
Expand Down