User’s Guide

Creating Pacts

A pact is a form of a promise. It’s similar to concepts like A+/promises that you may be familiar with already. It is intended for scenarios where you want to return ‘handles’ to asynchronous events, and be able to elegantly control what happens with those events.

Suppose we have an API that copies a large directory structure in the background, delete_async(path), returning some identifier for the ongoing task, and a complementary API, is_async_delete_finished(id) asking if it completed.

A pact wraps this api nicely:

>>> from pact import Pact

>>> def pact_delete_async(path):
...     returned = Pact('Deleting {0}'.format(path))
...     operation_id = delete_async(path)
...     returned.until(is_async_delete_finished, operation_id)
...     return returned

Note the example uses pact.Pact.until() to denote when the pact can be considered ‘finished’.

Checking Pact Status

Our code can now interact with the returned pact object. Checking whether or not a pact is finished can be done using the is_finished predicate, but it does not cause our pact to actually check its predicates for completion. That is done by poll:

>>> p = pact_delete_async('/path')
>>> p.poll()
False
>>> p.is_finished()
False
>>> sleep(10)
>>> p.is_finished()
False
>>> p.poll()
True
>>> p.is_finished()
True

Waiting on Pacts

A very common scenario is to wait until a pact is finished. This is what the pact.Pact.wait() method is for:

>>> from pact import TimeoutExpired
>>> p = pact_delete_async('/path')
>>> p.wait()
>>> p.is_finished()
True

You can also specify a timeout in seconds. Expiration of the timeout will result in an exception:

>>> p = pact_delete_async('/path')
>>> try:
...     p.wait(timeout_seconds=1.5)
... except TimeoutExpired as e:
...     print('Got exception:', e)
Got exception: Timeout of 1.5 seconds expired waiting for <Pact: Deleting /path>

Grouping Pacts

Pacts support joining multiple instances together to form a group:

>>> from pact import PactGroup
>>> p1 = pact_delete_async('/path1')
>>> p2 = pact_delete_async('/path2')
>>> group = PactGroup([p1, p2])

There is a shorter syntax as well, using the + operator:

>>> group = p1 + p2

The most immediate thing you can do on a pact group is wait for it to end altogether:

>>> group.wait()

And of course it will be more descriptive when only one pact was not satisfied:

>>> group =(pact_delete_async('/path1') + pact_delete_async('/huge_directory'))
>>> try:
...     group.wait(timeout_seconds=10)
... except TimeoutExpired as e:
...     print('Got exception:', e)
Got exception: Timeout of 10 seconds expired waiting for [<Pact: Deleting /huge_directory>]

Waiting for group is a lazy operation, by default, which means that will poll pacts only if previous pact had finished:

>>> pact_a = pact_delete_async('/path_a').during(print, 'a', end='').then(print, 'A', end='')
>>> pact_b = pact_delete_async('/path_b').during(print, 'b', end='').then(print, 'B')
>>> PactGroup([pact_a, pact_b]).wait()
aaaaaaaaaaaAbB

Group can be poll eagerly by passing lazy=False to its creation. This will make each polling operation to poll all unfinished pacts in the group every time.

>>> pact_a = pact_delete_async('/path_a').during(print, 'a', end='').then(print, 'A', end='')
>>> pact_b = pact_delete_async('/path_b').during(print, 'b', end='').then(print, 'B')
>>> PactGroup([pact_a, pact_b], lazy=False).wait()
ababababababababababaAbB

Specifying Pre-Compouted Deadlines

Pacts and pact groups allow you to specify a deadline using the timeout_seconds parameter passed to their constructors.

This parameter specifies the overall number of seconds within which the pact is expected to finish, starting from it’s creation:

>>> def pact_delete_async_known_time(path, timeout_seconds=None):
...     returned = Pact('Deleting {0}'.format(path), timeout_seconds=timeout_seconds)
...     operation_id = delete_async(path)
...     returned.until(is_async_delete_finished, operation_id)
...     return returned

When calling pact.Pact.wait() witheout the parameter timeout_seconds, it will expire when the overall deadline is reached (or immediately if has already passed).

>>> pact = pact_delete_async_known_time('/path', timeout_seconds=8)
>>> try:
...     pact.wait()
... except TimeoutExpired as e:
...     print('Got exception:', e)
Got exception: Timeout of 8.0 seconds expired waiting for <Pact: Deleting /path>

A common use-case is executing asynchronous command with known expected duration, peforming other tasks, and then waiting for the command to finish. Calling pact.Pact.wait() with the parameter timeout_seconds will behave as regular (wait until timeout_seconds passed or until the pact is finished).

Absorbing Pacts into Groups

Sometimes you would like to group pacts, but only fire the then callbacks when the entire group is satisfied. In addition to adding the then to the group itself, there is another shortcut called absorb:

>>> group = pact_delete_async('/path1').then(print, 'finished') + pact_delete_async('/huge_directory').then(print, 'also finished')

In the above example, the also finished string will get printed once huge_directory is deleted. However this may be long before /path is deleted. To force all then callbacks to happen after the entire group finishes, we can use absorb:

>>> group = PactGroup()
>>> p1 = pact_delete_async('/path1').then(print, 'finished')
>>> p2 = pact_delete_async('/huge_directory').then(print, 'also finished')
>>> group.add(p1, absorb=True)
>>> group.add(p2, absorb=True)

Note

When absorbing pacts, the callbacks are no longer owned by the absorbed pacts, so waiting for them alone would not trigger them

Triggering Actions

You can easily attach callbacks to occur when a pact finishes:

>>> pact_delete_async('/path1').then(print, 'finished').wait()
finished

This can be chained multiple times

>>> pact_delete_async('/path1').\
...    then(print, 'message1').\
...    then(print, 'message2').\
...    wait()
message1
message2

Also for groups:

>>> start_time = time()
>>> group = pact_delete_async('/path1').\
...     then(lambda: print('path1 finished after', time() - start_time, 'seconds')) \
...   + pact_delete_async('/huge_dir').\
...     then(lambda: print('huge_dir finished after', time() - start_time, 'seconds'))
>>> group.wait()
path1 finished after 10.0 seconds
huge_dir finished after 30.0 seconds

Similarly, you can attach callbacks used for cleanup that will occur when a pact finishes but after all ‘then’ callbacks:

>>> pact_delete_async('/path1').lastly(print, 'cleaning up').then(print, 'finished').wait()
finished
cleaning up

Lastly callbacks can be chained or added to groups just like normal ‘then’ callbacks.

Triggering Actions During a Wait

You can specify a callback to be called while the wait is ongoing, using pact.Pact.during():

>>> pact_delete_async('/path').during(print, '~', end='').then(print, 'Done!').wait()
~~~~~~~~~~~Done!

Triggering Actions on Timeout

Using the pact.Pact.on_timeout() method, you can add additional callbacks to be called when a timeout is encountered:

>>> pact_delete_async('/path').on_timeout(print, 'bummer').on_timeout(print, 'so what now?').wait()