488 lines
14 KiB
Python
488 lines
14 KiB
Python
|
##############################################################################
|
||
|
#
|
||
|
# Copyright (c) 2014 Zope Foundation and Contributors.
|
||
|
# All Rights Reserved.
|
||
|
#
|
||
|
# This software is subject to the provisions of the Zope Public License,
|
||
|
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
|
||
|
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
|
||
|
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||
|
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
|
||
|
# FOR A PARTICULAR PURPOSE.
|
||
|
#
|
||
|
##############################################################################
|
||
|
"""Resolution ordering utility tests"""
|
||
|
import unittest
|
||
|
|
||
|
|
||
|
# pylint:disable=blacklisted-name
|
||
|
# pylint:disable=protected-access
|
||
|
# pylint:disable=attribute-defined-outside-init
|
||
|
|
||
|
class Test__mergeOrderings(unittest.TestCase):
|
||
|
|
||
|
def _callFUT(self, orderings):
|
||
|
from zope.interface.ro import _legacy_mergeOrderings
|
||
|
return _legacy_mergeOrderings(orderings)
|
||
|
|
||
|
def test_empty(self):
|
||
|
self.assertEqual(self._callFUT([]), [])
|
||
|
|
||
|
def test_single(self):
|
||
|
self.assertEqual(self._callFUT(['a', 'b', 'c']), ['a', 'b', 'c'])
|
||
|
|
||
|
def test_w_duplicates(self):
|
||
|
self.assertEqual(self._callFUT([['a'], ['b', 'a']]), ['b', 'a'])
|
||
|
|
||
|
def test_suffix_across_multiple_duplicates(self):
|
||
|
O1 = ['x', 'y', 'z']
|
||
|
O2 = ['q', 'z']
|
||
|
O3 = [1, 3, 5]
|
||
|
O4 = ['z']
|
||
|
self.assertEqual(self._callFUT([O1, O2, O3, O4]),
|
||
|
['x', 'y', 'q', 1, 3, 5, 'z'])
|
||
|
|
||
|
|
||
|
class Test__flatten(unittest.TestCase):
|
||
|
|
||
|
def _callFUT(self, ob):
|
||
|
from zope.interface.ro import _legacy_flatten
|
||
|
return _legacy_flatten(ob)
|
||
|
|
||
|
def test_w_empty_bases(self):
|
||
|
|
||
|
class Foo:
|
||
|
pass
|
||
|
|
||
|
foo = Foo()
|
||
|
foo.__bases__ = ()
|
||
|
self.assertEqual(self._callFUT(foo), [foo])
|
||
|
|
||
|
def test_w_single_base(self):
|
||
|
|
||
|
class Foo:
|
||
|
pass
|
||
|
|
||
|
self.assertEqual(self._callFUT(Foo), [Foo, object])
|
||
|
|
||
|
def test_w_bases(self):
|
||
|
|
||
|
class Foo:
|
||
|
pass
|
||
|
|
||
|
class Bar(Foo):
|
||
|
pass
|
||
|
|
||
|
self.assertEqual(self._callFUT(Bar), [Bar, Foo, object])
|
||
|
|
||
|
def test_w_diamond(self):
|
||
|
|
||
|
class Foo:
|
||
|
pass
|
||
|
|
||
|
class Bar(Foo):
|
||
|
pass
|
||
|
|
||
|
class Baz(Foo):
|
||
|
pass
|
||
|
|
||
|
class Qux(Bar, Baz):
|
||
|
pass
|
||
|
|
||
|
self.assertEqual(self._callFUT(Qux),
|
||
|
[Qux, Bar, Foo, object, Baz, Foo, object])
|
||
|
|
||
|
|
||
|
class Test_ro(unittest.TestCase):
|
||
|
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:
|
||
|
pass
|
||
|
|
||
|
foo = Foo()
|
||
|
foo.__bases__ = ()
|
||
|
self.assertEqual(self._callFUT(foo), [foo])
|
||
|
|
||
|
def test_w_single_base(self):
|
||
|
|
||
|
class Foo:
|
||
|
pass
|
||
|
|
||
|
self.assertEqual(self._callFUT(Foo), [Foo, object])
|
||
|
|
||
|
def test_w_bases(self):
|
||
|
|
||
|
class Foo:
|
||
|
pass
|
||
|
|
||
|
class Bar(Foo):
|
||
|
pass
|
||
|
|
||
|
self.assertEqual(self._callFUT(Bar), [Bar, Foo, object])
|
||
|
|
||
|
def test_w_diamond(self):
|
||
|
|
||
|
class Foo:
|
||
|
pass
|
||
|
|
||
|
class Bar(Foo):
|
||
|
pass
|
||
|
|
||
|
class Baz(Foo):
|
||
|
pass
|
||
|
|
||
|
class Qux(Bar, Baz):
|
||
|
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:
|
||
|
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 Interface
|
||
|
from zope.interface import implementedBy
|
||
|
from zope.interface import implementer
|
||
|
from zope.interface import providedBy
|
||
|
|
||
|
class IFoo(Interface):
|
||
|
pass
|
||
|
|
||
|
@implementer(IFoo)
|
||
|
class ImplementsFoo:
|
||
|
pass
|
||
|
|
||
|
class ExtendsFoo(ImplementsFoo):
|
||
|
pass
|
||
|
|
||
|
class ImplementsNothing:
|
||
|
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 C3Setting:
|
||
|
|
||
|
def __init__(self, setting, value):
|
||
|
self._setting = setting
|
||
|
self._value = value
|
||
|
|
||
|
def __enter__(self):
|
||
|
from zope.interface import ro
|
||
|
setattr(ro.C3, self._setting.__name__, self._value)
|
||
|
|
||
|
def __exit__(self, t, v, tb):
|
||
|
from zope.interface import ro
|
||
|
setattr(ro.C3, self._setting.__name__, self._setting)
|
||
|
|
||
|
|
||
|
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 _make_complex_diamond(self, base):
|
||
|
# https://github.com/zopefoundation/zope.interface/issues/21
|
||
|
|
||
|
class F(base):
|
||
|
pass
|
||
|
|
||
|
class E(base):
|
||
|
pass
|
||
|
|
||
|
class D(base):
|
||
|
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_object(self):
|
||
|
self._make_complex_diamond(object)
|
||
|
|
||
|
def test_complex_diamond_interface(self):
|
||
|
from zope.interface import Interface
|
||
|
|
||
|
IA = self._make_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._make_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._make_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]
|
||
|
|
||
|
expected = """\
|
||
|
Object <InterfaceClass {name}> has different legacy and C3 MROs:
|
||
|
Legacy RO (len=7) C3 RO (len=7; inconsistent=no)
|
||
|
==================================================================
|
||
|
zope.interface.tests.test_ro.A zope.interface.tests.test_ro.A
|
||
|
zope.interface.tests.test_ro.B zope.interface.tests.test_ro.B
|
||
|
- zope.interface.tests.test_ro.E
|
||
|
zope.interface.tests.test_ro.C zope.interface.tests.test_ro.C
|
||
|
zope.interface.tests.test_ro.D zope.interface.tests.test_ro.D
|
||
|
+ zope.interface.tests.test_ro.E
|
||
|
zope.interface.tests.test_ro.F zope.interface.tests.test_ro.F
|
||
|
zope.interface.Interface zope.interface.Interface""".format(
|
||
|
name="zope.interface.tests.test_ro.A"
|
||
|
)
|
||
|
|
||
|
self.assertEqual(
|
||
|
'\n'.join(ln.rstrip() for ln in record.getMessage().splitlines()),
|
||
|
expected,
|
||
|
)
|
||
|
|
||
|
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:
|
||
|
__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, strict=False
|
||
|
)
|
||
|
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):
|
||
|
from zope.interface import providedBy
|
||
|
from zope.interface.common import interfaces
|
||
|
|
||
|
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')
|
||
|
with C3Setting(
|
||
|
ro.C3.WARN_BAD_IRO, True
|
||
|
), C3Setting(
|
||
|
ro.C3.STRICT_IRO, False
|
||
|
):
|
||
|
with self.assertRaises(ro.InconsistentResolutionOrderWarning):
|
||
|
super().test_non_orderable()
|
||
|
|
||
|
IOErr, _ = self._make_IOErr()
|
||
|
with self.assertRaises(ro.InconsistentResolutionOrderError):
|
||
|
self._callFUT(IOErr, strict=True)
|
||
|
|
||
|
with C3Setting(
|
||
|
ro.C3.TRACK_BAD_IRO, True
|
||
|
), C3Setting(
|
||
|
ro.C3.STRICT_IRO, False
|
||
|
):
|
||
|
with warnings.catch_warnings():
|
||
|
warnings.simplefilter('ignore')
|
||
|
self._callFUT(IOErr)
|
||
|
self.assertIn(IOErr, ro.C3.BAD_IROS)
|
||
|
|
||
|
iro = self._callFUT(IOErr, strict=False)
|
||
|
legacy_iro = self._callFUT(IOErr, use_legacy_ro=True, strict=False)
|
||
|
self.assertEqual(iro, legacy_iro)
|
||
|
|
||
|
|
||
|
class TestC3(unittest.TestCase):
|
||
|
def _makeOne(self, C, strict=False, base_mros=None):
|
||
|
from zope.interface.ro import C3
|
||
|
return C3.resolver(C, strict, base_mros)
|
||
|
|
||
|
def test_base_mros_given(self):
|
||
|
c3 = self._makeOne(
|
||
|
type(self),
|
||
|
base_mros={unittest.TestCase: unittest.TestCase.__mro__}
|
||
|
)
|
||
|
memo = c3.memo
|
||
|
self.assertIn(unittest.TestCase, memo)
|
||
|
# We used the StaticMRO class
|
||
|
self.assertIsNone(memo[unittest.TestCase].had_inconsistency)
|
||
|
|
||
|
def test_one_base_optimization(self):
|
||
|
c3 = self._makeOne(type(self))
|
||
|
# Even though we didn't call .mro() yet, the MRO has been
|
||
|
# computed.
|
||
|
self.assertIsNotNone(c3._C3__mro) # pylint:disable=no-member
|
||
|
c3._merge = None
|
||
|
self.assertEqual(c3.mro(), list(type(self).__mro__))
|
||
|
|
||
|
|
||
|
class Test_ROComparison(unittest.TestCase):
|
||
|
|
||
|
class MockC3:
|
||
|
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)
|