summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2020-03-17 05:22:10 -0500
committerGitHub <noreply@github.com>2020-03-17 05:22:10 -0500
commitd0c6a5967af074b1a7d60a1bb20d9337263b9571 (patch)
treebcaae43437453c18768716a07ce71184b45bb5d6
parent7d638c3b149222b164794cac3907c32ab19d2655 (diff)
parent024f6432270afd021da2d9fff5c3f496f788e54d (diff)
downloadzope-interface-d0c6a5967af074b1a7d60a1bb20d9337263b9571.tar.gz
Merge pull request #182 from zopefoundation/issue21
Use C3 (mostly) to compute IRO.
-rw-r--r--CHANGES.rst43
-rw-r--r--appveyor.yml4
-rw-r--r--docs/api/declarations.rst168
-rw-r--r--docs/api/index.rst1
-rw-r--r--docs/api/ro.rst19
-rw-r--r--docs/verify.rst46
-rw-r--r--setup.py3
-rw-r--r--src/zope/interface/__init__.py3
-rw-r--r--src/zope/interface/common/__init__.py16
-rw-r--r--src/zope/interface/common/builtins.py1
-rw-r--r--src/zope/interface/common/collections.py4
-rw-r--r--src/zope/interface/common/mapping.py6
-rw-r--r--src/zope/interface/common/tests/__init__.py31
-rw-r--r--src/zope/interface/common/tests/test_collections.py4
-rw-r--r--src/zope/interface/declarations.py86
-rw-r--r--src/zope/interface/interface.py13
-rw-r--r--src/zope/interface/interfaces.py5
-rw-r--r--src/zope/interface/registry.py2
-rw-r--r--src/zope/interface/ro.py555
-rw-r--r--src/zope/interface/tests/test_declarations.py19
-rw-r--r--src/zope/interface/tests/test_interface.py8
-rw-r--r--src/zope/interface/tests/test_ro.py302
22 files changed, 1236 insertions, 103 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 6b0784c..6f3eabc 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,16 @@
5.0.0 (unreleased)
==================
+- Adopt Python's standard `C3 resolution order
+ <https://www.python.org/download/releases/2.3/mro/>`_ for interface
+ linearization, with tweaks to support additional cases that are
+ common in interfaces but disallowed for Python classes.
+
+ In complex multiple-inheritance like scenerios, this may change the
+ interface resolution order, resulting in finding different adapters.
+ However, the results should make more sense. See `issue 21
+ <https://github.com/zopefoundation/zope.interface/issues/21>`_.
+
- Make an internal singleton object returned by APIs like
``implementedBy`` and ``directlyProvidedBy`` immutable. Previously,
it was fully mutable and allowed changing its ``__bases___``. That
@@ -152,6 +162,39 @@
- Fix a potential interpreter crash in the low-level adapter
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
+ ordering that made no particular guarantees was used.
+
+ This has many beneficial properties, including the fact that base
+ interface and base classes tend to appear near the end of the
+ resolution order instead of the beginning. The resolution order in
+ general should be more predictable and consistent.
+
+ .. caution::
+ In some cases, especially with complex interface inheritance
+ trees or when manually providing or implementing interfaces, the
+ resulting IRO may be quite different. This may affect adapter
+ lookup.
+
+ The C3 order enforces some constraints in order to be able to
+ guarantee a sensible ordering. Older versions of zope.interface did
+ not impose similar constraints, so it was possible to create
+ interfaces and declarations that are inconsistent with the C3
+ constraints. In that event, zope.interface will still produce a
+ resolution order equal to the old order, but it won't be guaranteed
+ to be fully C3 compliant. In the future, strict enforcement of C3
+ order may be the default.
+
+ A set of environment variables and module constants allows
+ controlling several aspects of this new behaviour. It is possible to
+ request warnings about inconsistent resolution orders encountered,
+ and even to forbid them. Differences between the C3 resolution order
+ and the previous order can be logged, and, in extreme cases, the
+ previous order can still be used (this ability will be removed in
+ the future). For details, see the documentation for
+ ``zope.interface.ro``.
+
4.7.2 (2020-03-10)
==================
diff --git a/appveyor.yml b/appveyor.yml
index 25ab921..41fbbbb 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -26,10 +26,10 @@ install:
}
- ps: if (-not (Test-Path $env:PYTHON)) { throw "No $env:PYTHON" }
- echo "C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\SetEnv.cmd" /x64 > "C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\amd64\vcvars64.bat"
- - pip install -e .
+ - python -m pip install -U pip setuptools wheel
+ - python -m pip install -U -e ".[test]"
build_script:
- - pip install wheel
- python -W ignore setup.py -q bdist_wheel
test_script:
diff --git a/docs/api/declarations.rst b/docs/api/declarations.rst
index cf38b7f..ce52833 100644
--- a/docs/api/declarations.rst
+++ b/docs/api/declarations.rst
@@ -34,22 +34,6 @@ implementer_only
.. autoclass:: implementer_only
-implements
-----------
-
-.. caution:: Does not work on Python 3. Use the `implementer` decorator instead.
-
-.. autofunction:: implements
-
-
-implementsOnly
---------------
-
-.. caution:: Does not work on Python 3. Use the `implementer_only` decorator instead.
-
-.. autofunction:: implementsOnly
-
-
classImplementsOnly
-------------------
@@ -99,34 +83,137 @@ Consider the following example:
>>> from zope.interface import Interface
>>> from zope.interface import classImplements
+ >>> from zope.interface.ro import is_consistent
>>> class I1(Interface): pass
...
>>> class I2(Interface): pass
...
- >>> class I3(Interface): pass
+ >>> class IA(Interface): pass
...
- >>> class I4(Interface): pass
+ >>> class IB(Interface): pass
...
>>> class I5(Interface): pass
...
- >>> @implementer(I3)
+ >>> @implementer(IA)
... class A(object):
... pass
- >>> @implementer(I4)
+ >>> @implementer(IB)
... class B(object):
... pass
>>> class C(A, B):
... pass
>>> classImplements(C, I1, I2)
>>> [i.getName() for i in implementedBy(C)]
- ['I1', 'I2', 'I3', 'I4']
+ ['I1', 'I2', 'IA', 'IB']
+
+Instances of ``C`` provide ``I1`` and ``I2``, plus whatever
+instances of ``A`` and ``B`` provide.
+
+.. doctest::
+
>>> classImplements(C, I5)
>>> [i.getName() for i in implementedBy(C)]
- ['I1', 'I2', 'I5', 'I3', 'I4']
+ ['I1', 'I2', 'I5', 'IA', 'IB']
+
+Instances of ``C`` now also provide ``I5``. Notice how ``I5`` was
+added to the *end* of the list of things provided directly by ``C``.
+
+If we ask a class to implement an interface that extends
+an interface it already implements, that interface will go at the
+*beginning* of the list, in order to preserve a consistent resolution
+order.
+
+.. doctest::
+
+ >>> class I6(I5): pass
+ >>> class I7(IA): pass
+ >>> classImplements(C, I6, I7)
+ >>> [i.getName() for i in implementedBy(C)]
+ ['I6', 'I1', 'I2', 'I5', 'I7', 'IA', 'IB']
+ >>> is_consistent(implementedBy(C))
+ True
+
+This cannot be used to introduce duplicates.
+
+.. doctest::
+
+ >>> classImplements(C, IA, IB, I1, I2)
+ >>> [i.getName() for i in implementedBy(C)]
+ ['I6', 'I1', 'I2', 'I5', 'I7', 'IA', 'IB']
+
+
+classImplementsFirst
+--------------------
+
+.. autofunction:: classImplementsFirst
+
+Consider the following example:
+
+.. doctest::
+
+ >>> from zope.interface import Interface
+ >>> from zope.interface import classImplements
+ >>> from zope.interface import classImplementsFirst
+ >>> class I1(Interface): pass
+ ...
+ >>> class I2(Interface): pass
+ ...
+ >>> class IA(Interface): pass
+ ...
+ >>> class IB(Interface): pass
+ ...
+ >>> class I5(Interface): pass
+ ...
+ >>> @implementer(IA)
+ ... class A(object):
+ ... pass
+ >>> @implementer(IB)
+ ... class B(object):
+ ... pass
+ >>> class C(A, B):
+ ... pass
+ >>> classImplementsFirst(C, I2)
+ >>> classImplementsFirst(C, I1)
+ >>> [i.getName() for i in implementedBy(C)]
+ ['I1', 'I2', 'IA', 'IB']
Instances of ``C`` provide ``I1``, ``I2``, ``I5``, and whatever
interfaces instances of ``A`` and ``B`` provide.
+.. doctest::
+
+ >>> classImplementsFirst(C, I5)
+ >>> [i.getName() for i in implementedBy(C)]
+ ['I5', 'I1', 'I2', 'IA', 'IB']
+
+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.
+
+.. doctest::
+
+ >>> class IBA(IB, IA): pass
+ >>> classImplementsFirst(C, IBA)
+ >>> classImplementsFirst(C, IA)
+ >>> [i.getName() for i in implementedBy(C)]
+ ['IA', 'IBA', 'I5', 'I1', 'I2', 'IB']
+
+This cannot be used to introduce duplicates.
+
+.. doctest::
+
+ >>> len(implementedBy(C).declared)
+ 5
+ >>> 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']
+ >>> len(implementedBy(C).declared)
+ 5
+
directlyProvides
----------------
@@ -332,14 +419,6 @@ Removing an interface that is provided through the class is not possible:
ValueError: Can only remove directly provided interfaces.
-classProvides
--------------
-
-.. caution:: Does not work on Python 3. Use the `provider` decorator instead.
-
-.. autofunction:: classProvides
-
-
provider
--------
@@ -413,6 +492,33 @@ When registering an adapter or utility component, the registry looks for the
provided.
+Deprecated Functions
+--------------------
+
+implements
+~~~~~~~~~~
+
+.. caution:: Does not work on Python 3. Use the `implementer` decorator instead.
+
+.. autofunction:: implements
+
+
+implementsOnly
+~~~~~~~~~~~~~~
+
+.. caution:: Does not work on Python 3. Use the `implementer_only` decorator instead.
+
+.. autofunction:: implementsOnly
+
+
+classProvides
+~~~~~~~~~~~~~
+
+.. caution:: Does not work on Python 3. Use the `provider` decorator instead.
+
+.. autofunction:: classProvides
+
+
Querying The Interfaces Of Objects
==================================
@@ -592,7 +698,7 @@ Exmples for :meth:`Declaration.flattened`:
>>> spec = Declaration(I4, spec)
>>> i = spec.flattened()
>>> [x.getName() for x in i]
- ['I4', 'I2', 'I1', 'I3', 'Interface']
+ ['I4', 'I2', 'I3', 'I1', 'Interface']
>>> list(i)
[]
diff --git a/docs/api/index.rst b/docs/api/index.rst
index e6affd8..7266966 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -12,3 +12,4 @@ Contents:
adapters
components
common
+ ro
diff --git a/docs/api/ro.rst b/docs/api/ro.rst
new file mode 100644
index 0000000..f14192a
--- /dev/null
+++ b/docs/api/ro.rst
@@ -0,0 +1,19 @@
+===========================================
+ Computing The Resolution Order (Priority)
+===========================================
+
+Just as Python classes have a method resolution order that determines
+which implementation of a method gets used when inheritance is used,
+interfaces have a resolution order that determines their ordering when
+searching for adapters.
+
+That order is computed by ``zope.interface.ro.ro``. This is an
+internal module not generally needed by a user of ``zope.interface``,
+but its documentation can be helpful to understand how orders are
+computed.
+
+``zope.interface.ro``
+=====================
+
+.. automodule:: zope.interface.ro
+ :member-order: alphabetical
diff --git a/docs/verify.rst b/docs/verify.rst
index 06a0a0f..bf602d0 100644
--- a/docs/verify.rst
+++ b/docs/verify.rst
@@ -23,10 +23,13 @@ that an object provides this interface.
>>> from zope.interface import Interface, Attribute, implementer
>>> from zope.interface import Invalid
>>> from zope.interface.verify import verifyObject
+ >>> oname, __name__ = __name__, 'base' # Pretend we're in a module, not a doctest
>>> class IBase(Interface):
... x = Attribute("The X attribute")
+ >>> __name__ = 'module' # Pretend to be a different module.
>>> class IFoo(IBase):
... y = Attribute("The Y attribute")
+ >>> __name__ = oname; del oname
>>> class Foo(object):
... pass
>>> def verify_foo(**kwargs):
@@ -48,8 +51,8 @@ defined.
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>:
Does not declaratively implement the interface
- The IBase.x attribute was not provided
- The IFoo.y attribute was not provided
+ The base.IBase.x attribute was not provided
+ The module.IFoo.y attribute was not provided
If we add the two missing attributes, we still have the error about not
declaring the correct interface.
@@ -116,13 +119,13 @@ exception.
... class Foo(object):
... x = 1
>>> verify_foo()
- The object <Foo...> has failed to implement interface <...IFoo>: The IFoo.y attribute was not provided.
+ The object <Foo...> has failed to implement interface <...IFoo>: The module.IFoo.y attribute was not provided.
>>> @implementer(IFoo)
... class Foo(object):
... def __init__(self):
... self.y = 2
>>> verify_foo()
- The object <Foo...> has failed to implement interface <...IFoo>: The IBase.x attribute was not provided.
+ The object <Foo...> has failed to implement interface <...IFoo>: The base.IBase.x attribute was not provided.
If both attributes are missing, an exception is raised reporting
both errors.
@@ -134,23 +137,25 @@ both errors.
... pass
>>> verify_foo()
The object <Foo ...> has failed to implement interface <...IFoo>:
- The IBase.x attribute was not provided
- The IFoo.y attribute was not provided
+ The base.IBase.x attribute was not provided
+ The module.IFoo.y attribute was not provided
If an attribute is implemented as a property that raises an ``AttributeError``
when trying to get its value, the attribute is considered missing:
.. doctest::
+ >>> oname, __name__ = __name__, 'module'
>>> class IFoo(Interface):
... x = Attribute('The X attribute')
+ >>> __name__ = oname; del oname
>>> @implementer(IFoo)
... class Foo(object):
... @property
... def x(self):
... raise AttributeError
>>> verify_foo()
- The object <Foo...> has failed to implement interface <...IFoo>: The IFoo.x attribute was not provided.
+ The object <Foo...> has failed to implement interface <...IFoo>: The module.IFoo.x attribute was not provided.
Any other exception raised by a property will propagate to the caller of
@@ -190,13 +195,15 @@ that takes one argument. If we don't provide it, we get an error.
.. doctest::
+ >>> oname, __name__ = __name__, 'module'
>>> class IFoo(Interface):
... def simple(arg1): "Takes one positional argument"
+ >>> __name__ = oname; del oname
>>> @implementer(IFoo)
... class Foo(object):
... pass
>>> verify_foo()
- The object <Foo...> has failed to implement interface <...IFoo>: The IFoo.simple(arg1) attribute was not provided.
+ The object <Foo...> has failed to implement interface <...IFoo>: The module.IFoo.simple(arg1) attribute was not provided.
Once they exist, they are checked to be callable, and for compatible signatures.
@@ -206,7 +213,7 @@ Not being callable is an error.
>>> Foo.simple = 42
>>> verify_foo()
- The object <Foo...> has failed to implement interface <...IFoo>: The contract of IFoo.simple(arg1) is violated because '42' is not a method.
+ The object <Foo...> has failed to implement interface <...IFoo>: The contract of module.IFoo.simple(arg1) is violated because '42' is not a method.
Taking too few arguments is an error. (Recall that the ``self``
argument is implicit.)
@@ -215,7 +222,7 @@ argument is implicit.)
>>> Foo.simple = lambda self: "I take no arguments"
>>> verify_foo()
- The object <Foo...> has failed to implement interface <...IFoo>: The contract of IFoo.simple(arg1) is violated because '<lambda>()' doesn't allow enough arguments.
+ The object <Foo...> has failed to implement interface <...IFoo>: The contract of module.IFoo.simple(arg1) is violated because '<lambda>()' doesn't allow enough arguments.
Requiring too many arguments is an error.
@@ -223,7 +230,7 @@ Requiring too many arguments is an error.
>>> Foo.simple = lambda self, a, b: "I require two arguments"
>>> verify_foo()
- The object <Foo...> has failed to implement interface <...IFoo>: The contract of IFoo.simple(arg1) is violated because '<lambda>(a, b)' requires too many arguments.
+ The object <Foo...> has failed to implement interface <...IFoo>: The contract of module.IFoo.simple(arg1) is violated because '<lambda>(a, b)' requires too many arguments.
Variable arguments can be used to implement the required number, as
can arguments with defaults.
@@ -242,21 +249,25 @@ variable keyword arguments, the implementation must also accept them.
.. doctest::
+ >>> oname, __name__ = __name__, 'module'
>>> class IFoo(Interface):
... def needs_kwargs(**kwargs): pass
+ >>> __name__ = oname; del oname
>>> @implementer(IFoo)
... class Foo(object):
... def needs_kwargs(self, a=1, b=2): pass
>>> verify_foo()
- The object <Foo...> has failed to implement interface <...IFoo>: The contract of IFoo.needs_kwargs(**kwargs) is violated because 'Foo.needs_kwargs(a=1, b=2)' doesn't support keyword arguments.
+ The object <Foo...> has failed to implement interface <...IFoo>: The contract of module.IFoo.needs_kwargs(**kwargs) is violated because 'Foo.needs_kwargs(a=1, b=2)' doesn't support keyword arguments.
+ >>> oname, __name__ = __name__, 'module'
>>> class IFoo(Interface):
... def needs_varargs(*args): pass
+ >>> __name__ = oname; del oname
>>> @implementer(IFoo)
... class Foo(object):
... def needs_varargs(self, **kwargs): pass
>>> verify_foo()
- The object <Foo...> has failed to implement interface <...IFoo>: The contract of IFoo.needs_varargs(*args) is violated because 'Foo.needs_varargs(**kwargs)' doesn't support variable arguments.
+ The object <Foo...> has failed to implement interface <...IFoo>: The contract of module.IFoo.needs_varargs(*args) is violated because 'Foo.needs_varargs(**kwargs)' doesn't support variable arguments.
Of course, missing attributes are also found and reported, and the
source interface of the missing attribute is included. Similarly, when
@@ -264,10 +275,13 @@ the failing method is from a parent class, that is also reported.
.. doctest::
+ >>> oname, __name__ = __name__, 'base'
>>> class IBase(Interface):
... def method(arg1): "Takes one positional argument"
+ >>> __name__ = 'module'
>>> class IFoo(IBase):
... x = Attribute('The X attribute')
+ >>> __name__ = oname; del oname
>>> class Base(object):
... def method(self): "I don't have enough arguments"
>>> @implementer(IFoo)
@@ -275,8 +289,8 @@ the failing method is from a parent class, that is also reported.
... pass
>>> verify_foo()
The object <Foo...> has failed to implement interface <...IFoo>:
- The contract of IBase.method(arg1) is violated because 'Base.method()' doesn't allow enough arguments
- The IFoo.x attribute was not provided
+ The contract of base.IBase.method(arg1) is violated because 'Base.method()' doesn't allow enough arguments
+ The module.IFoo.x attribute was not provided
Verifying Classes
=================
@@ -299,4 +313,4 @@ attributes, cannot be verified.
... print(e)
>>> verify_foo_class()
- The object <class 'Foo'> has failed to implement interface <...IFoo>: The contract of IBase.method(arg1) is violated because 'Base.method(self)' doesn't allow enough arguments.
+ The object <class 'Foo'> has failed to implement interface <...IFoo>: The contract of base.IBase.method(arg1) is violated because 'Base.method(self)' doesn't allow enough arguments.
diff --git a/setup.py b/setup.py
index 9b8b4b8..0e4aa23 100644
--- a/setup.py
+++ b/setup.py
@@ -79,8 +79,11 @@ if is_jython or is_pypy:
else:
ext_modules = codeoptimization
tests_require = [
+ # The test dependencies should NOT have direct or transitive
+ # dependencies on zope.interface.
'coverage >= 5.0.3',
'zope.event',
+ 'zope.testing',
]
testing_extras = tests_require
diff --git a/src/zope/interface/__init__.py b/src/zope/interface/__init__.py
index 605b706..e282fbd 100644
--- a/src/zope/interface/__init__.py
+++ b/src/zope/interface/__init__.py
@@ -60,6 +60,7 @@ del _wire
from zope.interface.declarations import Declaration
from zope.interface.declarations import alsoProvides
from zope.interface.declarations import classImplements
+from zope.interface.declarations import classImplementsFirst
from zope.interface.declarations import classImplementsOnly
from zope.interface.declarations import classProvides
from zope.interface.declarations import directlyProvidedBy
@@ -88,3 +89,5 @@ from zope.interface.interfaces import IInterfaceDeclaration
moduleProvides(IInterfaceDeclaration)
__all__ = ('Interface', 'Attribute') + tuple(IInterfaceDeclaration)
+
+assert all(k in globals() for k in __all__)
diff --git a/src/zope/interface/common/__init__.py b/src/zope/interface/common/__init__.py
index acbc581..a8bedf0 100644
--- a/src/zope/interface/common/__init__.py
+++ b/src/zope/interface/common/__init__.py
@@ -121,19 +121,20 @@ class ABCInterfaceClass(InterfaceClass):
# go ahead and give us a name to ease debugging.
self.__name__ = name
extra_classes = attrs.pop('extra_classes', ())
+ ignored_classes = attrs.pop('ignored_classes', ())
if 'abc' not in attrs:
# Something like ``IList(ISequence)``: We're extending
# abc interfaces but not an ABC interface ourself.
- self.__class__ = InterfaceClass
InterfaceClass.__init__(self, name, bases, attrs)
- for cls in extra_classes:
- classImplements(cls, self)
+ ABCInterfaceClass.__register_classes(self, extra_classes, ignored_classes)
+ self.__class__ = InterfaceClass
return
based_on = attrs.pop('abc')
self.__abc = based_on
self.__extra_classes = tuple(extra_classes)
+ self.__ignored_classes = tuple(ignored_classes)
assert name[1:] == based_on.__name__, (name, based_on)
methods = {
@@ -216,11 +217,14 @@ class ABCInterfaceClass(InterfaceClass):
method.positional = method.positional[1:]
return method
- def __register_classes(self):
+ def __register_classes(self, conformers=None, ignored_classes=None):
# Make the concrete classes already present in our ABC's registry
# declare that they implement this interface.
-
- for cls in self.getRegisteredConformers():
+ conformers = conformers if conformers is not None else self.getRegisteredConformers()
+ ignored = ignored_classes if ignored_classes is not None else self.__ignored_classes
+ for cls in conformers:
+ if cls in ignored:
+ continue
classImplements(cls, self)
def getABC(self):
diff --git a/src/zope/interface/common/builtins.py b/src/zope/interface/common/builtins.py
index 9262340..a07c0a3 100644
--- a/src/zope/interface/common/builtins.py
+++ b/src/zope/interface/common/builtins.py
@@ -37,7 +37,6 @@ __all__ = [
]
# pylint:disable=no-self-argument
-
class IList(collections.IMutableSequence):
"""
Interface for :class:`list`
diff --git a/src/zope/interface/common/collections.py b/src/zope/interface/common/collections.py
index 9731069..6c0496e 100644
--- a/src/zope/interface/common/collections.py
+++ b/src/zope/interface/common/collections.py
@@ -177,6 +177,10 @@ class ISequence(IReversible,
ICollection):
abc = abc.Sequence
extra_classes = (UserString,)
+ # On Python 2, basestring is registered as an ISequence, and
+ # its subclass str is an IByteString. If we also register str as
+ # an ISequence, that tends to lead to inconsistent resolution order.
+ ignored_classes = (basestring,) if str is bytes else () # pylint:disable=undefined-variable
@optional
def __reversed__():
diff --git a/src/zope/interface/common/mapping.py b/src/zope/interface/common/mapping.py
index 13fa317..de56cf8 100644
--- a/src/zope/interface/common/mapping.py
+++ b/src/zope/interface/common/mapping.py
@@ -43,7 +43,7 @@ class IItemMapping(Interface):
"""
-class IReadMapping(IItemMapping, collections.IContainer):
+class IReadMapping(collections.IContainer, IItemMapping):
"""
Basic mapping interface.
@@ -72,7 +72,7 @@ class IWriteMapping(Interface):
"""Set a new item in the mapping."""
-class IEnumerableMapping(IReadMapping, collections.ISized):
+class IEnumerableMapping(collections.ISized, IReadMapping):
"""
Mapping objects whose items can be enumerated.
@@ -171,7 +171,7 @@ class IExtendedWriteMapping(IWriteMapping):
class IFullMapping(
collections.IMutableMapping,
- IExtendedReadMapping, IExtendedWriteMapping, IClonableMapping, IMapping):
+ IExtendedReadMapping, IExtendedWriteMapping, IClonableMapping, IMapping,):
"""
Full mapping interface.
diff --git a/src/zope/interface/common/tests/__init__.py b/src/zope/interface/common/tests/__init__.py
index 059e46c..ade2bf3 100644
--- a/src/zope/interface/common/tests/__init__.py
+++ b/src/zope/interface/common/tests/__init__.py
@@ -38,7 +38,8 @@ def iter_abc_interfaces(predicate=lambda iface: True):
if not predicate(iface):
continue
- registered = list(iface.getRegisteredConformers())
+ registered = set(iface.getRegisteredConformers())
+ registered -= set(iface._ABCInterfaceClass__ignored_classes)
if registered:
yield iface, registered
@@ -50,24 +51,46 @@ def add_abc_interface_tests(cls, module):
def add_verify_tests(cls, iface_classes_iter):
+ cls.maxDiff = None
for iface, registered_classes in iface_classes_iter:
for stdlib_class in registered_classes:
-
def test(self, stdlib_class=stdlib_class, iface=iface):
if stdlib_class in self.UNVERIFIABLE or stdlib_class.__name__ in self.UNVERIFIABLE:
self.skipTest("Unable to verify %s" % stdlib_class)
self.assertTrue(self.verify(iface, stdlib_class))
- name = 'test_auto_' + stdlib_class.__name__ + '_' + iface.__name__
+ suffix = "%s_%s_%s" % (
+ stdlib_class.__name__,
+ iface.__module__.replace('.', '_'),
+ iface.__name__
+ )
+ name = 'test_auto_' + suffix
test.__name__ = name
- assert not hasattr(cls, name)
+ assert not hasattr(cls, name), (name, list(cls.__dict__))
setattr(cls, name, test)
+ def test_ro(self, stdlib_class=stdlib_class, iface=iface):
+ from zope.interface import ro
+ from zope.interface import implementedBy
+ self.assertEqual(
+ tuple(ro.ro(iface, strict=True)),
+ iface.__sro__)
+ implements = implementedBy(stdlib_class)
+ strict = stdlib_class not in self.NON_STRICT_RO
+ self.assertEqual(
+ tuple(ro.ro(implements, strict=strict)),
+ implements.__sro__)
+
+ name = 'test_auto_ro_' + suffix
+ test_ro.__name__ = name
+ assert not hasattr(cls, name)
+ setattr(cls, name, test_ro)
class VerifyClassMixin(unittest.TestCase):
verifier = staticmethod(verifyClass)
UNVERIFIABLE = ()
+ NON_STRICT_RO = ()
def _adjust_object_before_verify(self, iface, x):
return x
diff --git a/src/zope/interface/common/tests/test_collections.py b/src/zope/interface/common/tests/test_collections.py
index 32ab801..f06e12e 100644
--- a/src/zope/interface/common/tests/test_collections.py
+++ b/src/zope/interface/common/tests/test_collections.py
@@ -17,6 +17,7 @@ try:
except ImportError:
import collections as abc
from collections import deque
+from collections import OrderedDict
try:
@@ -118,6 +119,9 @@ class TestVerifyClass(VerifyClassMixin, unittest.TestCase):
type({}.viewitems()),
type({}.viewkeys()),
})
+ NON_STRICT_RO = {
+ OrderedDict
+ }
add_abc_interface_tests(TestVerifyClass, collections.ISet.__module__)
diff --git a/src/zope/interface/declarations.py b/src/zope/interface/declarations.py
index 9c15b9b..1e9a2ea 100644
--- a/src/zope/interface/declarations.py
+++ b/src/zope/interface/declarations.py
@@ -449,35 +449,79 @@ def classImplementsOnly(cls, *interfaces):
def classImplements(cls, *interfaces):
- """Declare additional interfaces implemented for instances of a class
+ """
+ Declare additional interfaces implemented for 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)
- are added to any interfaces previously declared.
+ The interfaces given (including the interfaces in the specifications)
+ are added to any interfaces previously declared. An effort is made to
+ keep a consistent C3 resolution order, but this cannot be guaranteed.
+
+ .. versionchanged:: 5.0.0
+ Each individual interface in *interfaces* may be added to either the
+ 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.
"""
spec = implementedBy(cls)
- spec.declared += tuple(_normalizeargs(interfaces))
+ interfaces = tuple(_normalizeargs(interfaces))
+
+ before = []
+ after = []
+
+ # Take steps to try to avoid producing an invalid resolution
+ # order, while still allowing for BWC (in the past, we always
+ # appended)
+ for iface in interfaces:
+ for b in spec.declared:
+ if iface.extends(b):
+ before.append(iface)
+ break
+ else:
+ after.append(iface)
+ _classImplements_ordered(spec, tuple(before), tuple(after))
- # compute the bases
- bases = []
- seen = {}
- for b in spec.declared:
+
+def classImplementsFirst(cls, iface):
+ """
+ Declare that instances of *cls* additionally provide *iface*.
+
+ The second argument is an interface or interface specification.
+ It is added as the highest priority (first in the IRO) interface;
+ no attempt is made to keep a consistent resolution order.
+
+ .. versionadded:: 5.0.0
+ """
+ spec = implementedBy(cls)
+ _classImplements_ordered(spec, (iface,), ())
+
+
+def _classImplements_ordered(spec, before=(), after=()):
+ # eliminate duplicates
+ new_declared = []
+ seen = set()
+ for b in before + spec.declared + after:
if b not in seen:
- seen[b] = 1
- bases.append(b)
+ new_declared.append(b)
+ seen.add(b)
- if spec.inherit is not None:
+ spec.declared = tuple(new_declared)
+
+ # compute the bases
+ bases = new_declared # guaranteed no dupes
+ if spec.inherit is not None:
for c in spec.inherit.__bases__:
b = implementedBy(c)
if b not in seen:
- seen[b] = 1
+ seen.add(b)
bases.append(b)
spec.__bases__ = tuple(bases)
+
def _implements_advice(cls):
interfaces, classImplements = cls.__dict__['__implements_advice_data__']
del cls.__implements_advice_data__
@@ -664,6 +708,13 @@ class Provides(Declaration): # Really named ProvidesClass
self._cls = cls
Declaration.__init__(self, *(interfaces + (implementedBy(cls), )))
+ def __repr__(self):
+ return "<%s.%s for %s>" % (
+ self.__class__.__module__,
+ self.__class__.__name__,
+ self._cls,
+ )
+
def __reduce__(self):
return Provides, self.__args
@@ -794,6 +845,13 @@ class ClassProvides(Declaration, ClassProvidesBase):
self.__args = (cls, metacls, ) + interfaces
Declaration.__init__(self, *(interfaces + (implementedBy(metacls), )))
+ def __repr__(self):
+ return "<%s.%s for %s>" % (
+ self.__class__.__module__,
+ self.__class__.__name__,
+ self._cls,
+ )
+
def __reduce__(self):
return self.__class__, self.__args
diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py
index ade6f42..3e81b6f 100644
--- a/src/zope/interface/interface.py
+++ b/src/zope/interface/interface.py
@@ -283,7 +283,14 @@ class Specification(SpecificationBase):
implied = self._implied
implied.clear()
- ancestors = ro(self)
+ if len(self.__bases__) == 1:
+ # Fast path: One base makes it trivial to calculate
+ # the MRO.
+ sro = self.__bases__[0].__sro__
+ ancestors = [self]
+ ancestors.extend(sro)
+ else:
+ ancestors = ro(self)
try:
if Interface not in ancestors:
@@ -647,7 +654,9 @@ class Attribute(Element):
return ""
def __str__(self):
- of = self.interface.__name__ + '.' if self.interface else ''
+ of = ''
+ if self.interface is not None:
+ of = self.interface.__module__ + '.' + self.interface.__name__ + '.'
return of + self.__name__ + self._get_str_info()
def __repr__(self):
diff --git a/src/zope/interface/interfaces.py b/src/zope/interface/interfaces.py
index 6ac235a..bf0d6c7 100644
--- a/src/zope/interface/interfaces.py
+++ b/src/zope/interface/interfaces.py
@@ -441,6 +441,11 @@ class IInterfaceDeclaration(Interface):
instances of ``A`` and ``B`` provide.
"""
+ def classImplementsFirst(cls, interface):
+ """
+ See :func:`zope.interface.classImplementsFirst`.
+ """
+
def implementer(*interfaces):
"""Create a decorator for declaring interfaces implemented by a factory.
diff --git a/src/zope/interface/registry.py b/src/zope/interface/registry.py
index 3f1306f..90ae1ad 100644
--- a/src/zope/interface/registry.py
+++ b/src/zope/interface/registry.py
@@ -550,7 +550,7 @@ def _getAdapterRequired(factory, required):
r = implementedBy(r)
else:
raise TypeError("Required specification must be a "
- "specification or class."
+ "specification or class, not %r" % type(r)
)
result.append(r)
return tuple(result)
diff --git a/src/zope/interface/ro.py b/src/zope/interface/ro.py
index 855f101..dbffb53 100644
--- a/src/zope/interface/ro.py
+++ b/src/zope/interface/ro.py
@@ -11,15 +11,71 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
-"""Compute a resolution order for an object and its bases
"""
+Compute a resolution order for an object and its bases.
+
+.. versionchanged:: 5.0
+ The resolution order is now based on the same C3 order that Python
+ uses for classes. In complex instances of multiple inheritance, this
+ may result in a different ordering.
+
+ In older versions, the ordering wasn't required to be C3 compliant,
+ and for backwards compatibility, it still isn't. If the ordering
+ isn't C3 compliant (if it is *inconsistent*), zope.interface will
+ make a best guess to try to produce a reasonable resolution order.
+ Still (just as before), the results in such cases may be
+ surprising.
+
+.. rubric:: Environment Variables
+
+Due to the change in 5.0, certain environment variables can be used to control errors
+and warnings about inconsistent resolution orders. They are listed in priority order, with
+variables at the bottom generally overriding variables above them.
+
+ZOPE_INTERFACE_WARN_BAD_IRO
+ If this is set to "1", then if there is at least one inconsistent resolution
+ order discovered, a warning (:class:`InconsistentResolutionOrderWarning`) will
+ be issued. Use the usual warning mechanisms to control this behaviour. The warning
+ text will contain additional information on debugging.
+ZOPE_INTERFACE_TRACK_BAD_IRO
+ If this is set to "1", then zope.interface will log information about each
+ inconsistent resolution order discovered, and keep those details in memory in this module
+ for later inspection.
+ZOPE_INTERFACE_STRICT_IRO
+ If this is set to "1", any attempt to use :func:`ro` that would produce a non-C3
+ ordering will fail by raising :class:`InconsistentResolutionOrderError`.
+
+There are two environment variables that are independent.
+
+ZOPE_INTERFACE_LOG_CHANGED_IRO
+ If this is set to "1", then if the C3 resolution order is different from
+ the legacy resolution order for any given object, a message explaining the differences
+ will be logged. This is intended to be used for debugging complicated IROs.
+ZOPE_INTERFACE_USE_LEGACY_IRO
+ If this is set to "1", then the C3 resolution order will *not* be used. The
+ legacy IRO will be used instead. This is a temporary measure and will be removed in the
+ future. It is intended to help during the transition.
+ It implies ``ZOPE_INTERFACE_LOG_CHANGED_IRO``.
+"""
+from __future__ import print_function
__docformat__ = 'restructuredtext'
__all__ = [
'ro',
+ 'InconsistentResolutionOrderError',
+ 'InconsistentResolutionOrderWarning',
]
-def _mergeOrderings(orderings):
+__logger = None
+
+def _logger():
+ global __logger # pylint:disable=global-statement
+ if __logger is None:
+ import logging
+ __logger = logging.getLogger(__name__)
+ return __logger
+
+def _legacy_mergeOrderings(orderings):
"""Merge multiple orderings so that within-ordering order is preserved
Orderings are constrained in such a way that if an object appears
@@ -38,18 +94,18 @@ def _mergeOrderings(orderings):
"""
- seen = {}
+ seen = set()
result = []
for ordering in reversed(orderings):
for o in reversed(ordering):
if o not in seen:
- seen[o] = 1
+ seen.add(o)
result.insert(0, o)
return result
-def _flatten(ob):
- result = [ob]
+def _legacy_flatten(begin):
+ result = [begin]
i = 0
for ob in iter(result):
i += 1
@@ -61,8 +117,489 @@ def _flatten(ob):
result[i:i] = ob.__bases__
return result
+def _legacy_ro(ob):
+ return _legacy_mergeOrderings([_legacy_flatten(ob)])
+
+###
+# Compare base objects using identity, not equality. This matches what
+# the CPython MRO algorithm does, and is *much* faster to boot: that,
+# plus some other small tweaks makes the difference between 25s and 6s
+# in loading 446 plone/zope interface.py modules (1925 InterfaceClass,
+# 1200 Implements, 1100 ClassProvides objects)
+###
+
+
+class InconsistentResolutionOrderWarning(PendingDeprecationWarning):
+ """
+ The warning issued when an invalid IRO is requested.
+ """
+
+class InconsistentResolutionOrderError(TypeError):
+ """
+ The error raised when an invalid IRO is requested in strict mode.
+ """
+
+ def __init__(self, c3, base_tree_remaining):
+ self.C = c3.leaf
+ base_tree = c3.base_tree
+ self.base_ros = {
+ base: base_tree[i + 1]
+ for i, base in enumerate(self.C.__bases__)
+ }
+ # Unfortunately, this doesn't necessarily directly match
+ # up to any transformation on C.__bases__, because
+ # if any were fully used up, they were removed already.
+ self.base_tree_remaining = base_tree_remaining
+
+ TypeError.__init__(self)
+
+ def __str__(self):
+ import pprint
+ return "%s: For object %r.\nBase ROs:\n%s\nConflict Location:\n%s" % (
+ self.__class__.__name__,
+ self.C,
+ pprint.pformat(self.base_ros),
+ pprint.pformat(self.base_tree_remaining),
+ )
+
+
+class _ClassBoolFromEnv(object):
+ """
+ Non-data descriptor that reads a transformed environment variable
+ as a boolean, and caches the result in the class.
+ """
+
+ def __get__(self, inst, klass):
+ import os
+ for cls in klass.__mro__:
+ my_name = None
+ for k in dir(klass):
+ if k in cls.__dict__ and cls.__dict__[k] is self:
+ my_name = k
+ break
+ if my_name is not None:
+ break
+ else: # pragma: no cover
+ raise RuntimeError("Unable to find self")
+
+ env_name = 'ZOPE_INTERFACE_' + my_name
+ val = os.environ.get(env_name, '') == '1'
+ setattr(klass, my_name, val)
+ setattr(klass, 'ORIG_' + my_name, self)
+ return val
+
+
+class C3(object):
+ # Holds the shared state during computation of an MRO.
+
+ @staticmethod
+ def resolver(C, strict):
+ strict = strict if strict is not None else C3.STRICT_RO
+ factory = C3
+ if strict:
+ factory = _StrictC3
+ elif C3.TRACK_BAD_IRO:
+ factory = _TrackingC3
+
+ return factory(C, {})
+
+ __mro = None
+ __legacy_ro = None
+ direct_inconsistency = False
+
+ def __init__(self, C, memo):
+ self.leaf = C
+ self.memo = memo
+ kind = self.__class__
+
+ base_resolvers = []
+ for base in C.__bases__:
+ if base not in memo:
+ resolver = kind(base, memo)
+ memo[base] = resolver
+ base_resolvers.append(memo[base])
+
+ self.base_tree = [
+ [C]
+ ] + [
+ memo[base].mro() for base in C.__bases__
+ ] + [
+ list(C.__bases__)
+ ]
+
+ self.bases_had_inconsistency = any(base.had_inconsistency for base in base_resolvers)
+
+ @property
+ def had_inconsistency(self):
+ return self.direct_inconsistency or self.bases_had_inconsistency
+
+ @property
+ def legacy_ro(self):
+ if self.__legacy_ro is None:
+ self.__legacy_ro = tuple(_legacy_ro(self.leaf))
+ return list(self.__legacy_ro)
+
+ TRACK_BAD_IRO = _ClassBoolFromEnv()
+ STRICT_RO = _ClassBoolFromEnv()
+ WARN_BAD_IRO = _ClassBoolFromEnv()
+ LOG_CHANGED_IRO = _ClassBoolFromEnv()
+ USE_LEGACY_IRO = _ClassBoolFromEnv()
+ BAD_IROS = ()
+
+ def _warn_iro(self):
+ if not self.WARN_BAD_IRO:
+ # For the initial release, one must opt-in to see the warning.
+ # In the future (2021?) seeing at least the first warning will
+ # be the default
+ return
+ import warnings
+ warnings.warn(
+ "An inconsistent resolution order is being requested. "
+ "(Interfaces should follow the Python class rules known as C3.) "
+ "For backwards compatibility, zope.interface will allow this, "
+ "making the best guess it can to produce as meaningful an order as possible. "
+ "In the future this might be an error. Set the warning filter to error, or set "
+ "the environment variable 'ZOPE_INTERFACE_TRACK_BAD_IRO' to '1' and examine "
+ "ro.C3.BAD_IROS to debug, or set 'ZOPE_INTERFACE_STRICT_IRO' to raise exceptions.",
+ InconsistentResolutionOrderWarning,
+ )
+
+ @staticmethod
+ def _can_choose_base(base, base_tree_remaining):
+ # From C3:
+ # nothead = [s for s in nonemptyseqs if cand in s[1:]]
+ for bases in base_tree_remaining:
+ if not bases or bases[0] is base:
+ continue
+
+ for b in bases:
+ if b is base:
+ return False
+ return True
+
+ @staticmethod
+ def _nonempty_bases_ignoring(base_tree, ignoring):
+ return list(filter(None, [
+ [b for b in bases if b is not ignoring]
+ for bases
+ in base_tree
+ ]))
+
+ def _choose_next_base(self, base_tree_remaining):
+ """
+ Return the next base.
+
+ The return value will either fit the C3 constraints or be our best
+ guess about what to do. If we cannot guess, this may raise an exception.
+ """
+ base = self._find_next_C3_base(base_tree_remaining)
+ if base is not None:
+ return base
+ return self._guess_next_base(base_tree_remaining)
+
+ def _find_next_C3_base(self, base_tree_remaining):
+ """
+ Return the next base that fits the constraints, or ``None`` if there isn't one.
+ """
+ for bases in base_tree_remaining:
+ base = bases[0]
+ if self._can_choose_base(base, base_tree_remaining):
+ return base
+ return None
+
+ class _UseLegacyRO(Exception):
+ pass
+
+ def _guess_next_base(self, base_tree_remaining):
+ # Narf. We may have an inconsistent order (we won't know for
+ # sure until we check all the bases). Python cannot create
+ # classes like this:
+ #
+ # class B1:
+ # pass
+ # class B2(B1):
+ # pass
+ # class C(B1, B2): # -> TypeError; this is like saying C(B1, B2, B1).
+ # pass
+ #
+ # However, older versions of zope.interface were fine with this order.
+ # A good example is ``providedBy(IOError())``. Because of the way
+ # ``classImplements`` works, it winds up with ``__bases__`` ==
+ # ``[IEnvironmentError, IIOError, IOSError, <implementedBy Exception>]``
+ # (on Python 3). But ``IEnvironmentError`` is a base of both ``IIOError``
+ # and ``IOSError``. Previously, we would get a resolution order of
+ # ``[IIOError, IOSError, IEnvironmentError, IStandardError, IException, Interface]``
+ # but the standard Python algorithm would forbid creating that order entirely.
-def ro(object):
- """Compute a "resolution order" for an object
+ # Unlike Python's MRO, we attempt to resolve the issue. A few
+ # heuristics have been tried. One was:
+ #
+ # Strip off the first (highest priority) base of each direct
+ # base one at a time and seeing if we can come to an agreement
+ # with the other bases. (We're trying for a partial ordering
+ # here.) This often resolves cases (such as the IOSError case
+ # above), and frequently produces the same ordering as the
+ # legacy MRO did. If we looked at all the highest priority
+ # bases and couldn't find any partial ordering, then we strip
+ # them *all* out and begin the C3 step again. We take care not
+ # to promote a common root over all others.
+ #
+ # If we only did the first part, stripped off the first
+ # element of the first item, we could resolve simple cases.
+ # But it tended to fail badly. If we did the whole thing, it
+ # could be extremely painful from a performance perspective
+ # for deep/wide things like Zope's OFS.SimpleItem.Item. Plus,
+ # anytime you get ExtensionClass.Base into the mix, you're
+ # likely to wind up in trouble, because it messes with the MRO
+ # of classes. Sigh.
+ #
+ # So now, we fall back to the old linearization (fast to compute).
+ self._warn_iro()
+ self.direct_inconsistency = InconsistentResolutionOrderError(self, base_tree_remaining)
+ raise self._UseLegacyRO
+
+ def _merge(self):
+ # Returns a merged *list*.
+ result = self.__mro = []
+ base_tree_remaining = self.base_tree
+ base = None
+ while 1:
+ # Take last picked base out of the base tree wherever it is.
+ # This differs slightly from the standard Python MRO and is needed
+ # because we have no other step that prevents duplicates
+ # from coming in (e.g., in the inconsistent fallback path)
+ base_tree_remaining = self._nonempty_bases_ignoring(base_tree_remaining, base)
+
+ if not base_tree_remaining:
+ return result
+ try:
+ base = self._choose_next_base(base_tree_remaining)
+ except self._UseLegacyRO:
+ self.__mro = self.legacy_ro
+ return self.legacy_ro
+
+ result.append(base)
+
+ def mro(self):
+ if self.__mro is None:
+ self.__mro = tuple(self._merge())
+ return list(self.__mro)
+
+
+class _StrictC3(C3):
+ __slots__ = ()
+ def _guess_next_base(self, base_tree_remaining):
+ raise InconsistentResolutionOrderError(self, base_tree_remaining)
+
+
+class _TrackingC3(C3):
+ __slots__ = ()
+ def _guess_next_base(self, base_tree_remaining):
+ import traceback
+ bad_iros = C3.BAD_IROS
+ if self.leaf not in bad_iros:
+ if bad_iros == ():
+ import weakref
+ # This is a race condition, but it doesn't matter much.
+ bad_iros = C3.BAD_IROS = weakref.WeakKeyDictionary()
+ bad_iros[self.leaf] = t = (
+ InconsistentResolutionOrderError(self, base_tree_remaining),
+ traceback.format_stack()
+ )
+ _logger().warning("Tracking inconsistent IRO: %s", t[0])
+ return C3._guess_next_base(self, base_tree_remaining)
+
+
+class _ROComparison(object):
+ # Exists to compute and print a pretty string comparison
+ # for differing ROs.
+ # Since we're used in a logging context, and may actually never be printed,
+ # this is a class so we can defer computing the diff until asked.
+
+ # Components we use to build up the comparison report
+ class Item(object):
+ prefix = ' '
+ def __init__(self, item):
+ self.item = item
+ def __str__(self):
+ return "%s%s" % (
+ self.prefix,
+ self.item,
+ )
+
+ class Deleted(Item):
+ prefix = '- '
+
+ class Inserted(Item):
+ prefix = '+ '
+
+ Empty = str
+
+ class ReplacedBy(object): # pragma: no cover
+ prefix = '- '
+ suffix = ''
+ def __init__(self, chunk, total_count):
+ self.chunk = chunk
+ self.total_count = total_count
+
+ def __iter__(self):
+ lines = [
+ self.prefix + str(item) + self.suffix
+ for item in self.chunk
+ ]
+ while len(lines) < self.total_count:
+ lines.append('')
+
+ return iter(lines)
+
+ class Replacing(ReplacedBy):
+ prefix = "+ "
+ suffix = ''
+
+
+ _c3_report = None
+ _legacy_report = None
+
+ def __init__(self, c3, c3_ro, legacy_ro):
+ self.c3 = c3
+ self.c3_ro = c3_ro
+ self.legacy_ro = legacy_ro
+
+ def __move(self, from_, to_, chunk, operation):
+ for x in chunk:
+ to_.append(operation(x))
+ from_.append(self.Empty())
+
+ def _generate_report(self):
+ if self._c3_report is None:
+ import difflib
+ # The opcodes we get describe how to turn 'a' into 'b'. So
+ # the old one (legacy) needs to be first ('a')
+ matcher = difflib.SequenceMatcher(None, self.legacy_ro, self.c3_ro)
+ # The reports are equal length sequences. We're going for a
+ # side-by-side diff.
+ self._c3_report = c3_report = []
+ self._legacy_report = legacy_report = []
+ for opcode, leg1, leg2, c31, c32 in matcher.get_opcodes():
+ c3_chunk = self.c3_ro[c31:c32]
+ legacy_chunk = self.legacy_ro[leg1:leg2]
+
+ if opcode == 'equal':
+ # Guaranteed same length
+ c3_report.extend((self.Item(x) for x in c3_chunk))
+ legacy_report.extend(self.Item(x) for x in legacy_chunk)
+ if opcode == 'delete':
+ # Guaranteed same length
+ assert not c3_chunk
+ self.__move(c3_report, legacy_report, legacy_chunk, self.Deleted)
+ if opcode == 'insert':
+ # Guaranteed same length
+ assert not legacy_chunk
+ self.__move(legacy_report, c3_report, c3_chunk, self.Inserted)
+ if opcode == 'replace': # pragma: no cover (How do you make it output this?)
+ # Either side could be longer.
+ chunk_size = max(len(c3_chunk), len(legacy_chunk))
+ c3_report.extend(self.Replacing(c3_chunk, chunk_size))
+ legacy_report.extend(self.ReplacedBy(legacy_chunk, chunk_size))
+
+ return self._c3_report, self._legacy_report
+
+ @property
+ def _inconsistent_label(self):
+ inconsistent = []
+ if self.c3.direct_inconsistency:
+ inconsistent.append('direct')
+ if self.c3.bases_had_inconsistency:
+ inconsistent.append('bases')
+ return '+'.join(inconsistent) if inconsistent else 'no'
+
+ def __str__(self):
+ c3_report, legacy_report = self._generate_report()
+ assert len(c3_report) == len(legacy_report)
+
+ left_lines = [str(x) for x in legacy_report]
+ right_lines = [str(x) for x in c3_report]
+
+ # We have the same number of non-empty lines as we do items
+ # in the resolution order.
+ assert len(list(filter(None, left_lines))) == len(self.c3_ro)
+ assert len(list(filter(None, right_lines))) == len(self.c3_ro)
+
+ padding = ' ' * 2
+ max_left = max(len(x) for x in left_lines)
+ max_right = max(len(x) for x in right_lines)
+
+ left_title = 'Legacy RO (len=%s)' % (len(self.legacy_ro),)
+
+ right_title = 'C3 RO (len=%s; inconsistent=%s)' % (
+ len(self.c3_ro),
+ self._inconsistent_label,
+ )
+ lines = [
+ (padding + left_title.ljust(max_left) + padding + right_title.ljust(max_right)),
+ padding + '=' * (max_left + len(padding) + max_right)
+ ]
+ lines += [
+ padding + left.ljust(max_left) + padding + right
+ for left, right in zip(left_lines, right_lines)
+ ]
+
+ return '\n'.join(lines)
+
+
+def ro(C, strict=None, log_changed_ro=None, use_legacy_ro=None):
+ """
+ ro(C) -> list
+
+ Compute the precedence list (mro) according to C3.
+
+ As an implementation note, this always calculates the full MRO by
+ examining all the bases recursively. If there are special cases
+ that can reuse pre-calculated partial MROs, such as a
+ ``__bases__`` of length one, the caller is responsible for
+ optimizing that. (This is because this function doesn't know how
+ to get the complete MRO of a base; it only knows how to get their
+ ``__bases__``.)
+
+ :return: A fresh `list` object.
+
+ .. versionchanged:: 5.0.0
+ Add the *strict*, *log_changed_ro* and *use_legacy_ro*
+ keyword arguments. These are provisional and likely to be
+ removed in the future. They are most useful for testing.
+ """
+ resolver = C3.resolver(C, strict)
+ mro = resolver.mro()
+
+ log_changed = log_changed_ro if log_changed_ro is not None else resolver.LOG_CHANGED_IRO
+ use_legacy = use_legacy_ro if use_legacy_ro is not None else resolver.USE_LEGACY_IRO
+
+ if log_changed or use_legacy:
+ legacy_ro = resolver.legacy_ro
+ assert isinstance(legacy_ro, list)
+ assert isinstance(mro, list)
+ if legacy_ro != mro:
+ comparison = _ROComparison(resolver, mro, legacy_ro)
+ _logger().warning(
+ "Object %r has different legacy and C3 MROs:\n%s",
+ C, comparison
+ )
+ if resolver.had_inconsistency and legacy_ro == mro:
+ comparison = _ROComparison(resolver, mro, legacy_ro)
+ _logger().warning(
+ "Object %r had inconsistent IRO and used the legacy RO:\n%s"
+ "\nInconsistency entered at:\n%s",
+ C, comparison, resolver.direct_inconsistency
+ )
+ if use_legacy:
+ return legacy_ro
+
+ return mro
+
+
+def is_consistent(C):
+ """
+ Check if the resolution order for *C*, as computed by :func:`ro`, is consistent
+ according to C3.
"""
- return _mergeOrderings([_flatten(object)])
+ return not C3.resolver(C, False).had_inconsistency
diff --git a/src/zope/interface/tests/test_declarations.py b/src/zope/interface/tests/test_declarations.py
index ccf4dde..0cdb326 100644
--- a/src/zope/interface/tests/test_declarations.py
+++ b/src/zope/interface/tests/test_declarations.py
@@ -191,9 +191,13 @@ class DeclarationTests(unittest.TestCase):
from zope.interface.interface import InterfaceClass
IFoo = InterfaceClass('IFoo')
IBar = InterfaceClass('IBar')
+ # This is the same as calling ``Declaration(IBar, IFoo, IBar)``
+ # which doesn't make much sense, but here it is. In older
+ # versions of zope.interface, the __iro__ would have been
+ # IFoo, IBar, Interface, which especially makes no sense.
decl = self._makeOne(IBar, (IFoo, IBar))
# Note that decl.__iro__ has IFoo first.
- self.assertEqual(list(decl.flattened()), [IFoo, IBar, Interface])
+ self.assertEqual(list(decl.flattened()), [IBar, IFoo, Interface])
def test___sub___unrelated_interface(self):
from zope.interface.interface import InterfaceClass
@@ -1122,6 +1126,13 @@ class ProvidesClassTests(unittest.TestCase):
return foo.__provides__
self.assertRaises(AttributeError, _test)
+ def test__repr__(self):
+ inst = self._makeOne(type(self))
+ self.assertEqual(
+ repr(inst),
+ "<zope.interface.Provides for %r>" % type(self)
+ )
+
class Test_Provides(unittest.TestCase):
@@ -1391,6 +1402,12 @@ class ClassProvidesTests(unittest.TestCase):
self.assertEqual(cp.__reduce__(),
(self._getTargetClass(), (Foo, type(Foo), IBar)))
+ def test__repr__(self):
+ inst = self._makeOne(type(self), type)
+ self.assertEqual(
+ repr(inst),
+ "<zope.interface.declarations.ClassProvides for %r>" % type(self)
+ )
class Test_directlyProvidedBy(unittest.TestCase):
diff --git a/src/zope/interface/tests/test_interface.py b/src/zope/interface/tests/test_interface.py
index 7bde955..df7f84b 100644
--- a/src/zope/interface/tests/test_interface.py
+++ b/src/zope/interface/tests/test_interface.py
@@ -1911,7 +1911,7 @@ class AttributeTests(ElementTests):
method.interface = type(self)
r = repr(method)
self.assertTrue(r.startswith('<zope.interface.interface.Attribute object at'), r)
- self.assertTrue(r.endswith(' AttributeTests.TestAttribute>'), r)
+ self.assertTrue(r.endswith(' ' + __name__ + '.AttributeTests.TestAttribute>'), r)
def test__repr__wo_interface(self):
method = self._makeOne()
@@ -1923,7 +1923,7 @@ class AttributeTests(ElementTests):
method = self._makeOne()
method.interface = type(self)
r = str(method)
- self.assertEqual(r, 'AttributeTests.TestAttribute')
+ self.assertEqual(r, __name__ + '.AttributeTests.TestAttribute')
def test__str__wo_interface(self):
method = self._makeOne()
@@ -1998,7 +1998,7 @@ class MethodTests(AttributeTests):
method.interface = type(self)
r = repr(method)
self.assertTrue(r.startswith('<zope.interface.interface.Method object at'), r)
- self.assertTrue(r.endswith(' MethodTests.TestMethod(**kw)>'), r)
+ self.assertTrue(r.endswith(' ' + __name__ + '.MethodTests.TestMethod(**kw)>'), r)
def test__repr__wo_interface(self):
method = self._makeOne()
@@ -2012,7 +2012,7 @@ class MethodTests(AttributeTests):
method.kwargs = 'kw'
method.interface = type(self)
r = str(method)
- self.assertEqual(r, 'MethodTests.TestMethod(**kw)')
+ self.assertEqual(r, __name__ + '.MethodTests.TestMethod(**kw)')
def test__str__wo_interface(self):
method = self._makeOne()
diff --git a/src/zope/interface/tests/test_ro.py b/src/zope/interface/tests/test_ro.py
index 0756c6d..3a516b5 100644
--- a/src/zope/interface/tests/test_ro.py
+++ b/src/zope/interface/tests/test_ro.py
@@ -14,12 +14,13 @@
"""Resolution ordering utility tests"""
import unittest
+# pylint:disable=blacklisted-name,protected-access,attribute-defined-outside-init
class Test__mergeOrderings(unittest.TestCase):
def _callFUT(self, orderings):
- from zope.interface.ro import _mergeOrderings
- return _mergeOrderings(orderings)
+ from zope.interface.ro import _legacy_mergeOrderings
+ return _legacy_mergeOrderings(orderings)
def test_empty(self):
self.assertEqual(self._callFUT([]), [])
@@ -30,7 +31,7 @@ class Test__mergeOrderings(unittest.TestCase):
def test_w_duplicates(self):
self.assertEqual(self._callFUT([['a'], ['b', 'a']]), ['b', 'a'])
- def test_suffix_across_multiple_duplicats(self):
+ def test_suffix_across_multiple_duplicates(self):
O1 = ['x', 'y', 'z']
O2 = ['q', 'z']
O3 = [1, 3, 5]
@@ -42,8 +43,8 @@ class Test__mergeOrderings(unittest.TestCase):
class Test__flatten(unittest.TestCase):
def _callFUT(self, ob):
- from zope.interface.ro import _flatten
- return _flatten(ob)
+ from zope.interface.ro import _legacy_flatten
+ return _legacy_flatten(ob)
def test_w_empty_bases(self):
class Foo(object):
@@ -78,10 +79,10 @@ class Test__flatten(unittest.TestCase):
class Test_ro(unittest.TestCase):
-
- def _callFUT(self, ob):
- from zope.interface.ro import ro
- return ro(ob)
+ maxDiff = None
+ def _callFUT(self, ob, **kwargs):
+ from zope.interface.ro import _legacy_ro
+ return _legacy_ro(ob, **kwargs)
def test_w_empty_bases(self):
class Foo(object):
@@ -113,3 +114,286 @@ class Test_ro(unittest.TestCase):
pass
self.assertEqual(self._callFUT(Qux),
[Qux, Bar, Baz, Foo, object])
+
+ def _make_IOErr(self):
+ # This can't be done in the standard C3 ordering.
+ class Foo(object):
+ def __init__(self, name, *bases):
+ self.__name__ = name
+ self.__bases__ = bases
+ def __repr__(self): # pragma: no cover
+ return self.__name__
+
+ # Mimic what classImplements(IOError, IIOError)
+ # does.
+ IEx = Foo('IEx')
+ IStdErr = Foo('IStdErr', IEx)
+ IEnvErr = Foo('IEnvErr', IStdErr)
+ IIOErr = Foo('IIOErr', IEnvErr)
+ IOSErr = Foo('IOSErr', IEnvErr)
+
+ IOErr = Foo('IOErr', IEnvErr, IIOErr, IOSErr)
+ return IOErr, [IOErr, IIOErr, IOSErr, IEnvErr, IStdErr, IEx]
+
+ def test_non_orderable(self):
+ IOErr, bases = self._make_IOErr()
+
+ self.assertEqual(self._callFUT(IOErr), bases)
+
+ def test_mixed_inheritance_and_implementation(self):
+ # https://github.com/zopefoundation/zope.interface/issues/8
+ # This test should fail, but doesn't, as described in that issue.
+ # pylint:disable=inherit-non-class
+ from zope.interface import implementer
+ from zope.interface import Interface
+ from zope.interface import providedBy
+ from zope.interface import implementedBy
+
+ class IFoo(Interface):
+ pass
+
+ @implementer(IFoo)
+ class ImplementsFoo(object):
+ pass
+
+ class ExtendsFoo(ImplementsFoo):
+ pass
+
+ class ImplementsNothing(object):
+ pass
+
+ class ExtendsFooImplementsNothing(ExtendsFoo, ImplementsNothing):
+ pass
+
+ self.assertEqual(
+ self._callFUT(providedBy(ExtendsFooImplementsNothing())),
+ [implementedBy(ExtendsFooImplementsNothing),
+ implementedBy(ExtendsFoo),
+ implementedBy(ImplementsFoo),
+ IFoo,
+ Interface,
+ implementedBy(ImplementsNothing),
+ implementedBy(object)])
+
+
+class Test_c3_ro(Test_ro):
+
+ def setUp(self):
+ Test_ro.setUp(self)
+ from zope.testing.loggingsupport import InstalledHandler
+ self.log_handler = handler = InstalledHandler('zope.interface.ro')
+ self.addCleanup(handler.uninstall)
+
+ def _callFUT(self, ob, **kwargs):
+ from zope.interface.ro import ro
+ return ro(ob, **kwargs)
+
+ def test_complex_diamond(self, base=object):
+ # https://github.com/zopefoundation/zope.interface/issues/21
+ O = base
+ class F(O):
+ pass
+ class E(O):
+ pass
+ class D(O):
+ pass
+ class C(D, F):
+ pass
+ class B(D, E):
+ pass
+ class A(B, C):
+ pass
+
+ if hasattr(A, 'mro'):
+ self.assertEqual(A.mro(), self._callFUT(A))
+
+ return A
+
+ def test_complex_diamond_interface(self):
+ from zope.interface import Interface
+
+ IA = self.test_complex_diamond(Interface)
+
+ self.assertEqual(
+ [x.__name__ for x in IA.__iro__],
+ ['A', 'B', 'C', 'D', 'E', 'F', 'Interface']
+ )
+
+ def test_complex_diamond_use_legacy_argument(self):
+ from zope.interface import Interface
+
+ A = self.test_complex_diamond(Interface)
+ legacy_A_iro = self._callFUT(A, use_legacy_ro=True)
+ self.assertNotEqual(A.__iro__, legacy_A_iro)
+
+ # And logging happened as a side-effect.
+ self._check_handler_complex_diamond()
+
+ def test_complex_diamond_compare_legacy_argument(self):
+ from zope.interface import Interface
+
+ A = self.test_complex_diamond(Interface)
+ computed_A_iro = self._callFUT(A, log_changed_ro=True)
+ # It matches, of course, but we did log a warning.
+ self.assertEqual(tuple(computed_A_iro), A.__iro__)
+ self._check_handler_complex_diamond()
+
+ def _check_handler_complex_diamond(self):
+ handler = self.log_handler
+ self.assertEqual(1, len(handler.records))
+ record = handler.records[0]
+
+ self.assertEqual('\n'.join(l.rstrip() for l in record.getMessage().splitlines()), """\
+Object <InterfaceClass zope.interface.tests.test_ro.A> has different legacy and C3 MROs:
+ Legacy RO (len=7) C3 RO (len=7; inconsistent=no)
+ ====================================================================================================
+ <InterfaceClass zope.interface.tests.test_ro.A> <InterfaceClass zope.interface.tests.test_ro.A>
+ <InterfaceClass zope.interface.tests.test_ro.B> <InterfaceClass zope.interface.tests.test_ro.B>
+ - <InterfaceClass zope.interface.tests.test_ro.E>
+ <InterfaceClass zope.interface.tests.test_ro.C> <InterfaceClass zope.interface.tests.test_ro.C>
+ <InterfaceClass zope.interface.tests.test_ro.D> <InterfaceClass zope.interface.tests.test_ro.D>
+ + <InterfaceClass zope.interface.tests.test_ro.E>
+ <InterfaceClass zope.interface.tests.test_ro.F> <InterfaceClass zope.interface.tests.test_ro.F>
+ <InterfaceClass zope.interface.Interface> <InterfaceClass zope.interface.Interface>""")
+
+ def test_ExtendedPathIndex_implement_thing_implementedby_super(self):
+ # See https://github.com/zopefoundation/zope.interface/pull/182#issuecomment-598754056
+ from zope.interface import ro
+ # pylint:disable=inherit-non-class
+ class _Based(object):
+ __bases__ = ()
+
+ def __init__(self, name, bases=(), attrs=None):
+ self.__name__ = name
+ self.__bases__ = bases
+
+ def __repr__(self):
+ return self.__name__
+
+ Interface = _Based('Interface', (), {})
+
+ class IPluggableIndex(Interface):
+ pass
+
+ class ILimitedResultIndex(IPluggableIndex):
+ pass
+
+ class IQueryIndex(IPluggableIndex):
+ pass
+
+ class IPathIndex(Interface):
+ pass
+
+ # A parent class who implements two distinct interfaces whose
+ # only common ancestor is Interface. An easy case.
+ # @implementer(IPathIndex, IQueryIndex)
+ # class PathIndex(object):
+ # pass
+ obj = _Based('object')
+ PathIndex = _Based('PathIndex', (IPathIndex, IQueryIndex, obj))
+
+ # Child class that tries to put an interface the parent declares
+ # later ahead of the parent.
+ # @implementer(ILimitedResultIndex, IQueryIndex)
+ # class ExtendedPathIndex(PathIndex):
+ # pass
+ ExtendedPathIndex = _Based('ExtendedPathIndex',
+ (ILimitedResultIndex, IQueryIndex, PathIndex))
+
+ # We were able to resolve it, and in exactly the same way as
+ # the legacy RO did, even though it is inconsistent.
+ result = self._callFUT(ExtendedPathIndex, log_changed_ro=True)
+ self.assertEqual(result, [
+ ExtendedPathIndex,
+ ILimitedResultIndex,
+ PathIndex,
+ IPathIndex,
+ IQueryIndex,
+ IPluggableIndex,
+ Interface,
+ obj])
+
+ record, = self.log_handler.records
+ self.assertIn('used the legacy', record.getMessage())
+
+ with self.assertRaises(ro.InconsistentResolutionOrderError):
+ self._callFUT(ExtendedPathIndex, strict=True)
+
+ def test_OSError_IOError(self):
+ if OSError is not IOError:
+ # Python 2
+ self.skipTest("Requires Python 3 IOError == OSError")
+ from zope.interface.common import interfaces
+ from zope.interface import providedBy
+
+ self.assertEqual(
+ list(providedBy(OSError()).flattened()),
+ [
+ interfaces.IOSError,
+ interfaces.IIOError,
+ interfaces.IEnvironmentError,
+ interfaces.IStandardError,
+ interfaces.IException,
+ interfaces.Interface,
+ ])
+
+ def test_non_orderable(self):
+ import warnings
+ from zope.interface import ro
+ try:
+ # If we've already warned, we must reset that state.
+ del ro.__warningregistry__
+ except AttributeError:
+ pass
+
+ with warnings.catch_warnings():
+ warnings.simplefilter('error')
+ orig_val = ro.C3.WARN_BAD_IRO
+ ro.C3.WARN_BAD_IRO = True
+ try:
+ with self.assertRaises(ro.InconsistentResolutionOrderWarning):
+ super(Test_c3_ro, self).test_non_orderable()
+ finally:
+ ro.C3.WARN_BAD_IRO = orig_val
+
+ IOErr, _ = self._make_IOErr()
+ with self.assertRaises(ro.InconsistentResolutionOrderError):
+ self._callFUT(IOErr, strict=True)
+
+ old_val = ro.C3.TRACK_BAD_IRO
+ try:
+ ro.C3.TRACK_BAD_IRO = True
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore')
+ self._callFUT(IOErr)
+ self.assertIn(IOErr, ro.C3.BAD_IROS)
+ finally:
+ ro.C3.TRACK_BAD_IRO = old_val
+
+ iro = self._callFUT(IOErr)
+ legacy_iro = self._callFUT(IOErr, use_legacy_ro=True)
+ self.assertEqual(iro, legacy_iro)
+
+
+class Test_ROComparison(unittest.TestCase):
+
+ class MockC3(object):
+ direct_inconsistency = False
+ bases_had_inconsistency = False
+
+ def _makeOne(self, c3=None, c3_ro=(), legacy_ro=()):
+ from zope.interface.ro import _ROComparison
+ return _ROComparison(c3 or self.MockC3(), c3_ro, legacy_ro)
+
+ def test_inconsistent_label(self):
+ comp = self._makeOne()
+ self.assertEqual('no', comp._inconsistent_label)
+
+ comp.c3.direct_inconsistency = True
+ self.assertEqual("direct", comp._inconsistent_label)
+
+ comp.c3.bases_had_inconsistency = True
+ self.assertEqual("direct+bases", comp._inconsistent_label)
+
+ comp.c3.direct_inconsistency = False
+ self.assertEqual('bases', comp._inconsistent_label)