Skip to content

Commit

Permalink
Make verification errors more readable and useful.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jamadden committed Feb 7, 2020
1 parent cc537c6 commit a825e5f
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 77 deletions.
10 changes: 10 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
==================

Expand Down
132 changes: 102 additions & 30 deletions docs/verify.rst
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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``:
Expand All @@ -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

Expand All @@ -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.
86 changes: 65 additions & 21 deletions src/zope/interface/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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*.
.. versionchanged:: 5.0.0
Add the *target* argument and attribute, and change the resulting
string value of this object accordingly.
class BrokenImplementation(Invalid):
"""An attribute is not completely implemented.
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*.
.. versionchanged:: 5.0.0
Add the *target* argument and attribute, and change the resulting
string value of this object accordingly.
class BrokenMethodImplementation(Invalid):
"""An method is not completely implemented.
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
Expand Down
18 changes: 18 additions & 0 deletions src/zope/interface/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -691,6 +707,8 @@ def getSignatureString(self):

return "(%s)" % ", ".join(sig)

_get_str_info = getSignatureString


def fromFunction(func, interface=None, imlevel=0, name=None):
name = name or func.__name__
Expand Down
Loading

0 comments on commit a825e5f

Please sign in to comment.