Skip to content

Commit

Permalink
Merge pull request #173 from zopefoundation/issue170
Browse files Browse the repository at this point in the history
Make verification errors more readable and useful.
  • Loading branch information
jamadden committed Feb 8, 2020
2 parents cc537c6 + 83f4f55 commit d6343ee
Show file tree
Hide file tree
Showing 9 changed files with 368 additions and 103 deletions.
12 changes: 12 additions & 0 deletions CHANGES.rst
Expand Up @@ -96,6 +96,18 @@
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 raised 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>`_.

.. caution:: This will break consumers (such as doctests) that
depended on the exact error messages.

4.7.1 (2019-11-11)
==================

Expand Down
10 changes: 9 additions & 1 deletion docs/README.rst
Expand Up @@ -2,6 +2,8 @@
Interfaces
==========

.. currentmodule:: zope.interface

Interfaces are objects that specify (document) the external behavior
of objects that "provide" them. An interface specifies behavior
through:
Expand Down Expand Up @@ -296,6 +298,7 @@ be used for classes, but in 3.6.0 and higher it can:
Note that class decorators using the ``@implementer(IFoo)`` syntax are only
supported in Python 2.6 and later.

.. autofunction:: implementer

Declaring provided interfaces
-----------------------------
Expand Down Expand Up @@ -412,6 +415,8 @@ We can find out what interfaces are directly provided by an object:
>>> list(zope.interface.directlyProvidedBy(newfoo))
[]

.. autofunction:: provider

Inherited declarations
----------------------

Expand Down Expand Up @@ -466,6 +471,8 @@ be used for this purpose:
>>> list(zope.interface.implementedBy(C))
[<InterfaceClass builtins.IFoo>]

.. autofunction:: classImplements

We can use ``classImplementsOnly`` to exclude inherited interfaces:

.. doctest::
Expand All @@ -477,6 +484,7 @@ We can use ``classImplementsOnly`` to exclude inherited interfaces:
>>> list(zope.interface.implementedBy(C))
[<InterfaceClass builtins.ISpecial>]

.. autofunction:: classImplementsOnly


Declaration Objects
Expand Down Expand Up @@ -791,7 +799,7 @@ exceptions as its argument:
... except Invalid as e:
... str(e)
'[RangeError(Range(2, 1))]'

And the list will be filled with the individual exceptions:

.. doctest::
Expand Down
184 changes: 136 additions & 48 deletions docs/verify.rst
@@ -1,32 +1,18 @@
===================================
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.


Verifying classes
=================

This is covered by unit tests defined in ``zope.interface.tests.test_verify``.
given interface is implemented by a class or provided by an object.

.. currentmodule:: zope.interface.verify

Verifying objects
=================

An object provides an interface if

- either its class declares that it implements the interfaces, or the object
declares that it directly provides the interface;

- the object defines all the methods required by the interface;

- all the methods have the correct signature;

- the object defines all non-method attributes required by the interface.
.. autofunction:: verifyObject

This doctest currently covers only the latter item.
.. autoexception:: zope.interface.Invalid

Testing for attributes
----------------------
Expand All @@ -52,33 +38,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 +75,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 +89,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 +104,115 @@ 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)

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.

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.

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.

.. autofunction:: verifyClass

.. doctest::

>>> from zope.interface.verify import verifyClass
>>> def verify_foo_class():
... try:
... return verifyClass(IFoo, Foo)
... except BrokenMethodImplementation 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.

0 comments on commit d6343ee

Please sign in to comment.