PyContracts low-level API

This is a discussion of the low-level features of the PyContracts API. This is primarely of interest to developers who want to modify PyContracts or integrate it with other libraries.

Decorating a function explicitly

The public interface of PyContracts contains a function decorate() that can be used to decorate functions after they are defined. For example:

from contracts import decorate

def f(a):
    a.pop()
    return a

f2 = decorate(f, a='list[N],N>1', returns='list[N-1]')

f2([]) # raises exception before pop() is executed

Checking contracts manually

In addition to the @contract() decorator, PyContracts offers an interface for checking expressions explicitly.

The check() function allows you to check that some value respect a given contract.

from contracts import check

check('tuple(int,int)', (2, 2) )

check() might raise:

ContractSyntaxError
If there are parsing errors.
ContractNotRespected
If the value does not respect the contract constraints.

Both exceptions are instances of ContractException. Usually you want to use check() as a better assert(), so you don’t want to catch those exceptions. The error message should be descriptive enough.

For example, the message generated by

check('tuple(int, tuple(*,str))', (0, (1, 2)))

is

contracts.ContractNotRespected: Expected a string, got 'int'.
 context: checking: str                       for value: Instance of int: 2
 context: checking: tuple(*,str)              for value: Instance of tuple: (1, 2)
 context: checking: tuple(int,tuple(*,str))   for value: Instance of tuple: (0, (1, 2))

The message shows the recursive matches of expressions, and should be enough to diagnose the problem. Nevertheless, you can add a third argument to specify a message that will be included in the error, if one is thrown. For example:

score = (2, None)
check('tuple(int,int)', score, 'Player score must be a tuple of 2 int.' )

Interpreting error messages

In the case you are using variables, the matching “context” is shown. The context is the set of variables which have already bound to values.

For example, suppose you want a tuple with two lists of equal length. This can be expressed as:

value = ([0,1],[1,2,3]) # invalid
check('tuple(list[N], list[N])', value)

What happens behind the scenes is that PyContracts first matches the first list, binds the variable N to the length of that list (=2), then checks that list[N] describes the second list. Now, however, N has already been bound, and the matching fails. The error you would see is:

contracts.ContractNotRespected: Expected that 'N' = 2, got 3.
checking: N                        (N=2)   for value: Instance of int: 3
checking: list[N]                  (N=2)   for value: Instance of list: [1, 2, 3]
checking: tuple(list[N],list[N])           for value: Instance of tuple: ([0, 1], [1, 2, 3])

PyContracts tries hard to give you a readable message even with complicated contracts. For example, consider the complicated expression:

check('int|str|tuple(str,int)', ('a', 'b'))

Because there is an OR (|), PyContracts will have to explain to you why none of the three OR clauses was satisfied.

contracts.interface.ContractNotRespected: Could not satisfy any of the 3 clauses in int|str|tuple(str,int).
 ---- Clause #1:   int
 | Expected type 'int', got 'tuple'.
 | checking: int      for value: Instance of tuple: ('a', 'b')
 ---- Clause #2:   str
 | Expected a string, got 'tuple'.
 | checking: str      for value: Instance of tuple: ('a', 'b')
 ---- Clause #3:   tuple(str,int)
 | Expected type 'int', got 'str'.
 | checking: int                 for value: Instance of str: 'b'
 | checking: tuple(str,int)      for value: Instance of tuple: ('a', 'b')
 ------- (end clauses) -------
checking: int|str|tuple(str,int)      for value: Instance of tuple: ('a', 'b')

Explicit parsing and checking

The results of parsing are cached internally, there is no performance penalty in using check() often.

If you wish, you can also separate the parsing and checking phases. The function parse() will return a Contract object, which in turn has a Contract.check() function you can call.

from contracts import parse

contract = parse('>0')  # May raise ContractSyntaxError
contract.check(2)       # May raise ContractNotRespected

Checking multiple values

The most interesting uses of PyContracts are when it is used to check that multiple objects respect mutual constraints.

You can use check_multiple() to check that a series of values respect a series of contracts, that might reference each other’s variables. The argument to check_multiple() must be a list of tuples with two elements (contract, value).

In the next example, we are defining a table. We want data to be a list of lists with equal length, and that row_labels and col_labels have the expected length, coherent with data. PyContracts allows you to do all this checks with a remarkably clear, concise, and readable notation.

from contracts import check_multiple

data = [[1,2,3],
        [4,5,6]]
row_labels = ['first season', 'second season']
col_labels = ['Team1', 'Team2', 'Team3']

check_multiple([('list[C](str),C>0', col_labels),
                ('list[R](str),R>0', row_labels),
                ('list[R](list[C](numbers))', data) ],
               'I expect col_labels, row_labels, and data to '
               'have coherent dimensions.')

Examining the Contract objects

If you want to examine the contract object, use Contract.__repr__() for eval()uable expression, or Contract.__str__() to get back a representation using PyContracts’ language.

from contracts import parse

contract = parse(' list[N](int), N > 0')

print("%s" % contract)   # => 'list[N](int),N>0'
print("%r" % contract)
# => And([List(BindVariable('N',int),CheckType(int)),
#         CheckOrder(VariableRef('N'),'>',SimpleRValue(0))])

The latter is useful if you are hacking PyContracts and you want to check in detail what is going on.

Examining bound variables

If you are using binding expression, you can examine what variables were bound using the return values. The functions check() and Contract.check() will return a Context object.

context = check('list[N](str), N>0', ['a','b','c'])

print('N' in context)  # => True
print(context['N'])    # => 3

Using PyContracts in your own PyParsing-based language

You can use contract_expression to add a contract expression to your PyParsing-based grammar:

from contracts import contract_expression

from pyparsing import Literal, Optional

my_grammar = ... + Optional(Literal('respects') + contract_expression)