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:
parent
a404e5fed4
commit
46781f87cc
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. :(
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue