Skip to content
Container and Transparent Proxy Promises in Python
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
docs
promises
tests
.flake8
.gitignore
.travis.yml
LICENSE
README.md
setup.py

README.md

Overview of python-promises

Build Status Coverage Status

A Python module providing container and proxy promises, supporting delayed linear and multi-processing delivery.

This is dissimilar to PEP-3148, where the focus is on a robust asynchronous delivery framework. We're mostly interested in simple deferral and most of all, transparent proxies.

At this stage this project is just a rough draft. I've set the version to 0.9.0 and am not promising any kind of API stability until 1.0.0 at which point I'll tag it and cut a release. Feel free to play, fork, or experiment.

Using promises

This is very much a work in progress. I am still working out how much I want to explain, in what order, etc. It may be best to just expect that everyone knows what a promise is and not explain anything at all... - Chris

So let's start simply, assuming that while everyone is already familiar with the concept of a promise and how it affects them socially, they may not be clear on how the concept relates to programming and computer science.

It's very likely that you're using something very akin to a promise in your code, and just not considering it as such. At the most basic level, one could conceive of a promise as nothing more than say, a memoized nullary function. One may have thought, "this function involves network access, so let's not call it unless we absolutely need to load this data." The placeholder for the value is the promise, as is the code and any data that would be needed to deliver on it.

There's no free computation involved. To get the value from a promise, the work still has to be done and delivered. But perhaps it can happen in another thread or process while you're working on collating ten thousand similar pieces.

Some languages are built on the concept of the promise and lazy evaluation. Others offer it as an option, but at the syntax level. And still others at least provide an OO representation of the concept in some library. Python doesn't, by default, have any of these.

In this library, the promise isn't necessarily that the passed work will be executed. The promise being made is that the answer or result of a piece of computation will be delivered if asked for. As such, if no code ever attempts to retrieve a promised value, it's perfectly acceptable for there to be no attempt to execute the underlying work. Put another way, promises are not the same as tasks.

Lazy Container

A lazy container is a simple, object-oriented placeholder. It can be created by invoking the lazy function, passing a work function and any arguments it needs. When delivered, the container will call that work and collect the result as its answer. Any further invocations of deliver will return the answer without re-executing the work. However, if an exception is raised by the work during delivery the container will not be considered as delivered. In the case of a transient issue (such as a time-out), delivery can be attempted again until an answer is finally returned.

>>> from promises import lazy, is_delivered, deliver
>>> A = lazy(set, [1, 2, 3])
>>> is_delivered(A)
False
>>> print A
<promises.Container undelivered>
>>> deliver(A)
set([1, 2, 3])
>>> print A
<promises.Container delivered:set([1, 2, 3])>
>>> is_delivered(A)
True

Lazy Proxy

Proxies are a way to consume promises without looking like you're consuming promises. You treat the proxy as though it were the answer itself. A proxy is created by invoking the lazy_proxy function, and passing a work function and any arguments it needs. If your work delivers an int, then treat the proxy like an int. If your work delivers a dictionary, then treat the proxy like it were a dictionary.

>>> from promises import lazy_proxy, is_delivered, promise_repr
>>> B = lazy_proxy(set, [1, 2, 3])
>>> is_delivered(B)
False
>>> print promise_repr(B)
<promises.Proxy undelivered>
>>> print B
set([1, 2, 3])
>>> print promise_repr(B)
<promises.Proxy delivered:set([1, 2, 3])>
>>> is_delivered(B)
True

Proxy Problems

A proxy tries fairly hard to act like the delivered value, by passing along almost every conceivable call to the underlying answer.

However, proxies are still their own type. As such, any code that is written which does a type check will potentially misbehave.

An example of this is the builtin set type. Below we show that the proxy will happily pass the richcompare call along to the underlying set and affirm that A and X are equal. However, reverse the operands and X will first check that the arguments to its richcompare call are another set instance. Since A is not a set (A is an instance of promises.Proxy), X's richcompare immediately returns False, indicating that X and A are not equal.

>>> from promises import lazy_proxy, deliver
>>> A = lazy_proxy(set, [1, 2, 3])
>>> A
set([1, 2, 3])
>>> X = set([1, 2, 3])
>>> X
set([1, 2, 3])
>>> A == X
True
>>> X == A
False
>>> X == deliver(A)
True

Broken Promises

The default behavior of deliver on a promise will allow any raised exception to propagate up. This may be undesireable, so there are three ways to instead gather a BrokenPromise which will wrap any raised exception and be returned as the result.

The functions breakable and breakable_proxy will create a container and proxy promise (respectively) for a piece of work. These functions wrap the work in a try/except clause to catch any exceptions. A promise created with these functions will be considered delivered but broken should it raise during delivery, and will not re-attempt delivery.

As an alternative to creating a breakable promise, the function breakable_deliver attempts delivery on a promise generated from lazy or lazy_proxy. If the promise raises during delivery, a BrokenPromise is generated and returned. However, the promise will not be considered delivered, and any future attempts at delivery will cause the work to be executed again.

Requirements

  • Python 2.6 or later (no support for Python 3 unless someone else wants to hack in all the macros for the proxy code)

In addition, following tools are used in building, testing, or generating documentation from the project sources.

These are all available in most linux distributions (eg. Fedora), and for OSX via MacPorts.

Building

This module uses setuptools, so simply run the following to build the project.

python setup.py build

Testing

Tests are written as unittest test cases. If you'd like to run the tests, simply invoke:

python setup.py test

You may check code coverage via coverage.py, invoked as:

# generates coverage data in .coverage
coverage run --source=promises setup.py test

# creates an html report from the above in htmlcov/index.html
coverage html

I've setup travis-ci and coveralls.io for this project, so tests are run automatically, and coverage is computed then. Results are available online:

Documentation

Documentation is built using Sphinx. Invoking the following will produce HTML documentation in the docs/_build/html directory.

cd docs
make html

Note that you will need the following installed to successfully build the documentation:

Documentation is also available online.

Related

There are multiple alternative implementations following different wavelengths of this concept. Here are some for your perusal.

Author

Christopher O'Brien obriencj@gmail.com

If this project interests you, you can read about more of my hacks and ideas on on my blog.

License

This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org/licenses/.

You can’t perform that action at this time.