summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2020-02-10 09:46:04 -0600
committerGitHub <noreply@github.com>2020-02-10 09:46:04 -0600
commit447121d0e41e9785251a1e7b1c7f162e4f7f00fd (patch)
treed078bec878c932d2f7b1c2e87c2b5e0113be5193
parentd6343eeaa7f67838db9de82974898b80938a7aef (diff)
parent39229790671a3f85486f26eecfa60fa824755db4 (diff)
downloadzope-interface-447121d0e41e9785251a1e7b1c7f162e4f7f00fd.tar.gz
Merge pull request #174 from zopefoundation/issue171
Make verifyObject/Class collect and raise all errors instead of only the first
-rw-r--r--.travis.yml15
-rw-r--r--CHANGES.rst8
-rw-r--r--docs/verify.rst172
-rw-r--r--src/zope/interface/exceptions.py231
-rw-r--r--src/zope/interface/tests/test_exceptions.py108
-rw-r--r--src/zope/interface/tests/test_verify.py33
-rw-r--r--src/zope/interface/verify.py154
7 files changed, 562 insertions, 159 deletions
diff --git a/.travis.yml b/.travis.yml
index dd4c3e0..8b5bd09 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -82,17 +82,22 @@ before_install:
fi
install:
- - pip install -U pip setuptools
- - pip install -U coveralls coverage
- - pip install -U -e ".[test]"
+ # virtualenv 20.0 together with terryfy macOS is broken, at least
+ # with CPython 2.7.17 (https://github.com/pypa/virtualenv/issues/1555):
+ # it doesn't install or activate the virtualenv correctly and PATH
+ # isn't setup right. So try to use `python -m` to workaround that.
+ - python -m pip install -U pip setuptools
+ - python -m pip install -U coveralls coverage
+ - python -m pip install -U -e ".[test]"
script:
+ - which python
- python --version
- - coverage run -m unittest discover -s src
+ - python -m coverage run -m unittest discover -s src
- python setup.py -q bdist_wheel
after_success:
- - coveralls
+ - python -m coveralls
- |
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
# macpython 3.5 doesn't support recent TLS protocols which causes twine
diff --git a/CHANGES.rst b/CHANGES.rst
index b484a99..b21f54d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -108,6 +108,14 @@
.. caution:: This will break consumers (such as doctests) that
depended on the exact error messages.
+- Make ``verifyObject`` and ``verifyClass`` report all errors, if the
+ candidate object has multiple detectable violations. Previously they
+ reported only the first error. See `issue
+ <https://github.com/zopefoundation/zope.interface/issues/171>`_.
+
+ Like the above, this will break consumers depending on the exact
+ output of error messages if more than one error is present.
+
4.7.1 (2019-11-11)
==================
diff --git a/docs/verify.rst b/docs/verify.rst
index 6f48218..06a0a0f 100644
--- a/docs/verify.rst
+++ b/docs/verify.rst
@@ -14,6 +14,82 @@ Verifying objects
.. autoexception:: zope.interface.Invalid
+Let's demonstrate. We'll begin by defining a simple interface hierarchy
+requiring two attributes, and a helper method that will instantiate and verify
+that an object provides this interface.
+
+.. doctest::
+
+ >>> from zope.interface import Interface, Attribute, implementer
+ >>> from zope.interface import Invalid
+ >>> from zope.interface.verify import verifyObject
+ >>> class IBase(Interface):
+ ... x = Attribute("The X attribute")
+ >>> class IFoo(IBase):
+ ... y = Attribute("The Y attribute")
+ >>> class Foo(object):
+ ... pass
+ >>> def verify_foo(**kwargs):
+ ... foo = Foo()
+ ... try:
+ ... return verifyObject(IFoo, foo, **kwargs)
+ ... except Invalid as e:
+ ... print(e)
+
+If we try to verify an instance of this ``Foo`` class, three errors
+will be reported. The declarations (does the object provide ``IFoo``)
+are checked, as are the attributes specified in the interface being
+validated (and its ancestors). Notice that the interface being
+verified is shown, as is the interface where the attribute was
+defined.
+
+.. doctest::
+
+ >>> 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
+
+If we add the two missing attributes, we still have the error about not
+declaring the correct interface.
+
+.. doctest::
+
+ >>> Foo.x = Foo.y = 42
+ >>> verify_foo()
+ The object <Foo...> has failed to implement interface <...IFoo>: Does not declaratively implement the interface.
+
+If we want to only check the structure of the object, without examining
+its declarations, we can use the ``tentative`` argument.
+
+.. doctest::
+
+ >>> verify_foo(tentative=True)
+ True
+
+Of course, we can always mark a particular instance as providing the
+desired interface.
+
+.. doctest::
+
+ >>> from zope.interface import alsoProvides
+ >>> foo = Foo()
+ >>> alsoProvides(foo, IFoo)
+ >>> verifyObject(IFoo, foo)
+ True
+
+If all instances will provide the interface, we can
+mark the class as implementing it.
+
+.. doctest::
+
+ >>> from zope.interface import classImplements
+ >>> classImplements(Foo, IFoo)
+ >>> verify_foo()
+ True
+
+
Testing for attributes
----------------------
@@ -22,46 +98,44 @@ Attributes of the object, be they defined by its class or added by its
.. doctest::
- >>> from zope.interface import Interface, Attribute, implementer
- >>> from zope.interface.exceptions import BrokenImplementation
- >>> class IFoo(Interface):
- ... x = Attribute("The X attribute")
- ... y = Attribute("The Y attribute")
-
>>> @implementer(IFoo)
... class Foo(object):
... x = 1
... def __init__(self):
... self.y = 2
- >>> from zope.interface.verify import verifyObject
>>> verifyObject(IFoo, Foo())
True
If either attribute is missing, verification will fail by raising an
-exception. (We'll define a helper to make this easier to show.)
+exception.
.. doctest::
- >>> def verify_foo():
- ... foo = Foo()
- ... try:
- ... return verifyObject(IFoo, foo)
- ... except BrokenImplementation as e:
- ... print(e)
-
>>> @implementer(IFoo)
... class Foo(object):
... x = 1
>>> verify_foo()
- The object <Foo...> has failed to implement interface <InterfaceClass ...IFoo>: The IFoo.y attribute was not provided.
+ The object <Foo...> has failed to implement interface <...IFoo>: The 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 <InterfaceClass ...IFoo>: The IFoo.x attribute was not provided.
+ The object <Foo...> has failed to implement interface <...IFoo>: The IBase.x attribute was not provided.
+If both attributes are missing, an exception is raised reporting
+both errors.
+
+.. doctest::
+
+ >>> @implementer(IFoo)
+ ... class Foo(object):
+ ... 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
If an attribute is implemented as a property that raises an ``AttributeError``
when trying to get its value, the attribute is considered missing:
@@ -76,7 +150,7 @@ when trying to get its value, the attribute is considered missing:
... def x(self):
... raise AttributeError
>>> verify_foo()
- The object <Foo...> has failed to implement interface <InterfaceClass ...IFoo>: The IFoo.x attribute was not provided.
+ The object <Foo...> has failed to implement interface <...IFoo>: The IFoo.x attribute was not provided.
Any other exception raised by a property will propagate to the caller of
@@ -122,20 +196,9 @@ that takes one argument. If we don't provide it, we get an error.
... class Foo(object):
... pass
>>> verify_foo()
- The object <Foo...> has failed to implement interface <InterfaceClass builtins.IFoo>: The IFoo.simple(arg1) attribute was not provided.
-
-Once they exist, they are checked for compatible signatures. This is a
-different type of exception, so we need an updated helper.
-
-.. doctest::
+ The object <Foo...> has failed to implement interface <...IFoo>: The IFoo.simple(arg1) attribute was not provided.
- >>> from zope.interface.exceptions import BrokenMethodImplementation
- >>> def verify_foo():
- ... foo = Foo()
- ... try:
- ... return verifyObject(IFoo, foo)
- ... except BrokenMethodImplementation as e:
- ... print(e)
+Once they exist, they are checked to be callable, and for compatible signatures.
Not being callable is an error.
@@ -143,24 +206,24 @@ Not being callable is an error.
>>> Foo.simple = 42
>>> verify_foo()
- The object <Foo...> violates its contract in IFoo.simple(arg1): implementation is not a method.
+ The object <Foo...> has failed to implement interface <...IFoo>: The contract of IFoo.simple(arg1) is violated because '42' is not a method.
-Taking too few arguments is an error.
+Taking too few arguments is an error. (Recall that the ``self``
+argument is implicit.)
.. doctest::
- >>> Foo.simple = lambda: "I take no arguments"
+ >>> Foo.simple = lambda self: "I take no arguments"
>>> verify_foo()
- The object <Foo...> violates its contract in IFoo.simple(arg1): implementation doesn't allow enough arguments.
+ The object <Foo...> has failed to implement interface <...IFoo>: The contract of IFoo.simple(arg1) is violated because '<lambda>()' doesn't allow enough arguments.
-Requiring too many arguments is an error. (Recall that the ``self``
-argument is implicit.)
+Requiring too many arguments is an error.
.. doctest::
>>> Foo.simple = lambda self, a, b: "I require two arguments"
>>> verify_foo()
- The object <Foo...> violates its contract in IFoo.simple(arg1): implementation requires too many arguments.
+ 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.
Variable arguments can be used to implement the required number, as
can arguments with defaults.
@@ -185,7 +248,7 @@ variable keyword arguments, the implementation must also accept them.
... class Foo(object):
... def needs_kwargs(self, a=1, b=2): pass
>>> verify_foo()
- The object <Foo...> violates its contract in IFoo.needs_kwargs(**kwargs): implementation doesn't support keyword arguments.
+ 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.
>>> class IFoo(Interface):
... def needs_varargs(*args): pass
@@ -193,15 +256,36 @@ variable keyword arguments, the implementation must also accept them.
... class Foo(object):
... def needs_varargs(self, **kwargs): pass
>>> verify_foo()
- The object <Foo...> violates its contract in IFoo.needs_varargs(*args): implementation doesn't support variable arguments.
+ 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.
+
+Of course, missing attributes are also found and reported, and the
+source interface of the missing attribute is included. Similarly, when
+the failing method is from a parent class, that is also reported.
+
+.. doctest::
+
+ >>> class IBase(Interface):
+ ... def method(arg1): "Takes one positional argument"
+ >>> class IFoo(IBase):
+ ... x = Attribute('The X attribute')
+ >>> class Base(object):
+ ... def method(self): "I don't have enough arguments"
+ >>> @implementer(IFoo)
+ ... class Foo(Base):
+ ... 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
Verifying Classes
=================
The function `verifyClass` is used to check that a class implements
an interface properly, meaning that its instances properly provide the
-interface. Most of the same things that `verifyObject` checks can be
-checked for classes.
+interface. Many of the same things that `verifyObject` checks can be
+checked for classes, but certain conditions, such as the presence of
+attributes, cannot be verified.
.. autofunction:: verifyClass
@@ -211,8 +295,8 @@ checked for classes.
>>> def verify_foo_class():
... try:
... return verifyClass(IFoo, Foo)
- ... except BrokenMethodImplementation as e:
+ ... except Invalid as e:
... print(e)
>>> verify_foo_class()
- The object <class 'Foo'> violates its contract in IFoo.needs_varargs(*args): implementation doesn't support variable arguments.
+ 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.
diff --git a/src/zope/interface/exceptions.py b/src/zope/interface/exceptions.py
index 8d21f33..2f3758b 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,19 +30,94 @@ class Invalid(Exception):
"""A specification is violated
"""
-_NotGiven = object()
-class _TargetMixin(object):
- target = _NotGiven
+class _TargetInvalid(Invalid):
+ # Internal use. Subclass this when you're describing
+ # a particular target object that's invalid according
+ # to a specific interface.
+ #
+ # For backwards compatibility, the *target* and *interface* are
+ # optional, and the signatures are inconsistent in their ordering.
+ #
+ # We deal with the inconsistency in ordering by defining the index
+ # of the two values in ``self.args``. *target* uses a marker object to
+ # distinguish "not given" from "given, but None", because the latter
+ # can be a value that gets passed to validation. For this reason, it must
+ # always be the last argument (we detect absense by the ``IndexError``).
+
+ _IX_INTERFACE = 0
+ _IX_TARGET = 1
+ # The exception to catch when indexing self.args indicating that
+ # an argument was not given. If all arguments are expected,
+ # a subclass should set this to ().
+ _NOT_GIVEN_CATCH = IndexError
+ _NOT_GIVEN = '<Not Given>'
+
+ def _get_arg_or_default(self, ix, default=None):
+ try:
+ return self.args[ix] # pylint:disable=unsubscriptable-object
+ except self._NOT_GIVEN_CATCH:
+ return default
+
+ @property
+ def interface(self):
+ return self._get_arg_or_default(self._IX_INTERFACE)
+
+ @property
+ def target(self):
+ return self._get_arg_or_default(self._IX_TARGET, self._NOT_GIVEN)
+
+ ###
+ # str
+ #
+ # The ``__str__`` of self is implemented by concatenating (%s), in order,
+ # these properties (none of which should have leading or trailing
+ # whitespace):
+ #
+ # - self._str_subject
+ # Begin the message, including a description of the target.
+ # - self._str_description
+ # Provide a general description of the type of error, including
+ # the interface name if possible and relevant.
+ # - self._str_conjunction
+ # Join the description to the details. Defaults to ": ".
+ # - self._str_details
+ # Provide details about how this particular instance of the error.
+ # - self._str_trailer
+ # End the message. Usually just a period.
+ ###
@property
- def _prefix(self):
- if self.target is _NotGiven:
+ def _str_subject(self):
+ target = self.target
+ if target is self._NOT_GIVEN:
return "An object"
- return "The object %r" % (self.target,)
+ return "The object %r" % (target,)
+
+ @property
+ def _str_description(self):
+ return "has failed to implement interface %s" % (
+ self.interface or '<Unknown>'
+ )
+
+ _str_conjunction = ": "
+ _str_details = "<unknown>"
+ _str_trailer = '.'
+
+ def __str__(self):
+ return "%s %s%s%s%s" % (
+ self._str_subject,
+ self._str_description,
+ self._str_conjunction,
+ self._str_details,
+ self._str_trailer
+ )
-class DoesNotImplement(Invalid, _TargetMixin):
+
+class DoesNotImplement(_TargetInvalid):
"""
+ DoesNotImplement(interface[, target])
+
The *target* (optional) does not implement the *interface*.
.. versionchanged:: 5.0.0
@@ -49,19 +125,13 @@ class DoesNotImplement(Invalid, _TargetMixin):
string value of this object accordingly.
"""
- def __init__(self, interface, target=_NotGiven):
- Invalid.__init__(self)
- self.interface = interface
- self.target = target
+ _str_details = "Does not declaratively implement the interface"
- def __str__(self):
- return "%s does not implement the interface %s." % (
- self._prefix,
- self.interface
- )
-class BrokenImplementation(Invalid, _TargetMixin):
+class BrokenImplementation(_TargetInvalid):
"""
+ BrokenImplementation(interface, name[, target])
+
The *target* (optional) is missing the attribute *name*.
.. versionchanged:: 5.0.0
@@ -71,45 +141,128 @@ class BrokenImplementation(Invalid, _TargetMixin):
The *name* can either be a simple string or a ``Attribute`` object.
"""
- def __init__(self, interface, name, target=_NotGiven):
- Invalid.__init__(self)
- self.interface = interface
- self.name = name
- self.target = target
+ _IX_NAME = _TargetInvalid._IX_INTERFACE + 1
+ _IX_TARGET = _IX_NAME + 1
- def __str__(self):
- return "%s has failed to implement interface %s: The %s attribute was not provided." % (
- self._prefix,
- self.interface,
+ @property
+ def name(self):
+ return self.args[1] # pylint:disable=unsubscriptable-object
+
+ @property
+ def _str_details(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(_TargetInvalid):
"""
- The *target* (optional) has a *method* that violates
+ BrokenMethodImplementation(method, message[, implementation, interface, target])
+
+ The *target* (optional) has a *method* in *implementation* that violates
its contract in a way described by *mess*.
.. versionchanged:: 5.0.0
- Add the *target* argument and attribute, and change the resulting
- string value of this object accordingly.
+ Add the *interface* and *target* argument and attribute,
+ and change the resulting string value of this object accordingly.
The *method* can either be a simple string or a ``Method`` object.
+
+ .. versionchanged:: 5.0.0
+ If *implementation* is given, then the *message* will have the
+ string "implementation" replaced with an short but informative
+ representation of *implementation*.
+
"""
- def __init__(self, method, mess, target=_NotGiven):
- Invalid.__init__(self)
- self.method = method
- self.mess = mess
- self.target = target
+ _IX_IMPL = 2
+ _IX_INTERFACE = _IX_IMPL + 1
+ _IX_TARGET = _IX_INTERFACE + 1
- def __str__(self):
- return "%s violates its contract in %s: %s." % (
- self._prefix,
+ @property
+ def method(self):
+ return self.args[0] # pylint:disable=unsubscriptable-object
+
+ @property
+ def mess(self):
+ return self.args[1] # pylint:disable=unsubscriptable-object
+
+ @staticmethod
+ def __implementation_str(impl):
+ # It could be a callable or some arbitrary object, we don't
+ # know yet.
+ import inspect # Inspect is a heavy-weight dependency, lots of imports
+ try:
+ sig = inspect.signature
+ formatsig = str
+ except AttributeError:
+ sig = inspect.getargspec
+ f = inspect.formatargspec
+ formatsig = lambda sig: f(*sig) # pylint:disable=deprecated-method
+
+ try:
+ sig = sig(impl)
+ except (ValueError, TypeError):
+ # Unable to introspect. Darn.
+ # This could be a non-callable, or a particular builtin,
+ # or a bound method that doesn't even accept 'self', e.g.,
+ # ``Class.method = lambda: None; Class().method``
+ return repr(impl)
+
+ try:
+ name = impl.__qualname__
+ except AttributeError:
+ name = impl.__name__
+
+ return name + formatsig(sig)
+
+ @property
+ def _str_details(self):
+ impl = self._get_arg_or_default(self._IX_IMPL, self._NOT_GIVEN)
+ message = self.mess
+ if impl is not self._NOT_GIVEN and 'implementation' in message:
+ message = message.replace("implementation", '%r')
+ message = message % (self.__implementation_str(impl),)
+
+ return 'The contract of %s is violated because %s' % (
repr(self.method) if isinstance(self.method, str) else self.method,
- self.mess
+ message,
)
+class MultipleInvalid(_TargetInvalid):
+ """
+ The *target* has failed to implement the *interface* in
+ multiple ways.
+
+ The failures are described by *exceptions*, a collection of
+ other `Invalid` instances.
+
+ .. versionadded:: 5.0
+ """
+
+ _NOT_GIVEN_CATCH = ()
+
+ def __init__(self, interface, target, exceptions):
+ super(MultipleInvalid, self).__init__(interface, target, tuple(exceptions))
+
+ @property
+ def exceptions(self):
+ return self.args[2] # pylint:disable=unsubscriptable-object
+
+ @property
+ def _str_details(self):
+ # It would be nice to use tabs here, but that
+ # is hard to represent in doctests.
+ return '\n ' + '\n '.join(
+ x._str_details.strip() if isinstance(x, _TargetInvalid) else str(x)
+ for x in self.exceptions
+ )
+
+ _str_conjunction = ':' # We don't want a trailing space, messes up doctests
+ _str_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..a55f522 100644
--- a/src/zope/interface/tests/test_exceptions.py
+++ b/src/zope/interface/tests/test_exceptions.py
@@ -35,15 +35,19 @@ class DoesNotImplementTests(unittest.TestCase):
dni = self._makeOne()
self.assertEqual(
str(dni),
- 'An object does not implement the interface '
- '<InterfaceClass zope.interface.tests.test_exceptions.IDummy>.')
+ "An object has failed to implement interface "
+ "<InterfaceClass zope.interface.tests.test_exceptions.IDummy>: "
+ "Does not declaratively implement the interface."
+ )
def test___str__w_candidate(self):
dni = self._makeOne('candidate')
self.assertEqual(
str(dni),
- 'The object \'candidate\' does not implement the interface '
- '<InterfaceClass zope.interface.tests.test_exceptions.IDummy>.')
+ "The object 'candidate' has failed to implement interface "
+ "<InterfaceClass zope.interface.tests.test_exceptions.IDummy>: "
+ "Does not declaratively implement the interface."
+ )
class BrokenImplementationTests(unittest.TestCase):
@@ -72,23 +76,109 @@ class BrokenImplementationTests(unittest.TestCase):
'<InterfaceClass zope.interface.tests.test_exceptions.IDummy>: '
"The 'missing' attribute was not provided.")
+
+def broken_function():
+ """
+ This is a global function with a simple argument list.
+
+ It exists to be able to report the same information when
+ formatting signatures under Python 2 and Python 3.
+ """
+
+
class BrokenMethodImplementationTests(unittest.TestCase):
def _getTargetClass(self):
from zope.interface.exceptions import BrokenMethodImplementation
return BrokenMethodImplementation
+ message = 'I said so'
+
def _makeOne(self, *args):
- return self._getTargetClass()('aMethod', 'I said so', *args)
+ return self._getTargetClass()('aMethod', self.message, *args)
def test___str__(self):
dni = self._makeOne()
self.assertEqual(
str(dni),
- "An object violates its contract in 'aMethod': I said so.")
+ "An object has failed to implement interface <Unknown>: "
+ "The contract of 'aMethod' is violated because I said so."
+ )
- def test___str__w_candidate(self):
- dni = self._makeOne('candidate')
+ def test___str__w_candidate_no_implementation(self):
+ dni = self._makeOne('some_function', '<IFoo>', 'candidate')
self.assertEqual(
str(dni),
- "The object 'candidate' violates its contract in 'aMethod': I said so.")
+ "The object 'candidate' has failed to implement interface <IFoo>: "
+ "The contract of 'aMethod' is violated because I said so."
+ )
+
+ def test___str__w_candidate_w_implementation(self):
+ self.message = 'implementation is wonky'
+ dni = self._makeOne(broken_function, '<IFoo>', 'candidate')
+ self.assertEqual(
+ str(dni),
+ "The object 'candidate' has failed to implement interface <IFoo>: "
+ "The contract of 'aMethod' is violated because "
+ "'broken_function()' is wonky."
+ )
+
+ def test___str__w_candidate_w_implementation_not_callable(self):
+ self.message = 'implementation is not callable'
+ dni = self._makeOne(42, '<IFoo>', 'candidate')
+ self.assertEqual(
+ str(dni),
+ "The object 'candidate' has failed to implement interface <IFoo>: "
+ "The contract of 'aMethod' is violated because "
+ "'42' is not callable."
+ )
+
+ def test___repr__w_candidate(self):
+ dni = self._makeOne(None, 'candidate')
+ self.assertEqual(
+ repr(dni),
+ "BrokenMethodImplementation('aMethod', 'I said so', None, '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"
+ " The contract of 'aMethod' is violated 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'),"
+ " 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..0a64aeb 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,92 @@ 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, candidate))
- # 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
+ # TODO: On Python 3, this should use ``raise...from``
+ 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",
+ attr, iface, 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, attr, iface, candidate)
+
+
+
def verifyClass(iface, candidate, tentative=False):
"""
Verify that the *candidate* might correctly provide *iface*.