Make @implementer and classImplements not re-declare redundant interfaces.

classImplementsOnly and @implementer_only can still be used to do that.
This commit is contained in:
Jason Madden 2020-04-06 07:57:12 -05:00
parent a404e5fed4
commit 46781f87cc
No known key found for this signature in database
GPG Key ID: 349F84431A08B99E
4 changed files with 164 additions and 97 deletions

View File

@ -5,6 +5,14 @@
5.1.0 (unreleased)
==================
- Make ``@implementer(*iface)`` and ``classImplements(cls, *iface)``
ignore redundant interfaces. If the class already implements an
interface through inheritance, it is no longer redeclared
specifically for *cls*. This solves many instances of inconsistent
resolution orders, while still allowing the interface to be declared
for readability and maintenance purposes. See `issue 199
<https://github.com/zopefoundation/zope.interface/issues/199>`_.
- Remove all bare ``except:`` statements. Previously, when accessing
special attributes such as ``__provides__``, ``__providedBy__``,
``__class__`` and ``__conform__``, this package wrapped such access

View File

@ -208,31 +208,34 @@ interfaces instances of ``A`` and ``B`` provide.
Instances of ``C`` now also provide ``I5``. Notice how ``I5`` was
added to the *beginning* of the list of things provided directly by
``C``. Unlike `classImplements`, this ignores inheritance and other
factors and does not attempt to ensure a consistent resolution order.
``C``. Unlike `classImplements`, this ignores interface inheritance
and does not attempt to ensure a consistent resolution order (except
that it continues to elide interfaces already implemented through
class inheritance)::
.. doctest::
>>> class IBA(IB, IA): pass
>>> class IBA(IB, IA):
... pass
>>> classImplementsFirst(C, IBA)
>>> classImplementsFirst(C, IA)
>>> [i.getName() for i in implementedBy(C)]
['IA', 'IBA', 'I5', 'I1', 'I2', 'IB']
['IBA', 'I5', 'I1', 'I2', 'IA', 'IB']
This cannot be used to introduce duplicates.
.. doctest::
>>> len(implementedBy(C).declared)
5
4
>>> classImplementsFirst(C, IA)
>>> classImplementsFirst(C, IBA)
>>> classImplementsFirst(C, IA)
>>> classImplementsFirst(C, IBA)
>>> [i.getName() for i in implementedBy(C)]
['IBA', 'IA', 'I5', 'I1', 'I2', 'IB']
['IBA', 'I5', 'I1', 'I2', 'IA', 'IB']
>>> len(implementedBy(C).declared)
5
4
directlyProvides

View File

@ -418,18 +418,21 @@ def implementedBy(cls): # pylint:disable=too-many-return-statements,too-many-bra
def classImplementsOnly(cls, *interfaces):
"""Declare the only interfaces implemented by instances of a class
"""
Declare the only interfaces implemented by instances of a class
The arguments after the class are one or more interfaces or interface
specifications (`~zope.interface.interfaces.IDeclaration` objects).
The arguments after the class are one or more interfaces or interface
specifications (`~zope.interface.interfaces.IDeclaration` objects).
The interfaces given (including the interfaces in the specifications)
replace any previous declarations.
The interfaces given (including the interfaces in the specifications)
replace any previous declarations, *including* inherited definitions. If you
wish to preserve inherited declarations, you can pass ``implementedBy(cls)``
in *interfaces*. This can be used to alter the interface resolution order.
"""
spec = implementedBy(cls)
spec.declared = ()
spec.inherit = None
classImplements(cls, *interfaces)
_classImplements_ordered(spec, interfaces, ())
def classImplements(cls, *interfaces):
@ -448,6 +451,14 @@ def classImplements(cls, *interfaces):
beginning or end of the list of interfaces declared for *cls*,
based on inheritance, in order to try to maintain a consistent
resolution order. Previously, all interfaces were added to the end.
.. versionchanged:: 5.1.0
If *cls* is already declared to implement an interface (or derived interface)
in *interfaces* through inheritance, the interface is ignored. Previously, it
would redundantly be made direct base of *cls*, which often produced inconsistent
interface resolution orders. Now, the order will be consistent, but may change.
Also, if the ``__bases__`` of the *cls* are later changed, the *cls* will no
longer be considered to implement such an interface (changing the ``__bases__`` of *cls*
has never been supported).
"""
spec = implementedBy(cls)
interfaces = tuple(_normalizeargs(interfaces))
@ -459,6 +470,9 @@ def classImplements(cls, *interfaces):
# order, while still allowing for BWC (in the past, we always
# appended)
for iface in interfaces:
if spec.isOrExtends(iface):
continue
for b in spec.declared:
if iface.extends(b):
before.append(iface)
@ -479,7 +493,8 @@ def classImplementsFirst(cls, iface):
.. versionadded:: 5.0.0
"""
spec = implementedBy(cls)
_classImplements_ordered(spec, (iface,), ())
if not spec.isOrExtends(iface):
_classImplements_ordered(spec, (iface,), ())
def _classImplements_ordered(spec, before=(), after=()):
@ -514,33 +529,38 @@ def _implements_advice(cls):
class implementer(object):
"""Declare the interfaces implemented by instances of a class.
"""
Declare the interfaces implemented by instances of a class.
This function is called as a class decorator.
This function is called as a class decorator.
The arguments are one or more interfaces or interface
specifications (`~zope.interface.interfaces.IDeclaration` objects).
The arguments are one or more interfaces or interface
specifications (`~zope.interface.interfaces.IDeclaration`
objects).
The interfaces given (including the interfaces in the
specifications) are added to any interfaces previously
declared.
The interfaces given (including the interfaces in the
specifications) are added to any interfaces previously declared,
unless the interface is already implemented.
Previous declarations include declarations for base classes
unless implementsOnly was used.
Previous declarations include declarations for base classes unless
implementsOnly was used.
This function is provided for convenience. It provides a more
convenient way to call `classImplements`. For example::
This function is provided for convenience. It provides a more
convenient way to call `classImplements`. For example::
@implementer(I1)
class C(object):
pass
is equivalent to calling::
is equivalent to calling::
classImplements(C, I1)
after the class has been created.
"""
after the class has been created.
.. seealso:: `classImplements`
The change history provided there applies to this function too.
"""
__slots__ = ('interfaces',)
def __init__(self, *interfaces):
@ -595,10 +615,10 @@ class implementer_only(object):
# on a method or function....
raise ValueError('The implementer_only decorator is not '
'supported for methods or functions.')
else:
# Assume it's a class:
classImplementsOnly(ob, *self.interfaces)
return ob
# Assume it's a class:
classImplementsOnly(ob, *self.interfaces)
return ob
def _implements(name, interfaces, do_classImplements):
# This entire approach is invalid under Py3K. Don't even try to fix
@ -619,30 +639,35 @@ def _implements(name, interfaces, do_classImplements):
addClassAdvisor(_implements_advice, depth=3)
def implements(*interfaces):
"""Declare interfaces implemented by instances of a class
"""
Declare interfaces implemented by instances of a class.
This function is called in a class definition.
.. deprecated:: 5.0
This only works for Python 2. The `implementer` decorator
is preferred for all versions.
The arguments are one or more interfaces or interface
specifications (`~zope.interface.interfaces.IDeclaration` objects).
This function is called in a class definition.
The interfaces given (including the interfaces in the
specifications) are added to any interfaces previously
declared.
The arguments are one or more interfaces or interface
specifications (`~zope.interface.interfaces.IDeclaration`
objects).
Previous declarations include declarations for base classes
unless `implementsOnly` was used.
The interfaces given (including the interfaces in the
specifications) are added to any interfaces previously declared.
This function is provided for convenience. It provides a more
convenient way to call `classImplements`. For example::
Previous declarations include declarations for base classes unless
`implementsOnly` was used.
This function is provided for convenience. It provides a more
convenient way to call `classImplements`. For example::
implements(I1)
is equivalent to calling::
is equivalent to calling::
classImplements(C, I1)
after the class has been created.
after the class has been created.
"""
# This entire approach is invalid under Py3K. Don't even try to fix
# the coverage for this block there. :(

View File

@ -825,25 +825,73 @@ class Test_classImplementsOnly(unittest.TestCase):
class Test_classImplements(unittest.TestCase):
def _callFUT(self, *args, **kw):
def _callFUT(self, cls, iface):
from zope.interface.declarations import classImplements
return classImplements(*args, **kw)
result = classImplements(cls, iface) # pylint:disable=assignment-from-no-return
self.assertIsNone(result)
return cls
def test_no_existing(self):
def __check_implementer(self, Foo):
from zope.interface.declarations import ClassProvides
from zope.interface.interface import InterfaceClass
class Foo(object):
pass
IFoo = InterfaceClass('IFoo')
self._callFUT(Foo, IFoo)
spec = Foo.__implemented__ # pylint:disable=no-member
returned = self._callFUT(Foo, IFoo)
self.assertIs(returned, Foo)
spec = Foo.__implemented__
self.assertEqual(spec.__name__,
'zope.interface.tests.test_declarations.Foo')
self.assertIs(spec.inherit, Foo)
self.assertIs(Foo.__implemented__, spec) # pylint:disable=no-member
self.assertIsInstance(Foo.__providedBy__, ClassProvides) # pylint:disable=no-member
self.assertIsInstance(Foo.__provides__, ClassProvides) # pylint:disable=no-member
self.assertEqual(Foo.__provides__, Foo.__providedBy__) # pylint:disable=no-member
self.assertIs(Foo.__implemented__, spec)
self.assertIsInstance(Foo.__providedBy__, ClassProvides)
self.assertIsInstance(Foo.__provides__, ClassProvides)
self.assertEqual(Foo.__provides__, Foo.__providedBy__)
return Foo, IFoo
def test_oldstyle_class(self):
# This only matters on Python 2
class Foo:
pass
self.__check_implementer(Foo)
def test_newstyle_class(self):
class Foo(object):
pass
self.__check_implementer(Foo)
def __check_implementer_redundant(self, Base):
# If we @implementer exactly what was already present, we write
# no declared attributes on the parent (we still set everything, though)
Base, IBase = self.__check_implementer(Base)
class Child(Base):
pass
returned = self._callFUT(Child, IBase)
self.assertIn('__implemented__', returned.__dict__)
self.assertNotIn('__providedBy__', returned.__dict__)
self.assertIn('__provides__', returned.__dict__)
spec = Child.__implemented__
self.assertEqual(spec.declared, ())
self.assertEqual(spec.inherit, Child)
self.assertTrue(IBase.providedBy(Child()))
def test_redundant_implementer_empty_class_declarations_newstyle(self):
self.__check_implementer_redundant(type('Foo', (object,), {}))
def test_redundant_implementer_empty_class_declarations_oldstyle(self):
# This only matters on Python 2
class Foo:
pass
self.__check_implementer_redundant(Foo)
def _order_for_two(self, applied_first, applied_second):
return (applied_first, applied_second)
def test_w_existing_Implements(self):
from zope.interface.declarations import Implements
@ -857,9 +905,10 @@ class Test_classImplements(unittest.TestCase):
impl.inherit = Foo
self._callFUT(Foo, IBar)
# Same spec, now different values
self.assertTrue(Foo.__implemented__ is impl)
self.assertIs(Foo.__implemented__, impl)
self.assertEqual(impl.inherit, Foo)
self.assertEqual(impl.declared, (IFoo, IBar,))
self.assertEqual(impl.declared,
self._order_for_two(IFoo, IBar))
def test_w_existing_Implements_w_bases(self):
from zope.interface.declarations import Implements
@ -886,8 +935,22 @@ class Test_classImplements(unittest.TestCase):
# Same spec, now different values
self.assertIs(ExtendsRoot.__implemented__, impl_extends_root)
self.assertEqual(impl_extends_root.inherit, ExtendsRoot)
self.assertEqual(impl_extends_root.declared, (IExtendsRoot, ISecondRoot,))
self.assertEqual(impl_extends_root.__bases__, (IExtendsRoot, ISecondRoot, impl_root))
self.assertEqual(impl_extends_root.declared,
self._order_for_two(IExtendsRoot, ISecondRoot,))
self.assertEqual(impl_extends_root.__bases__,
self._order_for_two(IExtendsRoot, ISecondRoot) + (impl_root,))
class Test_classImplementsFirst(Test_classImplements):
def _callFUT(self, cls, iface):
from zope.interface.declarations import classImplementsFirst
result = classImplementsFirst(cls, iface) # pylint:disable=assignment-from-no-return
self.assertIsNone(result)
return cls
def _order_for_two(self, applied_first, applied_second):
return (applied_second, applied_first)
class Test__implements_advice(unittest.TestCase):
@ -909,7 +972,7 @@ class Test__implements_advice(unittest.TestCase):
self.assertEqual(list(Foo.__implemented__), [IFoo]) # pylint:disable=no-member
class Test_implementer(unittest.TestCase):
class Test_implementer(Test_classImplements):
def _getTargetClass(self):
from zope.interface.declarations import implementer
@ -918,42 +981,9 @@ class Test_implementer(unittest.TestCase):
def _makeOne(self, *args, **kw):
return self._getTargetClass()(*args, **kw)
def test_oldstyle_class(self):
# TODO Py3 story
from zope.interface.declarations import ClassProvides
from zope.interface.interface import InterfaceClass
IFoo = InterfaceClass('IFoo')
class Foo:
pass
decorator = self._makeOne(IFoo)
returned = decorator(Foo)
self.assertTrue(returned is Foo)
spec = Foo.__implemented__ # pylint:disable=no-member
self.assertEqual(spec.__name__,
'zope.interface.tests.test_declarations.Foo')
self.assertIs(spec.inherit, Foo)
self.assertIs(Foo.__implemented__, spec) # pylint:disable=no-member
self.assertIsInstance(Foo.__providedBy__, ClassProvides) # pylint:disable=no-member
self.assertIsInstance(Foo.__provides__, ClassProvides) # pylint:disable=no-member
self.assertEqual(Foo.__provides__, Foo.__providedBy__) # pylint:disable=no-member
def test_newstyle_class(self):
from zope.interface.declarations import ClassProvides
from zope.interface.interface import InterfaceClass
IFoo = InterfaceClass('IFoo')
class Foo(object):
pass
decorator = self._makeOne(IFoo)
returned = decorator(Foo)
self.assertTrue(returned is Foo)
spec = Foo.__implemented__ # pylint:disable=no-member
self.assertEqual(spec.__name__,
'zope.interface.tests.test_declarations.Foo')
self.assertIs(spec.inherit, Foo)
self.assertIs(Foo.__implemented__, spec) # pylint:disable=no-member
self.assertIsInstance(Foo.__providedBy__, ClassProvides) # pylint:disable=no-member
self.assertIsInstance(Foo.__provides__, ClassProvides) # pylint:disable=no-member
self.assertEqual(Foo.__provides__, Foo.__providedBy__) # pylint:disable=no-member
def _callFUT(self, cls, *ifaces):
decorator = self._makeOne(*ifaces)
return decorator(cls)
def test_nonclass_cannot_assign_attr(self):
from zope.interface.interface import InterfaceClass
@ -976,6 +1006,7 @@ class Test_implementer(unittest.TestCase):
self.assertIs(foo.__implemented__, spec) # pylint:disable=no-member
class Test_implementer_only(unittest.TestCase):
def _getTargetClass(self):