Make verification errors more readable and useful.

Eliminate the trailing newlines and blank spaces (the code called them
"a stupid artifact").

Include the name of the defining interface (so the user can easily look up
any requirements on the attribute) and, for methods, the expected
signature (no more guessing about how many arguments are required!).

This is implemented by giving Attribute and Method useful reprs and strs.
Previously, they just had the defaults.

Fixes #170
This commit is contained in:
Jason Madden 2020-02-06 10:48:04 -06:00
parent cc537c613b
commit a825e5f29e
No known key found for this signature in database
GPG Key ID: 349F84431A08B99E
7 changed files with 292 additions and 77 deletions

View File

@ -96,6 +96,16 @@
verify as ``IFullMapping``, ``ISequence`` and ``IReadSequence,``
respectively on all versions of Python.
- Add human-readable ``__str___`` and ``__repr___`` to ``Attribute``
and ``Method``. These contain the name of the defining interface
and the attribute. For methods, it also includes the signature.
- Change the error strings returned by ``verifyObject`` and
``verifyClass``. They now include more human-readable information
and exclude extraneous lines and spaces. See `issue 170
<https://github.com/zopefoundation/zope.interface/issues/170>`_.
4.7.1 (2019-11-11)
==================

View File

@ -1,6 +1,6 @@
===================================
Verifying interface implementations
===================================
=====================================
Verifying interface implementations
=====================================
The ``zope.interface.verify`` module provides functions that test whether a
given interface is implemented by a class or provided by an object, resp.
@ -52,33 +52,30 @@ Attributes of the object, be they defined by its class or added by its
>>> verifyObject(IFoo, Foo())
True
If either attribute is missing, verification will fail:
If either attribute is missing, verification will fail by raising an
exception. (We'll define a helper to make this easier to show.)
.. doctest::
>>> def verify_foo():
... foo = Foo()
... try:
... return verifyObject(IFoo, foo)
... except BrokenImplementation as e:
... print(e)
>>> @implementer(IFoo)
... class Foo(object):
... x = 1
>>> try: #doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
... verifyObject(IFoo, Foo())
... except BrokenImplementation as e:
... print(e)
An object has failed to implement interface <InterfaceClass ...IFoo>
<BLANKLINE>
The y attribute was not provided.
<BLANKLINE>
>>> verify_foo()
The object <Foo...> has failed to implement interface <InterfaceClass ...IFoo>: The IFoo.y attribute was not provided.
>>> @implementer(IFoo)
... class Foo(object):
... def __init__(self):
... self.y = 2
>>> try: #doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
... verifyObject(IFoo, Foo())
... except BrokenImplementation as e:
... print(e)
An object has failed to implement interface <InterfaceClass ...IFoo>
<BLANKLINE>
The x attribute was not provided.
<BLANKLINE>
>>> verify_foo()
The object <Foo...> has failed to implement interface <InterfaceClass ...IFoo>: The IFoo.x 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:
@ -92,14 +89,9 @@ when trying to get its value, the attribute is considered missing:
... @property
... def x(self):
... raise AttributeError
>>> try: #doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
... verifyObject(IFoo, Foo())
... except BrokenImplementation as e:
... print(e)
An object has failed to implement interface <InterfaceClass ...IFoo>
<BLANKLINE>
The x attribute was not provided.
<BLANKLINE>
>>> verify_foo()
The object <Foo...> has failed to implement interface <InterfaceClass ...IFoo>: The IFoo.x attribute was not provided.
Any other exception raised by a property will propagate to the caller of
``verifyObject``:
@ -111,7 +103,7 @@ Any other exception raised by a property will propagate to the caller of
... @property
... def x(self):
... raise Exception
>>> verifyObject(IFoo, Foo())
>>> verify_foo()
Traceback (most recent call last):
Exception
@ -126,5 +118,85 @@ any harm:
... @property
... def y(self):
... raise Exception
>>> verifyObject(IFoo, Foo())
>>> verify_foo()
True
Testing For Methods
-------------------
Methods are also validated to exist. We'll start by defining a method
that takes one argument. If we don't provide it, we get an error.
.. doctest::
>>> class IFoo(Interface):
... def simple(arg1): "Takes one positional argument"
>>> @implementer(IFoo)
... 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::
>>> from zope.interface.exceptions import BrokenMethodImplementation
>>> def verify_foo():
... foo = Foo()
... try:
... return verifyObject(IFoo, foo)
... except BrokenMethodImplementation as e:
... print(e)
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.
Requiring too many arguments is an error. (Recall that the ``self``
argument is implicit.)
.. 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.
Variable arguments can be used to implement the required number, as
can arguments with defaults.
.. doctest::
>>> Foo.simple = lambda self, *args: "Varargs work."
>>> verify_foo()
True
>>> Foo.simple = lambda self, a=1, b=2: "Default args work."
>>> verify_foo()
True
If our interface defines a method that uses variable positional or
variable keyword arguments, the implementation must also accept them.
.. doctest::
>>> class IFoo(Interface):
... def needs_kwargs(**kwargs): pass
>>> @implementer(IFoo)
... 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.
>>> 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.

View File

@ -29,42 +29,86 @@ class Invalid(Exception):
"""A specification is violated
"""
class DoesNotImplement(Invalid):
""" This object does not implement """
def __init__(self, interface):
_NotGiven = object()
class _TargetMixin(object):
target = _NotGiven
@property
def _prefix(self):
if self.target is _NotGiven:
return "An object"
return "The object %r" % (self.target,)
class DoesNotImplement(Invalid, _TargetMixin):
"""
The *target* (optional) does not implement the *interface*.
.. versionchanged:: 5.0.0
Add the *target* argument and attribute, and change the resulting
string value of this object accordingly.
"""
def __init__(self, interface, target=_NotGiven):
Invalid.__init__(self)
self.interface = interface
self.target = target
def __str__(self):
return """An object does not implement interface %(interface)s
return "%s does not implement the interface %s." % (
self._prefix,
self.interface
)
""" % self.__dict__
class BrokenImplementation(Invalid, _TargetMixin):
"""
The *target* (optional) is missing the attribute *name*.
class BrokenImplementation(Invalid):
"""An attribute is not completely implemented.
.. versionchanged:: 5.0.0
Add the *target* argument and attribute, and change the resulting
string value of this object accordingly.
The *name* can either be a simple string or a ``Attribute`` object.
"""
def __init__(self, interface, name):
self.interface=interface
self.name=name
def __init__(self, interface, name, target=_NotGiven):
Invalid.__init__(self)
self.interface = interface
self.name = name
self.target = target
def __str__(self):
return """An object has failed to implement interface %(interface)s
return "%s has failed to implement interface %s: The %s attribute was not provided." % (
self._prefix,
self.interface,
repr(self.name) if isinstance(self.name, str) else self.name
)
The %(name)s attribute was not provided.
""" % self.__dict__
class BrokenMethodImplementation(Invalid, _TargetMixin):
"""
The *target* (optional) has a *method* that violates
its contract in a way described by *mess*.
class BrokenMethodImplementation(Invalid):
"""An method is not completely implemented.
.. versionchanged:: 5.0.0
Add the *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.
"""
def __init__(self, method, mess):
self.method=method
self.mess=mess
def __init__(self, method, mess, target=_NotGiven):
Invalid.__init__(self)
self.method = method
self.mess = mess
self.target = target
def __str__(self):
return """The implementation of %(method)s violates its contract
because %(mess)s.
""" % self.__dict__
return "%s violates its contract in %s: %s." % (
self._prefix,
repr(self.method) if isinstance(self.method, str) else self.method,
self.mess
)
class InvalidInterface(Exception):
"""The interface has invalid contents

View File

@ -642,6 +642,22 @@ class Attribute(Element):
interface = None
def _get_str_info(self):
"""Return extra data to put at the end of __str__."""
return ""
def __str__(self):
of = self.interface.__name__ + '.' if self.interface else ''
return of + self.__name__ + self._get_str_info()
def __repr__(self):
return "<%s.%s at 0x%x %s>" % (
type(self).__module__,
type(self).__name__,
id(self),
self
)
class Method(Attribute):
"""Method interfaces
@ -691,6 +707,8 @@ class Method(Attribute):
return "(%s)" % ", ".join(sig)
_get_str_info = getSignatureString
def fromFunction(func, interface=None, imlevel=0, name=None):
name = name or func.__name__

View File

@ -27,16 +27,24 @@ class DoesNotImplementTests(unittest.TestCase):
from zope.interface.exceptions import DoesNotImplement
return DoesNotImplement
def _makeOne(self):
def _makeOne(self, *args):
iface = _makeIface()
return self._getTargetClass()(iface)
return self._getTargetClass()(iface, *args)
def test___str__(self):
dni = self._makeOne()
# XXX The trailing newlines and blank spaces are a stupid artifact.
self.assertEqual(str(dni),
'An object does not implement interface <InterfaceClass '
'zope.interface.tests.test_exceptions.IDummy>\n\n ')
self.assertEqual(
str(dni),
'An object does not implement the interface '
'<InterfaceClass zope.interface.tests.test_exceptions.IDummy>.')
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>.')
class BrokenImplementationTests(unittest.TestCase):
@ -44,17 +52,25 @@ class BrokenImplementationTests(unittest.TestCase):
from zope.interface.exceptions import BrokenImplementation
return BrokenImplementation
def _makeOne(self, name='missing'):
def _makeOne(self, *args):
iface = _makeIface()
return self._getTargetClass()(iface, name)
return self._getTargetClass()(iface, 'missing', *args)
def test___str__(self):
dni = self._makeOne()
# XXX The trailing newlines and blank spaces are a stupid artifact.
self.assertEqual(str(dni),
'An object has failed to implement interface <InterfaceClass '
'zope.interface.tests.test_exceptions.IDummy>\n\n'
' The missing attribute was not provided.\n ')
self.assertEqual(
str(dni),
'An object has failed to implement interface '
'<InterfaceClass zope.interface.tests.test_exceptions.IDummy>: '
"The 'missing' attribute was not provided.")
def test___str__w_candidate(self):
dni = self._makeOne('candidate')
self.assertEqual(
str(dni),
'The object \'candidate\' has failed to implement interface '
'<InterfaceClass zope.interface.tests.test_exceptions.IDummy>: '
"The 'missing' attribute was not provided.")
class BrokenMethodImplementationTests(unittest.TestCase):
@ -62,11 +78,17 @@ class BrokenMethodImplementationTests(unittest.TestCase):
from zope.interface.exceptions import BrokenMethodImplementation
return BrokenMethodImplementation
def _makeOne(self, method='aMethod', mess='I said so'):
return self._getTargetClass()(method, mess)
def _makeOne(self, *args):
return self._getTargetClass()('aMethod', 'I said so', *args)
def test___str__(self):
dni = self._makeOne()
self.assertEqual(str(dni),
'The implementation of aMethod violates its contract\n'
' because I said so.\n ')
self.assertEqual(
str(dni),
"An object violates its contract in 'aMethod': 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.")

View File

@ -1857,6 +1857,30 @@ class AttributeTests(ElementTests):
from zope.interface.interface import Attribute
return Attribute
def test__repr__w_interface(self):
method = self._makeOne()
method.interface = type(self)
r = repr(method)
self.assertTrue(r.startswith('<zope.interface.interface.Attribute at'), r)
self.assertTrue(r.endswith(' AttributeTests.TestAttribute>'), r)
def test__repr__wo_interface(self):
method = self._makeOne()
r = repr(method)
self.assertTrue(r.startswith('<zope.interface.interface.Attribute at'), r)
self.assertTrue(r.endswith(' TestAttribute>'), r)
def test__str__w_interface(self):
method = self._makeOne()
method.interface = type(self)
r = str(method)
self.assertEqual(r, 'AttributeTests.TestAttribute')
def test__str__wo_interface(self):
method = self._makeOne()
r = str(method)
self.assertEqual(r, 'TestAttribute')
class MethodTests(AttributeTests):
@ -1919,6 +1943,34 @@ class MethodTests(AttributeTests):
method.kwargs = 'kw'
self.assertEqual(method.getSignatureString(), "(**kw)")
def test__repr__w_interface(self):
method = self._makeOne()
method.kwargs = 'kw'
method.interface = type(self)
r = repr(method)
self.assertTrue(r.startswith('<zope.interface.interface.Method at'), r)
self.assertTrue(r.endswith(' MethodTests.TestMethod(**kw)>'), r)
def test__repr__wo_interface(self):
method = self._makeOne()
method.kwargs = 'kw'
r = repr(method)
self.assertTrue(r.startswith('<zope.interface.interface.Method at'), r)
self.assertTrue(r.endswith(' TestMethod(**kw)>'), r)
def test__str__w_interface(self):
method = self._makeOne()
method.kwargs = 'kw'
method.interface = type(self)
r = str(method)
self.assertEqual(r, 'MethodTests.TestMethod(**kw)')
def test__str__wo_interface(self):
method = self._makeOne()
method.kwargs = 'kw'
r = str(method)
self.assertEqual(r, 'TestMethod(**kw)')
class Test_fromFunction(unittest.TestCase):

View File

@ -61,7 +61,7 @@ def _verify(iface, candidate, tentative=False, vtype=None):
raise DoesNotImplement(iface)
# Here the `desc` is either an `Attribute` or `Method` instance
for name, desc in iface.namesAndDescriptions(1):
for name, desc in iface.namesAndDescriptions(all=True):
try:
attr = getattr(candidate, name)
except AttributeError:
@ -70,7 +70,7 @@ def _verify(iface, candidate, tentative=False, vtype=None):
# class may provide attrs in it's __init__.
continue
raise BrokenImplementation(iface, name)
raise BrokenImplementation(iface, desc, candidate)
if not isinstance(desc, Method):
# If it's not a method, there's nothing else we can test
@ -110,21 +110,18 @@ def _verify(iface, candidate, tentative=False, vtype=None):
continue
else:
if not callable(attr):
raise BrokenMethodImplementation(name, "Not a method")
raise BrokenMethodImplementation(name, "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.
desc = desc.getSignatureInfo()
meth = meth.getSignatureInfo()
mess = _incompat(desc, meth)
mess = _incompat(desc.getSignatureInfo(), meth.getSignatureInfo())
if mess:
if PYPY2 and _pypy2_false_positive(mess, candidate, vtype):
continue
raise BrokenMethodImplementation(name, mess)
raise BrokenMethodImplementation(desc, mess, candidate)
return True