diff options
Diffstat (limited to 'src/zope/interface')
| -rw-r--r-- | src/zope/interface/exceptions.py | 92 | ||||
| -rw-r--r-- | src/zope/interface/tests/test_exceptions.py | 54 | ||||
| -rw-r--r-- | src/zope/interface/tests/test_verify.py | 33 | ||||
| -rw-r--r-- | src/zope/interface/verify.py | 153 |
4 files changed, 247 insertions, 85 deletions
diff --git a/src/zope/interface/exceptions.py b/src/zope/interface/exceptions.py index 8d21f33..86caf3f 100644 --- a/src/zope/interface/exceptions.py +++ b/src/zope/interface/exceptions.py @@ -20,6 +20,7 @@ __all__ = [ 'DoesNotImplement', 'BrokenImplementation', 'BrokenMethodImplementation', + 'MultipleInvalid', # Other 'BadImplements', 'InvalidInterface', @@ -29,18 +30,37 @@ class Invalid(Exception): """A specification is violated """ -_NotGiven = object() +_NotGiven = '<Not Given>' class _TargetMixin(object): target = _NotGiven + interface = None @property - def _prefix(self): + def _target_prefix(self): if self.target is _NotGiven: return "An object" return "The object %r" % (self.target,) -class DoesNotImplement(Invalid, _TargetMixin): + _trailer = '.' + + @property + def _general_description(self): + return "has failed to implement interface %s:" % ( + self.interface + ) if self.interface is not None else '' + + + def __str__(self): + return "%s %s%s%s" % ( + self._target_prefix, + self._general_description, + self._specifics, + self._trailer + ) + + +class DoesNotImplement(_TargetMixin, Invalid): """ The *target* (optional) does not implement the *interface*. @@ -50,17 +70,17 @@ class DoesNotImplement(Invalid, _TargetMixin): """ def __init__(self, interface, target=_NotGiven): - Invalid.__init__(self) + Invalid.__init__(self, interface, target) self.interface = interface self.target = target - def __str__(self): - return "%s does not implement the interface %s." % ( - self._prefix, - self.interface - ) + _general_description = "does not implement the interface" -class BrokenImplementation(Invalid, _TargetMixin): + @property + def _specifics(self): + return ' ' + str(self.interface) + +class BrokenImplementation(_TargetMixin, Invalid): """ The *target* (optional) is missing the attribute *name*. @@ -72,19 +92,19 @@ class BrokenImplementation(Invalid, _TargetMixin): """ def __init__(self, interface, name, target=_NotGiven): - Invalid.__init__(self) + Invalid.__init__(self, interface, name, target) self.interface = interface self.name = name self.target = target - def __str__(self): - return "%s has failed to implement interface %s: The %s attribute was not provided." % ( - self._prefix, - self.interface, + + @property + def _specifics(self): + return " The %s attribute was not provided" % ( repr(self.name) if isinstance(self.name, str) else self.name ) -class BrokenMethodImplementation(Invalid, _TargetMixin): +class BrokenMethodImplementation(_TargetMixin, Invalid): """ The *target* (optional) has a *method* that violates its contract in a way described by *mess*. @@ -97,19 +117,49 @@ class BrokenMethodImplementation(Invalid, _TargetMixin): """ def __init__(self, method, mess, target=_NotGiven): - Invalid.__init__(self) + Invalid.__init__(self, method, mess, target) self.method = method self.mess = mess self.target = target - def __str__(self): - return "%s violates its contract in %s: %s." % ( - self._prefix, + @property + def _specifics(self): + return 'violates the contract of %s because %s' % ( repr(self.method) if isinstance(self.method, str) else self.method, - self.mess + self.mess, ) +class MultipleInvalid(_TargetMixin, Invalid): + """ + The *target* has failed to implement the *iface* in + multiple ways. + + The failures are described by *exceptions*, a collection of + other `Invalid` instances. + + .. versionadded:: 5.0 + """ + + def __init__(self, iface, target, exceptions): + exceptions = list(exceptions) + Invalid.__init__(self, iface, target, exceptions) + self.target = target + self.interface = iface + self.exceptions = exceptions + + @property + def _specifics(self): + # It would be nice to use tabs here, but that + # is hard to represent in doctests. + return '\n ' + '\n '.join( + x._specifics.strip() if isinstance(x, _TargetMixin) else(str(x)) + for x in self.exceptions + ) + + _trailer = '' + + class InvalidInterface(Exception): """The interface has invalid contents """ diff --git a/src/zope/interface/tests/test_exceptions.py b/src/zope/interface/tests/test_exceptions.py index 34ee071..42ea7eb 100644 --- a/src/zope/interface/tests/test_exceptions.py +++ b/src/zope/interface/tests/test_exceptions.py @@ -85,10 +85,60 @@ class BrokenMethodImplementationTests(unittest.TestCase): dni = self._makeOne() self.assertEqual( str(dni), - "An object violates its contract in 'aMethod': I said so.") + "An object violates the contract of 'aMethod' because I said so.") def test___str__w_candidate(self): dni = self._makeOne('candidate') self.assertEqual( str(dni), - "The object 'candidate' violates its contract in 'aMethod': I said so.") + "The object 'candidate' violates the contract of 'aMethod' because I said so.") + + def test___repr__w_candidate(self): + dni = self._makeOne('candidate') + self.assertEqual( + repr(dni), + "BrokenMethodImplementation('aMethod', 'I said so', 'candidate')" + ) + + +class MultipleInvalidTests(unittest.TestCase): + + def _getTargetClass(self): + from zope.interface.exceptions import MultipleInvalid + return MultipleInvalid + + def _makeOne(self, excs): + iface = _makeIface() + return self._getTargetClass()(iface, 'target', excs) + + def test__str__(self): + from zope.interface.exceptions import BrokenMethodImplementation + excs = [ + BrokenMethodImplementation('aMethod', 'I said so'), + Exception("Regular exception") + ] + dni = self._makeOne(excs) + self.assertEqual( + str(dni), + "The object 'target' has failed to implement interface " + "<InterfaceClass zope.interface.tests.test_exceptions.IDummy>:\n" + " violates the contract of 'aMethod' because I said so\n" + " Regular exception" + ) + + def test__repr__(self): + from zope.interface.exceptions import BrokenMethodImplementation + excs = [ + BrokenMethodImplementation('aMethod', 'I said so'), + # Use multiple arguments to normalize repr; versions of Python + # prior to 3.7 add a trailing comma if there's just one. + Exception("Regular", "exception") + ] + dni = self._makeOne(excs) + self.assertEqual( + repr(dni), + "MultipleInvalid(<InterfaceClass zope.interface.tests.test_exceptions.IDummy>," + " 'target'," + " [BrokenMethodImplementation('aMethod', 'I said so', '<Not Given>')," + " Exception('Regular', 'exception')])" + ) diff --git a/src/zope/interface/tests/test_verify.py b/src/zope/interface/tests/test_verify.py index 65e390a..3a68d20 100644 --- a/src/zope/interface/tests/test_verify.py +++ b/src/zope/interface/tests/test_verify.py @@ -554,6 +554,39 @@ class Test_verifyClass(unittest.TestCase): self._callFUT(IReadSequence, tuple, tentative=True) + def test_multiple_invalid(self): + from zope.interface.exceptions import MultipleInvalid + from zope.interface.exceptions import DoesNotImplement + from zope.interface.exceptions import BrokenImplementation + from zope.interface import Interface + from zope.interface import classImplements + + class ISeveralMethods(Interface): + def meth1(arg1): + "Method 1" + def meth2(arg1): + "Method 2" + + class SeveralMethods(object): + pass + + with self.assertRaises(MultipleInvalid) as exc: + self._callFUT(ISeveralMethods, SeveralMethods) + + ex = exc.exception + self.assertEqual(3, len(ex.exceptions)) + self.assertIsInstance(ex.exceptions[0], DoesNotImplement) + self.assertIsInstance(ex.exceptions[1], BrokenImplementation) + self.assertIsInstance(ex.exceptions[2], BrokenImplementation) + + # If everything else is correct, only the single error is raised without + # the wrapper. + classImplements(SeveralMethods, ISeveralMethods) + SeveralMethods.meth1 = lambda self, arg1: "Hi" + + with self.assertRaises(BrokenImplementation): + self._callFUT(ISeveralMethods, SeveralMethods) + class Test_verifyObject(Test_verifyClass): @classmethod diff --git a/src/zope/interface/verify.py b/src/zope/interface/verify.py index a8dd952..a9b80a7 100644 --- a/src/zope/interface/verify.py +++ b/src/zope/interface/verify.py @@ -21,8 +21,12 @@ from types import MethodType from zope.interface._compat import PYPY2 -from zope.interface.exceptions import BrokenImplementation, DoesNotImplement +from zope.interface.exceptions import BrokenImplementation from zope.interface.exceptions import BrokenMethodImplementation +from zope.interface.exceptions import DoesNotImplement +from zope.interface.exceptions import Invalid +from zope.interface.exceptions import MultipleInvalid + from zope.interface.interface import fromMethod, fromFunction, Method __all__ = [ @@ -55,8 +59,16 @@ def _verify(iface, candidate, tentative=False, vtype=None): - Making sure the candidate defines all the necessary attributes + :return bool: Returns a true value if everything that could be + checked passed. :raises zope.interface.Invalid: If any of the previous conditions does not hold. + + .. versionchanged:: 5.0 + If multiple methods or attributes are invalid, all such errors + are collected and reported. Previously, only the first error was reported. + As a special case, if only one such error is present, it is raised + alone, like before. """ if vtype == 'c': @@ -64,74 +76,91 @@ def _verify(iface, candidate, tentative=False, vtype=None): else: tester = iface.providedBy + excs = [] if not tentative and not tester(candidate): - raise DoesNotImplement(iface) + excs.append(DoesNotImplement(iface)) - # Here the `desc` is either an `Attribute` or `Method` instance for name, desc in iface.namesAndDescriptions(all=True): try: - attr = getattr(candidate, name) - except AttributeError: - if (not isinstance(desc, Method)) and vtype == 'c': - # We can't verify non-methods on classes, since the - # class may provide attrs in it's __init__. - continue - - raise BrokenImplementation(iface, desc, candidate) - - if not isinstance(desc, Method): - # If it's not a method, there's nothing else we can test - continue - - if inspect.ismethoddescriptor(attr) or inspect.isbuiltin(attr): - # The first case is what you get for things like ``dict.pop`` - # on CPython (e.g., ``verifyClass(IFullMapping, dict))``). The - # second case is what you get for things like ``dict().pop`` on - # CPython (e.g., ``verifyObject(IFullMapping, dict()))``. - # In neither case can we get a signature, so there's nothing - # to verify. Even the inspect module gives up and raises - # ValueError: no signature found. The ``__text_signature__`` attribute - # isn't typically populated either. - # - # Note that on PyPy 2 or 3 (up through 7.3 at least), these are - # not true for things like ``dict.pop`` (but might be true for C extensions?) - continue - - if isinstance(attr, FunctionType): - if sys.version_info[0] >= 3 and isinstance(candidate, type) and vtype == 'c': - # This is an "unbound method" in Python 3. - # Only unwrap this if we're verifying implementedBy; - # otherwise we can unwrap @staticmethod on classes that directly - # provide an interface. - meth = fromFunction(attr, iface, name=name, - imlevel=1) - else: - # Nope, just a normal function - meth = fromFunction(attr, iface, name=name) - elif (isinstance(attr, MethodTypes) - and type(attr.__func__) is FunctionType): - meth = fromMethod(attr, iface, name) - elif isinstance(attr, property) and vtype == 'c': - # We without an instance we cannot be sure it's not a - # callable. - continue - else: - if not callable(attr): - raise BrokenMethodImplementation(desc, "implementation is not a method", candidate) - # sigh, it's callable, but we don't know how to introspect it, so - # we have to give it a pass. - continue - - # Make sure that the required and implemented method signatures are - # the same. - mess = _incompat(desc.getSignatureInfo(), meth.getSignatureInfo()) - if mess: - if PYPY2 and _pypy2_false_positive(mess, candidate, vtype): - continue - raise BrokenMethodImplementation(desc, mess, candidate) + _verify_element(iface, name, desc, candidate, vtype) + except Invalid as e: + excs.append(e) + + if excs: + if len(excs) == 1: + raise excs[0] + raise MultipleInvalid(iface, candidate, excs) return True +def _verify_element(iface, name, desc, candidate, vtype): + # Here the `desc` is either an `Attribute` or `Method` instance + try: + attr = getattr(candidate, name) + except AttributeError: + if (not isinstance(desc, Method)) and vtype == 'c': + # We can't verify non-methods on classes, since the + # class may provide attrs in it's __init__. + return + + raise BrokenImplementation(iface, desc, candidate) + + if not isinstance(desc, Method): + # If it's not a method, there's nothing else we can test + return + + if inspect.ismethoddescriptor(attr) or inspect.isbuiltin(attr): + # The first case is what you get for things like ``dict.pop`` + # on CPython (e.g., ``verifyClass(IFullMapping, dict))``). The + # second case is what you get for things like ``dict().pop`` on + # CPython (e.g., ``verifyObject(IFullMapping, dict()))``. + # In neither case can we get a signature, so there's nothing + # to verify. Even the inspect module gives up and raises + # ValueError: no signature found. The ``__text_signature__`` attribute + # isn't typically populated either. + # + # Note that on PyPy 2 or 3 (up through 7.3 at least), these are + # not true for things like ``dict.pop`` (but might be true for C extensions?) + return + + if isinstance(attr, FunctionType): + if sys.version_info[0] >= 3 and isinstance(candidate, type) and vtype == 'c': + # This is an "unbound method" in Python 3. + # Only unwrap this if we're verifying implementedBy; + # otherwise we can unwrap @staticmethod on classes that directly + # provide an interface. + meth = fromFunction(attr, iface, name=name, + imlevel=1) + else: + # Nope, just a normal function + meth = fromFunction(attr, iface, name=name) + elif (isinstance(attr, MethodTypes) + and type(attr.__func__) is FunctionType): + meth = fromMethod(attr, iface, name) + elif isinstance(attr, property) and vtype == 'c': + # Without an instance we cannot be sure it's not a + # callable. + # TODO: This should probably check inspect.isdatadescriptor(), + # a more general form than ``property`` + return + + else: + if not callable(attr): + raise BrokenMethodImplementation(desc, "implementation is not a method", candidate) + # sigh, it's callable, but we don't know how to introspect it, so + # we have to give it a pass. + return + + # Make sure that the required and implemented method signatures are + # the same. + mess = _incompat(desc.getSignatureInfo(), meth.getSignatureInfo()) + if mess: + if PYPY2 and _pypy2_false_positive(mess, candidate, vtype): + return + raise BrokenMethodImplementation(desc, mess, candidate) + + + def verifyClass(iface, candidate, tentative=False): """ Verify that the *candidate* might correctly provide *iface*. |
