Documenting exception raising behaviour

In addition to documenting the individual exceptions in an interface, it is also necessary to document which of those exceptions may be raised by the methods and functions in that interface.

Languages like Java and Ada have mechanisms for declaring the exceptions that a method or function is allowed to raise, and those languages enforce that interface constraint.

Python (currently) has no such mechanism, but XC offers a partial stand-in, in the form of the @raises decorator.

@raises(exc1[, exc2, exc_list...])

This specifies a list of exceptions, or exception classes, that may be raised by the callable to which the decorator is applied.

Note

This decorator may be applied to any function or method, but not (yet) to a class.

Each of the parameters exc1, exc2, exc_list may be:

An exception class (with no subclasses)

The callable is permitted to raise an instance of this class.

An exception class (that has subclasses)

The callable is permitted to raise an instance of this class or any of its subclasses.

A list of either of the above

A parameter that is a list is simply ‘flattened’; each member of the list is interpreted according to these three rules.

The decorator performs two functions:

  1. It adds documentation of the exception-raising behaviour of the callable to which it is applied;

  2. It creates a run-time check that the declared exception-raising behaviour is followed. If an exception is raised that is not included in the permitted set, that exception is replaced by a BadExceptionBug exception that wraps the original (bad) exception.

Example: Enforcement

The following code defines a function that is declared capable of raising an exception, but which can in fact raise another:


from rjgtoys.xc import Bug

from rjgtoys.xc.raises import raises

class Allowed(Exception):
    """This exception may be raised by do_operation."""

    def __str__(self):
        return "I am allowed"

class NotAllowed(Exception):
    """This exception may not be raised by do_operation."""

    def __str__(self):
        return "I am not allowed"


@raises(Allowed)
def do_operation(ok: bool):
    if ok:
        raise Allowed()
    else:
        raise NotAllowed()

try:
    do_operation(True)
    assert False, "Should not be reached"
except Allowed:
    print("Expected exception raised")

try:
    do_operation(False)
    assert False, "Should not be reached"
except Bug:
    print("There is a bug in do_operation")

Example: Documentation

Here are some exception declarations:

class Failed(xc.Error):
    """Raised when something fails."""

    what: str = xc.Title("What failed")

    detail = "Failed to {what}"


class FooError(xc.Error):
    """Base class for Foo errors."""

    pass

class PleaseWait(FooError):
    """Raised when an operation can't be done right now."""

    howlong: int = xc.Title("Minimum amount of time to wait")

    detail = "Please wait at least {howlong} seconds"

class Other(Exception):
    """Raised for some other condition."""
    pass

class Bad(Exception):
    pass

Now a function that may raise them:

@raises(Failed, FooError, Other, Bad)
def foo():
    """Does a foo on a bar.

    This is yet more documentation about foo.

    """

    pass

The markup to document that would be something like this:

.. autofunction:: examples.raiser.foo

.. autoexception:: examples.raiser.Failed
.. autoexception:: examples.raiser.FooError
.. autoexception:: examples.raiser.PleaseWait
.. autoexception:: examples.raiser.Other
.. autoexception:: examples.raiser.Bad

And the result:

foo()[source]

Does a foo on a bar.

This is yet more documentation about foo.

Raises:

Failed

Raised when something fails.

FooError (or a subclass of it)

Base class for Foo errors.

Other

Raised for some other condition.

Bad

(not documented)

OSError (or a subclass of it)

Base class for I/O related errors.

exception Failed(what)[source]

Raised when something fails.

A subclass of rjgtoys.xc.Error

Parameters

what (str) – What failed

Each parameter defines an attribute of the same name.

Properties (read-only)

typename = ‘examples.raiser.Failed’

title = ‘Raised when something fails.’

detail = ‘Failed to {what}’

status = 400

For more information about the above properties please refer to the documentation for rjgtoys.xc.

exception FooError[source]

Base class for Foo errors.

A subclass of rjgtoys.xc.Error

Properties (read-only)

status = 400

For more information about the above properties please refer to the documentation for rjgtoys.xc.

Subclasses:

exception PleaseWait(howlong)[source]

Raised when an operation can’t be done right now.

A subclass of examples.raiser.FooError

Parameters

howlong (int) – Minimum amount of time to wait

Each parameter defines an attribute of the same name.

Properties (read-only)

typename = ‘examples.raiser.PleaseWait’

title = “Raised when an operation can’t be done right now.”

detail = ‘Please wait at least {howlong} seconds’

status = 400

For more information about the above properties please refer to the documentation for rjgtoys.xc.

exception Other[source]

Raised for some other condition.

exception Bad[source]

Notice how the exception names in the description of foo() are linked to the full description of the exception.