pcontracts module

This module is a thin wrapper around the PyContracts library that enables customization of the exception type raised and limited customization of the exception message. Additionally, custom contracts specified via pexdoc.pcontracts.new_contract() and enforced via pexdoc.pcontracts.contract() register exceptions using the pexdoc.exh module, which means that the exceptions raised by these contracts can be automatically documented using the pexdoc.exdoc module.

The way a contract is specified is identical to the decorator way of specifying a contract with the PyContracts library. By default a RuntimeError exception with the message 'Argument `*[argument_name]*` is not valid' is raised unless a custom contract specifies a different exception (the token *[argument_name]* is replaced by the argument name the contract is attached to). For example, the definitions of the custom contracts pexdoc.ptypes.file_name() and pexdoc.ptypes.file_name_exists() are:

@pexdoc.pcontracts.new_contract()
def file_name(obj):
    r"""
    Validate if an object is a legal name for a file.

    :param obj: Object
    :type  obj: any

    :raises: RuntimeError (Argument \`*[argument_name]*\` is not
     valid). The token \*[argument_name]\* is replaced by the name
     of the argument the contract is attached to

    :rtype: None
    """
    msg = pexdoc.pcontracts.get_exdesc()
    # Check that argument is a string
    if not isinstance(obj, str) or (isinstance(obj, str) and ("\0" in obj)):
        raise ValueError(msg)
    # If file exists, argument is a valid file name, otherwise test
    # if file can be created. User may not have permission to
    # write file, but call to os.access should not fail if the file
    # name is correct
    try:
        if not os.path.exists(obj):
            os.access(obj, os.W_OK)
    except (TypeError, ValueError):  # pragma: no cover
        raise ValueError(msg)
@pexdoc.pcontracts.new_contract(
    argument_invalid="Argument `*[argument_name]*` is not valid",
    file_not_found=(OSError, "File *[fname]* could not be found"),
)
def file_name_exists(obj):
    r"""
    Validate if an object is a legal name for a file *and* that the file exists.

    :param obj: Object
    :type  obj: any

    :raises:
     * OSError (File *[fname]* could not be found). The
       token \*[fname]\* is replaced by the *value* of the
       argument the contract is attached to

     * RuntimeError (Argument \`*[argument_name]*\` is not valid).
       The token \*[argument_name]\* is replaced by the name of
       the argument the contract is attached to

    :rtype: None
    """
    exdesc = pexdoc.pcontracts.get_exdesc()
    msg = exdesc["argument_invalid"]
    # Check that argument is a string
    if not isinstance(obj, str) or (isinstance(obj, str) and ("\0" in obj)):
        raise ValueError(msg)
    # Check that file name is valid
    try:
        os.path.exists(obj)
    except (TypeError, ValueError):  # pragma: no cover
        raise ValueError(msg)
    # Check that file exists
    obj = _normalize_windows_fname(obj)
    if not os.path.exists(obj):
        msg = exdesc["file_not_found"]
        raise ValueError(msg)

This is nearly identical to the way custom contracts are defined using the PyContracts library with two exceptions:

  1. To avoid repetition and errors, the exception messages defined in the pexdoc.pcontracts.new_contract() decorator are available in the contract definition function via pexdoc.pcontracts.get_exdesc().
  2. A PyContracts new contract can return False or raise a ValueError exception to indicate a contract breach, however a new contract specified via the pexdoc.pcontracts.new_contract() decorator has to raise a ValueError exception to indicate a contract breach.

Exceptions can be specified in a variety of ways and verbosity is minimized by having reasonable defaults (see pexdoc.pcontracts.new_contract() for a full description). What follows is a simple usage example of the two contracts shown above and the exceptions they produce:

# pcontracts_example_1.py
from __future__ import print_function
import pexdoc


@pexdoc.pcontracts.contract(name="file_name")
def print_if_fname_valid(name):
    """Sample function 1."""
    print("Valid file name: {0}".format(name))


@pexdoc.pcontracts.contract(num=int, name="file_name_exists")
def print_if_fname_exists(num, name):
    """Sample function 2."""
    print("Valid file name: [{0}] {1}".format(num, name))
>>> import os
>>> from docs.support.pcontracts_example_1 import *
>>> print_if_fname_valid('some_file.txt')
Valid file name: some_file.txt
>>> print_if_fname_valid('invalid_fname.txt\0')
Traceback (most recent call last):
    ...
RuntimeError: Argument `name` is not valid
>>> fname = os.path.join('..', 'docs', 'pcontracts.rst') #doctest: +ELLIPSIS
>>> print_if_fname_exists(10, fname)
Valid file name: [10] ...pcontracts.rst
>>> print_if_fname_exists('hello', fname)
Traceback (most recent call last):
    ...
RuntimeError: Argument `num` is not valid
>>> print_if_fname_exists(5, 'another_invalid_fname.txt\0')
Traceback (most recent call last):
    ...
RuntimeError: Argument `name` is not valid
>>> print_if_fname_exists(5, '/dev/null/some_file.txt')
Traceback (most recent call last):
    ...
OSError: File /dev/null/some_file.txt could not be found

Functions

pexdoc.pcontracts.all_disabled()

Wrap PyContracts all_disabled() function.

From the PyContracts documentation: “Returns true if all contracts are disabled”

pexdoc.pcontracts.disable_all()

Wrap PyContracts disable_all() function.

From the PyContracts documentation: “Disables all contract checks”

pexdoc.pcontracts.enable_all()

Wrap PyContracts enable_all() function.

From the PyContracts documentation: “Enables all contract checks. Can be overridden by an environment variable”

pexdoc.pcontracts.get_exdesc()

Retrieve contract exception(s) message(s).

If the custom contract is specified with only one exception the return value is the message associated with that exception; if the custom contract is specified with several exceptions, the return value is a dictionary whose keys are the exception names and whose values are the exception messages.

Raises:RuntimeError (Function object could not be found for function [function_name])
Return type:string or dictionary

For example:

# pcontracts_example_2.py
import pexdoc.pcontracts

@pexdoc.pcontracts.new_contract('Only one exception')
def custom_contract_a(name):
    msg = pexdoc.pcontracts.get_exdesc()
    if not name:
        raise ValueError(msg)

@pexdoc.pcontracts.new_contract(ex1='Empty name', ex2='Invalid name')
def custom_contract_b(name):
    msg = pexdoc.pcontracts.get_exdesc()
    if not name:
        raise ValueError(msg['ex1'])
    elif name.find('[') != -1:
        raise ValueError(msg['ex2'])

In custom_contract1() the variable msg contains the string 'Only one exception', in custom_contract2() the variable msg contains the dictionary {'ex1':'Empty name', 'ex2':'Invalid name'}.

Decorators

pexdoc.pcontracts.contract(**contract_args)

Wraps PyContracts contract() decorator (only the decorator way of specifying a contract is supported and tested). A RuntimeError exception with the message 'Argument `*[argument_name]*` is not valid' is raised when a contract is breached ('*[argument_name]*' is replaced by the argument name the contract is attached to) unless the contract is custom and specified with the pexdoc.pcontracts.new_contract() decorator. In this case the exception type and message are controlled by the custom contract specification.

pexdoc.pcontracts.new_contract(*args, **kwargs)

Defines a new (custom) contract with custom exceptions.

Raises:
  • RuntimeError (Attempt to redefine custom contract `*[contract_name]*`)
  • TypeError (Argument `contract_exceptions` is of the wrong type)
  • TypeError (Argument `contract_name` is of the wrong type)
  • TypeError (Contract exception definition is of the wrong type)
  • TypeError (Illegal custom contract exception definition)
  • ValueError (Empty custom contract exception message)
  • ValueError (Contract exception messages are not unique)
  • ValueError (Contract exception names are not unique)
  • ValueError (Multiple replacement fields to be substituted by argument value)

The decorator argument(s) is(are) the exception(s) that can be raised by the contract. The most general way to define an exception is using a 2-item tuple with the following members:

  • exception type (type) – Either a built-in exception or sub-classed from Exception. Default is RuntimeError
  • exception message (string) – Default is 'Argument `*[argument_name]*` is not valid', where the token *[argument_name]* is replaced by the argument name the contract is attached to

The order of the tuple elements is not important, i.e. the following are valid exception specifications and define the same exception:

@pexdoc.pcontracts.new_contract(ex1=(RuntimeError, 'Invalid name'))
def custom_contract1(arg):
    if not arg:
        raise ValueError(pexdoc.pcontracts.get_exdesc())

@pexdoc.pcontracts.new_contract(ex1=('Invalid name', RuntimeError))
def custom_contract2(arg):
    if not arg:
        raise ValueError(pexdoc.pcontracts.get_exdesc())

The exception definition simplifies to just one of the exception definition tuple items if the other exception definition tuple item takes its default value. For example, the same exception is defined in these two contracts:

@pexdoc.pcontracts.new_contract(ex1=ValueError)
def custom_contract3(arg):
    if not arg:
        raise ValueError(pexdoc.pcontracts.get_exdesc())

@pexdoc.pcontracts.new_contract(ex1=(
    ValueError,
    'Argument `*[argument_name]*` is not valid'
))
def custom_contract4(arg):
    if not arg:
        raise ValueError(pexdoc.pcontracts.get_exdesc())

and these contracts also define the same exception (but different from that of the previous example):

@pexdoc.pcontracts.new_contract(ex1='Invalid name')
def custom_contract5(arg):
    if not arg:
        raise ValueError(pexdoc.pcontracts.get_exdesc())

@pexdoc.pcontracts.new_contract(ex1=('Invalid name', RuntimeError))
def custom_contract6(arg):
    if not arg:
        raise ValueError(pexdoc.pcontracts.get_exdesc())

In fact the exception need not be specified by keyword if the contract only uses one exception. All of the following are valid one-exception contract specifications:

@pexdoc.pcontracts.new_contract(
    (OSError, 'File could not be opened')
)
def custom_contract7(arg):
    if not arg:
        raise ValueError(pexdoc.pcontracts.get_exdesc())

@pexdoc.pcontracts.new_contract('Invalid name')
def custom_contract8(arg):
    if not arg:
        raise ValueError(pexdoc.pcontracts.get_exdesc())

@pexdoc.pcontracts.new_contract(TypeError)
def custom_contract9(arg):
    if not arg:
        raise ValueError(pexdoc.pcontracts.get_exdesc())

No arguments are needed if a contract only needs a single exception and the default exception type and message suffice:

@pexdoc.pcontracts.new_contract()
def custom_contract10(arg):
    if not arg:
        raise ValueError(pexdoc.pcontracts.get_exdesc())

For code conciseness and correctness the exception message(s) should be retrieved via the pexdoc.pcontracts.get_exdesc() function.

A PyContracts new contract can return False or raise a ValueError exception to indicate a contract breach, however a new contract specified via the pexdoc.pcontracts.new_contract() decorator has to raise a ValueError exception to indicate a contract breach.

The exception message can have substitution “tokens” of the form *[token_name]*. The token *[argument_name]* is substituted with the argument name the contract it is attached to. For example:

@pexdoc.pcontracts.new_contract((
    TypeError,
    'Argument `*[argument_name]*` has to be a string'
))
def custom_contract11(city):
    if not isinstance(city, str):
        raise ValueError(pexdoc.pcontracts.get_exdesc())

@pexdoc.pcontracts.contract(city_name='custom_contract11')
def print_city_name(city_name):
    return 'City: {0}'.format(city_name)
>>> from __future__ import print_function
>>> from docs.support.pcontracts_example_3 import print_city_name
>>> print(print_city_name('Omaha'))
City: Omaha
>>> print(print_city_name(5))   
Traceback (most recent call last):
    ...
TypeError: Argument `city_name` has to be a string

Any other token is substituted with the argument value. For example:

@pexdoc.pcontracts.new_contract((
    OSError, 'File `*[fname]*` not found'
))
def custom_contract12(fname):
    if not os.path.exists(fname):
        raise ValueError(pexdoc.pcontracts.get_exdesc())

@pexdoc.pcontracts.contract(fname='custom_contract12')
def print_fname(fname):
    print('File name to find: {0}'.format(fname))
>>> from __future__ import print_function
>>> import os
>>> from docs.support.pcontracts_example_3 import print_fname
>>> fname = os.path.join(os.sep, 'dev', 'null', '_not_a_file_')
>>> print(print_fname(fname))   
Traceback (most recent call last):
    ...
OSError: File `..._not_a_file_` not found