Skip to content

Commit

Permalink
Make verifyObject/Class collect and raise all errors instead of only …
Browse files Browse the repository at this point in the history
…the first.

Fixes #171.
  • Loading branch information
jamadden committed Feb 8, 2020
1 parent d6343ee commit f6d2e94
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 108 deletions.
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
==================

Expand Down
64 changes: 41 additions & 23 deletions docs/verify.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ 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
>>> from zope.interface import Invalid
>>> class IFoo(Interface):
... x = Attribute("The X attribute")
... y = Attribute("The Y attribute")
Expand All @@ -47,7 +47,7 @@ exception. (We'll define a helper to make this easier to show.)
... foo = Foo()
... try:
... return verifyObject(IFoo, foo)
... except BrokenImplementation as e:
... except Invalid as e:
... print(e)

>>> @implementer(IFoo)
Expand All @@ -62,6 +62,18 @@ exception. (We'll define a helper to make this easier to show.)
>>> verify_foo()
The object <Foo...> has failed to implement interface <InterfaceClass ...IFoo>: The IFoo.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 <InterfaceClass ...IFoo>:
The IFoo.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:
Expand Down Expand Up @@ -124,34 +136,23 @@ that takes one argument. If we don't provide it, we get an error.
>>> 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::

>>> 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.

.. doctest::

>>> Foo.simple = 42
>>> verify_foo()
The object <Foo...> violates its contract in IFoo.simple(arg1): implementation is not a method.
The object <Foo...> violates the contract of IFoo.simple(arg1) because implementation is not a method.

Taking too few arguments is an error.

.. doctest::

>>> Foo.simple = lambda: "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...> violates the contract of IFoo.simple(arg1) because implementation doesn't allow enough arguments.

Requiring too many arguments is an error. (Recall that the ``self``
argument is implicit.)
Expand All @@ -160,7 +161,7 @@ argument is implicit.)

>>> 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...> violates the contract of IFoo.simple(arg1) because implementation requires too many arguments.

Variable arguments can be used to implement the required number, as
can arguments with defaults.
Expand All @@ -185,23 +186,40 @@ 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...> violates the contract of IFoo.needs_kwargs(**kwargs) because implementation doesn't support keyword arguments.

>>> class IFoo(Interface):
... def needs_varargs(*args): pass
>>> @implementer(IFoo)
... 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...> violates the contract of IFoo.needs_varargs(*args) because implementation doesn't support variable arguments.


Of course, missing attributes are also found and reported.

.. doctest::

>>> class IFoo(Interface):
... x = Attribute('The X attribute')
... def method(arg1): "Takes one positional argument"
>>> @implementer(IFoo)
... class Foo(object):
... def method(self): "I don't have enough arguments"
>>> verify_foo()
The object <Foo...> has failed to implement interface <InterfaceClass ...IFoo>:
The IFoo.x attribute was not provided
violates the contract of IFoo.method(arg1) because implementation doesn't allow enough arguments

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

Expand All @@ -211,8 +229,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'> violates the contract of IFoo.method(arg1) because implementation doesn't allow enough arguments.
92 changes: 71 additions & 21 deletions src/zope/interface/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
'DoesNotImplement',
'BrokenImplementation',
'BrokenMethodImplementation',
'MultipleInvalid',
# Other
'BadImplements',
'InvalidInterface',
Expand All @@ -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*.
Expand All @@ -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*.
Expand All @@ -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*.
Expand All @@ -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
"""
Expand Down
54 changes: 52 additions & 2 deletions src/zope/interface/tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,60 @@ def test___str__(self):
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')])"
)
Loading

0 comments on commit f6d2e94

Please sign in to comment.