Simplify the string formatting rules for the Invalid exceptions.

As per review.

Also include the actual failing implementation object in the BrokenMethodImplementation to make it easier to track down what's going on when inheritance is involved.
This commit is contained in:
Jason Madden 2020-02-10 08:20:12 -06:00
parent f6d2e9445d
commit e53e797787
No known key found for this signature in database
GPG Key ID: 349F84431A08B99E
4 changed files with 327 additions and 117 deletions

View File

@ -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,45 +98,31 @@ 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 import Invalid
>>> 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 Invalid 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.
@ -71,8 +133,8 @@ both errors.
... class Foo(object):
... pass
>>> 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
The IFoo.y attribute was not provided
If an attribute is implemented as a property that raises an ``AttributeError``
@ -88,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
@ -134,7 +196,7 @@ 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.
The object <Foo...> has failed to implement interface <...IFoo>: The IFoo.simple(arg1) attribute was not provided.
Once they exist, they are checked to be callable, and for compatible signatures.
@ -144,24 +206,24 @@ Not being callable is an error.
>>> Foo.simple = 42
>>> verify_foo()
The object <Foo...> violates the contract of IFoo.simple(arg1) because 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 the contract of IFoo.simple(arg1) because 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 the contract of IFoo.simple(arg1) because 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.
@ -186,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 the contract of IFoo.needs_kwargs(**kwargs) because 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
@ -194,23 +256,27 @@ variable keyword arguments, the implementation must also accept them.
... class Foo(object):
... def needs_varargs(self, **kwargs): pass
>>> verify_foo()
The object <Foo...> violates the contract of IFoo.needs_varargs(*args) because 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.
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 IFoo(Interface):
... x = Attribute('The X attribute')
>>> 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(object):
... def method(self): "I don't have enough arguments"
... class Foo(Base):
... pass
>>> verify_foo()
The object <Foo...> has failed to implement interface <InterfaceClass ...IFoo>:
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
violates the contract of IFoo.method(arg1) because implementation doesn't allow enough arguments
Verifying Classes
=================
@ -233,4 +299,4 @@ attributes, cannot be verified.
... print(e)
>>> verify_foo_class()
The object <class 'Foo'> violates the contract of IFoo.method(arg1) because implementation doesn't allow enough 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.

View File

@ -30,38 +30,94 @@ class Invalid(Exception):
"""A specification is violated
"""
_NotGiven = '<Not Given>'
class _TargetMixin(object):
target = _NotGiven
interface = None
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 _target_prefix(self):
if self.target is _NotGiven:
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 _str_subject(self):
target = self.target
if target is self._NOT_GIVEN:
return "An object"
return "The object %r" % (self.target,)
_trailer = '.'
return "The object %r" % (target,)
@property
def _general_description(self):
return "has failed to implement interface %s:" % (
self.interface
) if self.interface is not None else ''
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" % (
self._target_prefix,
self._general_description,
self._specifics,
self._trailer
return "%s %s%s%s%s" % (
self._str_subject,
self._str_description,
self._str_conjunction,
self._str_details,
self._str_trailer
)
class DoesNotImplement(_TargetMixin, Invalid):
class DoesNotImplement(_TargetInvalid):
"""
DoesNotImplement(interface[, target])
The *target* (optional) does not implement the *interface*.
.. versionchanged:: 5.0.0
@ -69,19 +125,13 @@ class DoesNotImplement(_TargetMixin, Invalid):
string value of this object accordingly.
"""
def __init__(self, interface, target=_NotGiven):
Invalid.__init__(self, interface, target)
self.interface = interface
self.target = target
_str_details = "Does not declaratively implement the interface"
_general_description = "does not implement the interface"
@property
def _specifics(self):
return ' ' + str(self.interface)
class BrokenImplementation(_TargetMixin, Invalid):
class BrokenImplementation(_TargetInvalid):
"""
BrokenImplementation(interface, name[, target])
The *target* (optional) is missing the attribute *name*.
.. versionchanged:: 5.0.0
@ -91,48 +141,98 @@ class BrokenImplementation(_TargetMixin, Invalid):
The *name* can either be a simple string or a ``Attribute`` object.
"""
def __init__(self, interface, name, target=_NotGiven):
Invalid.__init__(self, interface, name, target)
self.interface = interface
self.name = name
self.target = target
_IX_NAME = _TargetInvalid._IX_INTERFACE + 1
_IX_TARGET = _IX_NAME + 1
@property
def _specifics(self):
return " The %s attribute was not provided" % (
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(_TargetMixin, Invalid):
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, method, mess, target)
self.method = method
self.mess = mess
self.target = target
_IX_IMPL = 2
_IX_INTERFACE = _IX_IMPL + 1
_IX_TARGET = _IX_INTERFACE + 1
@property
def _specifics(self):
return 'violates the contract of %s because %s' % (
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(_TargetMixin, Invalid):
class MultipleInvalid(_TargetInvalid):
"""
The *target* has failed to implement the *iface* in
The *target* has failed to implement the *interface* in
multiple ways.
The failures are described by *exceptions*, a collection of
@ -141,23 +241,26 @@ class MultipleInvalid(_TargetMixin, Invalid):
.. 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
_NOT_GIVEN_CATCH = ()
def __init__(self, interface, target, exceptions):
super(MultipleInvalid, self).__init__(interface, target, tuple(exceptions))
@property
def _specifics(self):
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._specifics.strip() if isinstance(x, _TargetMixin) else(str(x))
x._str_details.strip() if isinstance(x, _TargetInvalid) else str(x)
for x in self.exceptions
)
_trailer = ''
_str_conjunction = ':' # We don't want a trailing space, messes up doctests
_str_trailer = ''
class InvalidInterface(Exception):

View File

@ -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,32 +76,68 @@ 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 the contract of 'aMethod' because 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 the contract of 'aMethod' because 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('candidate')
dni = self._makeOne(None, 'candidate')
self.assertEqual(
repr(dni),
"BrokenMethodImplementation('aMethod', 'I said so', 'candidate')"
"BrokenMethodImplementation('aMethod', 'I said so', None, 'candidate')"
)
@ -122,7 +162,7 @@ class MultipleInvalidTests(unittest.TestCase):
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"
" The contract of 'aMethod' is violated because I said so\n"
" Regular exception"
)
@ -139,6 +179,6 @@ class MultipleInvalidTests(unittest.TestCase):
repr(dni),
"MultipleInvalid(<InterfaceClass zope.interface.tests.test_exceptions.IDummy>,"
" 'target',"
" [BrokenMethodImplementation('aMethod', 'I said so', '<Not Given>'),"
" Exception('Regular', 'exception')])"
" (BrokenMethodImplementation('aMethod', 'I said so'),"
" Exception('Regular', 'exception')))"
)

View File

@ -78,7 +78,7 @@ def _verify(iface, candidate, tentative=False, vtype=None):
excs = []
if not tentative and not tester(candidate):
excs.append(DoesNotImplement(iface))
excs.append(DoesNotImplement(iface, candidate))
for name, desc in iface.namesAndDescriptions(all=True):
try:
@ -102,7 +102,7 @@ def _verify_element(iface, name, desc, candidate, vtype):
# 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):
@ -146,7 +146,8 @@ def _verify_element(iface, name, desc, candidate, vtype):
else:
if not callable(attr):
raise BrokenMethodImplementation(desc, "implementation is not a method", candidate)
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
@ -157,7 +158,7 @@ def _verify_element(iface, name, desc, candidate, vtype):
if mess:
if PYPY2 and _pypy2_false_positive(mess, candidate, vtype):
return
raise BrokenMethodImplementation(desc, mess, candidate)
raise BrokenMethodImplementation(desc, mess, attr, iface, candidate)