├── .flake8
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── docs
├── Makefile
├── _templates
│ └── layout.html
├── conf.py
├── index.rst
├── multiprocess.rst
├── multithread.rst
├── overview.rst
├── promises.rst
└── xmlrpc.rst
├── promises
├── __init__.py
├── multiprocess.py
├── multithread.py
├── proxy.c
└── xmlrpc.py
├── setup.py
└── tests
├── __init__.py
├── multiprocess.py
├── multithread.py
└── xmlrpc.py
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 |
3 | # E251 complains about unexpected spaces around keyword/parameter equals
4 | # E303 complains about more than one blank lines between methods in a class
5 |
6 | ignore = E251,E303
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *#
2 | *.bak
3 | *.egg-info
4 | *.gz
5 | *.pyc
6 | *.so
7 | *.tar
8 | *~
9 | .#*
10 | .DS_Store
11 | .coverage
12 | MANIFEST
13 | _build
14 | _deploy
15 | build
16 | coverage.xml
17 | dist
18 | htmlcov
19 | tmp
20 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "2.7"
4 | - "2.6"
5 | install: pip install coveralls
6 | script: coverage run --source=promises setup.py test
7 |
8 | after_success:
9 | coveralls
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Overview of python-promises
3 |
4 | [](https://travis-ci.org/obriencj/python-promises)
5 | [](https://coveralls.io/r/obriencj/python-promises?branch=master)
6 |
7 | A [Python] module providing container and proxy promises, supporting
8 | delayed linear and multi-processing delivery.
9 |
10 | This is dissimilar to [PEP-3148], where the focus is on a robust
11 | asynchronous delivery framework. We're mostly interested in simple
12 | deferral and most of all, transparent proxies.
13 |
14 | At this stage this project is just a rough draft. I've set the version
15 | to 0.9.0 and am not promising any kind of API stability until 1.0.0 at
16 | which point I'll tag it and cut a release. Feel free to play, fork, or
17 | experiment.
18 |
19 | * [python-promises documentation][docs]
20 | * [python-promises on GitHub][github]
21 | * python-promises not on PyPI until version 1.0.0
22 |
23 | [python]: http://python.org "Python"
24 |
25 | [pep-3148]: http://www.python.org/dev/peps/pep-3148
26 | "PEP-3148 - futures - execute computations asynchronously"
27 |
28 | [docs]: http://obriencj.preoccupied.net/python-promises/
29 |
30 | [github]: https://github.com/obriencj/python-promises/
31 | "python-promises on GitHub"
32 |
33 |
34 | ## Using promises
35 |
36 | *This is very much a work in progress. I am still working out how much
37 | I want to explain, in what order, etc. It may be best to just expect
38 | that everyone knows what a promise is and not explain anything at
39 | all... - Chris*
40 |
41 | So let's start simply, assuming that while everyone is already
42 | familiar with the concept of a [promise][promise-noun] and how it
43 | affects them socially, they may not be clear on how the concept
44 | relates to programming and [computer science][cs-promise].
45 |
46 | It's very likely that you're using something very akin to a promise in
47 | your code, and just not considering it as such. At the most basic
48 | level, one could conceive of a promise as nothing more than say, a
49 | memoized nullary function. One may have thought, "this function
50 | involves network access, so let's not call it unless we absolutely
51 | need to load this data." The placeholder for the value is the promise,
52 | as is the code and any data that would be needed to deliver on it.
53 |
54 | There's no free computation involved. To get the value from a promise,
55 | the work still has to be done and delivered. But perhaps it can happen
56 | in another thread or process while you're working on collating ten
57 | thousand similar pieces.
58 |
59 | Some languages are built on the concept of the promise and lazy
60 | evaluation. Others offer it as an option, but at the syntax level. And
61 | still others at least provide an OO representation of the concept in
62 | some library. Python doesn't, by default, have any of these.
63 |
64 | In this library, the promise isn't necessarily that the passed work
65 | will be executed. The promise being made is that the answer or result
66 | of a piece of computation will be delivered *if asked for*. As such,
67 | if no code ever attempts to retrieve a promised value, it's perfectly
68 | acceptable for there to be no attempt to execute the underlying
69 | work. Put another way, promises are not the same as tasks.
70 |
71 | [promise-noun]: http://en.wiktionary.org/wiki/promise#Noun
72 |
73 | [cs-promise]: http://en.wikipedia.org/wiki/Futures_and_promises
74 | "Futures and Promises"
75 |
76 |
77 | ### Lazy Container
78 |
79 | A lazy container is a simple, object-oriented placeholder. It can be
80 | created by invoking the `lazy` function, passing a work function and
81 | any arguments it needs. When delivered, the container will call that
82 | work and collect the result as its answer. Any further invocations of
83 | deliver will return the answer without re-executing the work. However,
84 | if an exception is raised by the work during delivery the container
85 | will not be considered as delivered. In the case of a transient issue
86 | (such as a time-out), delivery can be attempted again until an answer
87 | is finally returned.
88 |
89 | ```
90 | >>> from promises import lazy, is_delivered, deliver
91 | >>> A = lazy(set, [1, 2, 3])
92 | >>> is_delivered(A)
93 | False
94 | >>> print A
95 |
96 | >>> deliver(A)
97 | set([1, 2, 3])
98 | >>> print A
99 |
100 | >>> is_delivered(A)
101 | True
102 | ```
103 |
104 |
105 | ### Lazy Proxy
106 |
107 | Proxies are a way to consume promises without *looking* like you're
108 | consuming promises. You treat the proxy as though it were the answer
109 | itself. A proxy is created by invoking the `lazy_proxy` function, and
110 | passing a work function and any arguments it needs. If your work
111 | delivers an int, then treat the proxy like an int. If your work
112 | delivers a dictionary, then treat the proxy like it were a dictionary.
113 |
114 | ```
115 | >>> from promises import lazy_proxy, is_delivered, promise_repr
116 | >>> B = lazy_proxy(set, [1, 2, 3])
117 | >>> is_delivered(B)
118 | False
119 | >>> print promise_repr(B)
120 |
121 | >>> print B
122 | set([1, 2, 3])
123 | >>> print promise_repr(B)
124 |
125 | >>> is_delivered(B)
126 | True
127 | ```
128 |
129 |
130 | ### Proxy Problems
131 |
132 | A proxy tries fairly hard to act like the delivered value, by passing
133 | along almost every conceivable call to the underlying answer.
134 |
135 | However, proxies are still their own type. As such, any code
136 | that is written which does a type check will potentially misbehave.
137 |
138 | An example of this is the builtin [set] type. Below we show that the
139 | proxy will happily pass the [richcompare] call along to the underlying
140 | set and affirm that A and X are equal. However, reverse the operands
141 | and X will first [check][set_richcompare] that the arguments to its
142 | richcompare call are another set instance. Since A is not a set (A is
143 | an instance of promises.Proxy), X's richcompare immediately returns
144 | False, indicating that X and A are not equal.
145 |
146 | ```
147 | >>> from promises import lazy_proxy, deliver
148 | >>> A = lazy_proxy(set, [1, 2, 3])
149 | >>> A
150 | set([1, 2, 3])
151 | >>> X = set([1, 2, 3])
152 | >>> X
153 | set([1, 2, 3])
154 | >>> A == X
155 | True
156 | >>> X == A
157 | False
158 | >>> X == deliver(A)
159 | True
160 | ```
161 |
162 | [set]: http://docs.python.org/2/library/stdtypes.html#set-types-set-frozenset
163 | "5.7. Set Types - set, frozenset"
164 |
165 | [richcompare]: http://docs.python.org/2/c-api/typeobj.html#PyTypeObject.tp_richcompare
166 |
167 | [set_richcompare]: http://hg.python.org/cpython/file/779de7b4909b/Objects/setobject.c#l1794
168 |
169 |
170 | ### Broken Promises
171 |
172 | The default behavior of `deliver` on a promise will allow any raised
173 | exception to propagate up. This may be undesireable, so there are
174 | three ways to instead gather a `BrokenPromise` which will wrap any
175 | raised exception and be returned as the result.
176 |
177 | The functions `breakable` and `breakable_proxy` will create a
178 | container and proxy promise (respectively) for a piece of work. These
179 | functions wrap the work in a try/except clause to catch any
180 | exceptions. A promise created with these functions will be considered
181 | delivered but broken should it raise during delivery, and will not
182 | re-attempt delivery.
183 |
184 | As an alternative to creating a breakable promise, the function
185 | `breakable_deliver` attempts delivery on a promise generated from
186 | `lazy` or `lazy_proxy`. If the promise raises during delivery, a
187 | `BrokenPromise` is generated and returned. However, the promise will
188 | not be considered delivered, and any future attempts at delivery will
189 | cause the work to be executed again.
190 |
191 |
192 | ## Requirements
193 |
194 | * [Python] 2.6 or later (no support for Python 3 unless someone else
195 | wants to hack in all the macros for the proxy code)
196 |
197 | In addition, following tools are used in building, testing, or
198 | generating documentation from the project sources.
199 |
200 | * [Setuptools]
201 | * [Coverage.py]
202 | * [GNU Make]
203 | * [Pandoc]
204 | * [Sphinx]
205 |
206 | These are all available in most linux distributions (eg. [Fedora]), and
207 | for OSX via [MacPorts].
208 |
209 | [setuptools]: http://pythonhosted.org/setuptools/
210 |
211 | [coverage.py]: http://nedbatchelder.com/code/coverage/
212 |
213 | [gnu make]: http://www.gnu.org/software/make/
214 |
215 | [pandoc]: http://johnmacfarlane.net/pandoc/
216 |
217 | [sphinx]: http://sphinx-doc.org/
218 |
219 | [fedora]: http://fedoraproject.org/
220 |
221 | [macports]: http://www.macports.org/
222 |
223 |
224 | ## Building
225 |
226 | This module uses [setuptools], so simply run the following to build
227 | the project.
228 |
229 | ```bash
230 | python setup.py build
231 | ```
232 |
233 |
234 | ### Testing
235 |
236 | Tests are written as `unittest` test cases. If you'd like to run the
237 | tests, simply invoke:
238 |
239 | ```bash
240 | python setup.py test
241 | ```
242 |
243 | You may check code coverage via [coverage.py], invoked as:
244 |
245 | ```bash
246 | # generates coverage data in .coverage
247 | coverage run --source=promises setup.py test
248 |
249 | # creates an html report from the above in htmlcov/index.html
250 | coverage html
251 | ```
252 |
253 | I've setup [travis-ci] and [coveralls.io] for this project, so tests
254 | are run automatically, and coverage is computed then. Results are
255 | available online:
256 |
257 | * [python-promises on Travis-CI][promises-travis]
258 | * [python-promises on Coveralls.io][promises-coveralls]
259 |
260 | [travis-ci]: https://travis-ci.org
261 |
262 | [coveralls.io]: https://coveralls.io
263 |
264 | [promises-travis]: https://travis-ci.org/obriencj/python-promises
265 |
266 | [promises-coveralls]: https://coveralls.io/r/obriencj/python-promises
267 |
268 |
269 | ### Documentation
270 |
271 | Documentation is built using [Sphinx]. Invoking the following will
272 | produce HTML documentation in the `docs/_build/html` directory.
273 |
274 | ```bash
275 | cd docs
276 | make html
277 | ```
278 |
279 | Note that you will need the following installed to successfully build
280 | the documentation:
281 |
282 | Documentation is [also available online][docs].
283 |
284 |
285 | ## Related
286 |
287 | There are multiple alternative implementations following different
288 | wavelengths of this concept. Here are some for your perusal.
289 |
290 | * [concurrent.futures] - [Python 3.4] includes [PEP-3148]
291 | * [futureutils] - Introduces futures and promises into iterators
292 | * [aplus] - Promises/A+ specification in Python
293 | * [promised] - Python "promise" for output of asynchronous operations,
294 | and callback chaining.
295 |
296 | [concurrent.futures]: http://docs.python.org/dev/library/concurrent.futures.html
297 | [futureutils]: https://pypi.python.org/pypi/futureutils
298 | [aplus]: https://github.com/xogeny/aplus
299 | [promised]: https://code.google.com/p/promised/
300 | [python 3.4]: http://docs.python.org/dev/whatsnew/3.4.html
301 |
302 |
303 | ## Author
304 |
305 | Christopher O'Brien
306 |
307 | If this project interests you, you can read about more of my hacks and
308 | ideas on [on my blog](http://obriencj.preoccupied.net).
309 |
310 |
311 | ## License
312 |
313 | This library is free software; you can redistribute it and/or modify
314 | it under the terms of the GNU Lesser General Public License as
315 | published by the Free Software Foundation; either version 3 of the
316 | License, or (at your option) any later version.
317 |
318 | This library is distributed in the hope that it will be useful, but
319 | WITHOUT ANY WARRANTY; without even the implied warranty of
320 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
321 | Lesser General Public License for more details.
322 |
323 | You should have received a copy of the GNU Lesser General Public
324 | License along with this library; if not, see
325 | .
326 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | SHELL = /bin/bash
5 |
6 | # You can set these variables from the command line.
7 | SPHINXOPTS =
8 | SPHINXBUILD = sphinx-build
9 | PAPER =
10 | BUILDDIR = _build
11 |
12 | PYTHONPATH := `echo ../build/lib*`
13 | SPHINXCMD := PYTHONPATH=${PYTHONPATH} $(SPHINXBUILD)
14 |
15 | DEPLOYDIR = _deploy
16 | DEPLOYFORMAT = dirhtml
17 | DEPLOYREPO = git@github.com:/obriencj/python-promises
18 | DEPLOYBRANCH = gh-pages
19 |
20 |
21 | # User-friendly check for sphinx-build
22 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
23 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
24 | endif
25 |
26 | # Internal variables.
27 | PAPEROPT_a4 = -D latex_paper_size=a4
28 | PAPEROPT_letter = -D latex_paper_size=letter
29 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
30 | # the i18n builder cannot share the environment and doctrees with the others
31 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
32 |
33 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext deploy
34 |
35 | help:
36 | @echo "Please use \`make ' where is one of"
37 | @echo " deploy to run $(DEPLOYFORMAT) and push to the $(DEPLOYBRANCH) branch under $(DEPLOYDIR)"
38 | @echo " html to make standalone HTML files"
39 | @echo " dirhtml to make HTML files named index.html in directories"
40 | @echo " singlehtml to make a single large HTML file"
41 | @echo " pickle to make pickle files"
42 | @echo " json to make JSON files"
43 | @echo " htmlhelp to make HTML files and a HTML help project"
44 | @echo " qthelp to make HTML files and a qthelp project"
45 | @echo " devhelp to make HTML files and a Devhelp project"
46 | @echo " epub to make an epub"
47 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
48 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
49 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
50 | @echo " text to make text files"
51 | @echo " man to make manual pages"
52 | @echo " texinfo to make Texinfo files"
53 | @echo " info to make Texinfo files and run them through makeinfo"
54 | @echo " gettext to make PO message catalogs"
55 | @echo " changes to make an overview of all changed/added/deprecated items"
56 | @echo " xml to make Docutils-native XML files"
57 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
58 | @echo " linkcheck to check all external links for integrity"
59 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
60 |
61 | clean:
62 | rm -rf $(BUILDDIR)/*
63 |
64 | html: build-project
65 | $(SPHINXCMD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
66 | @echo
67 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
68 |
69 | dirhtml: build-project
70 | @${SPHINXCMD} -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
71 | @echo
72 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
73 |
74 | singlehtml: build-project
75 | $(SPHINXCMD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
76 | @echo
77 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
78 |
79 | pickle: build-project
80 | $(SPHINXCMD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
81 | @echo
82 | @echo "Build finished; now you can process the pickle files."
83 |
84 | json: build-project
85 | $(SPHINXCMD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
86 | @echo
87 | @echo "Build finished; now you can process the JSON files."
88 |
89 | htmlhelp: build-project
90 | $(SPHINXCMD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
91 | @echo
92 | @echo "Build finished; now you can run HTML Help Workshop with the" \
93 | ".hhp project file in $(BUILDDIR)/htmlhelp."
94 |
95 | qthelp: build-project
96 | $(SPHINXCMD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
97 | @echo
98 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
99 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
100 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/brine.qhcp"
101 | @echo "To view the help file:"
102 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/brine.qhc"
103 |
104 | devhelp: build-project
105 | $(SPHINXCMD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
106 | @echo
107 | @echo "Build finished."
108 | @echo "To view the help file:"
109 | @echo "# mkdir -p $$HOME/.local/share/devhelp/brine"
110 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/brine"
111 | @echo "# devhelp"
112 |
113 | epub: build-project
114 | $(SPHINXCMD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
115 | @echo
116 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
117 |
118 | latex: build-project
119 | $(SPHINXCMD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
120 | @echo
121 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
122 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
123 | "(use \`make latexpdf' here to do that automatically)."
124 |
125 | latexpdf: build-project
126 | $(SPHINXCMD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
127 | @echo "Running LaTeX files through pdflatex..."
128 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
129 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
130 |
131 | latexpdfja: build-project
132 | $(SPHINXCMD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
133 | @echo "Running LaTeX files through platex and dvipdfmx..."
134 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
135 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
136 |
137 | text: build-project
138 | $(SPHINXCMD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
139 | @echo
140 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
141 |
142 | man: build-project
143 | $(SPHINXCMD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
144 | @echo
145 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
146 |
147 | texinfo: build-project
148 | $(SPHINXCMD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
149 | @echo
150 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
151 | @echo "Run \`make' in that directory to run these through makeinfo" \
152 | "(use \`make info' here to do that automatically)."
153 |
154 | info: build-project
155 | $(SPHINXCMD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
156 | @echo "Running Texinfo files through makeinfo..."
157 | make -C $(BUILDDIR)/texinfo info
158 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
159 |
160 | gettext: build-project
161 | $(SPHINXCMD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
162 | @echo
163 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
164 |
165 | changes: build-project
166 | $(SPHINXCMD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
167 | @echo
168 | @echo "The overview file is in $(BUILDDIR)/changes."
169 |
170 | linkcheck: build-project
171 | $(SPHINXCMD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
172 | @echo
173 | @echo "Link check complete; look for any errors in the above output " \
174 | "or in $(BUILDDIR)/linkcheck/output.txt."
175 |
176 | doctest: build-project
177 | $(SPHINXCMD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
178 | @echo "Testing of doctests in the sources finished, look at the " \
179 | "results in $(BUILDDIR)/doctest/output.txt."
180 |
181 | xml: build-project
182 | $(SPHINXCMD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
183 | @echo
184 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
185 |
186 | pseudoxml: build-project
187 | $(SPHINXCMD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
188 | @echo
189 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
190 |
191 | dirs: _static _templates $(DEPLOYDIR)
192 |
193 | _static _templates $(DEPLOYDIR):
194 | mkdir -p $@
195 |
196 | build-project:
197 | pushd ../ >/dev/null ;\
198 | rm -rf build/* ;\
199 | ./setup.py clean build ;\
200 | popd
201 |
202 | overview.rst: ../README.md
203 | sed 's/^\[\!.*/ /g' ../README.md > overview.md
204 | pandoc --from=markdown --to=rst -o "overview.rst" "overview.md"
205 | rm -f overview.md
206 |
207 | generate: build-project overview.rst $(DEPLOYFORMAT)
208 |
209 | # clones the appropriate git repo and branch in the deploy directory
210 | # if it isn't already setup
211 | setup-stage: $(DEPLOYDIR)
212 | if ! [ -d "$(DEPLOYDIR)/.git" ] ; then \
213 | git clone $(DEPLOYREPO) $(DEPLOYDIR) \
214 | --single-branch \
215 | --branch $(DEPLOYBRANCH) ;\
216 | fi ;\
217 |
218 | # clears out the old contents of _deploy so that we can replace them
219 | # with our newly generated version
220 | clean-stage: setup-stage
221 | pushd $(DEPLOYDIR) ;\
222 | git checkout --force $(DEPLOYBRANCH) ;\
223 | git pull ;\
224 | git rm -rf * ;\
225 | rm -rf * ;\
226 | touch .nojekyll ; git add .nojekyll ;\
227 | popd
228 |
229 | stage: generate clean-stage
230 | cp -r "$(BUILDDIR)/$(DEPLOYFORMAT)/"* $(DEPLOYDIR)
231 | pushd $(DEPLOYDIR) ;\
232 | git add . ;\
233 | popd
234 |
235 | # builds the selected format of docs, and checks them into the right
236 | # branch on our repo.
237 | deploy: stage
238 | pushd $(DEPLOYDIR) ;\
239 | git commit -m "deploying sphinx update" ;\
240 | git push ;\
241 | popd
242 |
--------------------------------------------------------------------------------
/docs/_templates/layout.html:
--------------------------------------------------------------------------------
1 | {% extends "!layout.html" %}
2 |
3 | {% block footer %}
4 | {{ super() }}
5 |
9 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # brine documentation build configuration file, created by
4 | # sphinx-quickstart on Sun Feb 9 11:15:28 2014.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | import sys
16 | import os
17 |
18 | # If extensions (or modules to document with autodoc) are in another directory,
19 | # add these directories to sys.path here. If the directory is relative to the
20 | # documentation root, use os.path.abspath to make it absolute, like shown here.
21 | #sys.path.insert(0, os.path.abspath('.'))
22 | #sys.path.insert(0, os.path.abspath('../'))
23 |
24 | # -- General configuration ------------------------------------------------
25 |
26 | # If your documentation needs a minimal Sphinx version, state it here.
27 | #needs_sphinx = '1.0'
28 |
29 | # Add any Sphinx extension module names here, as strings. They can be
30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
31 | # ones.
32 | extensions = [
33 | 'sphinx.ext.autodoc',
34 | 'sphinx.ext.viewcode',
35 | 'sphinx.ext.intersphinx',
36 | 'numpydoc',
37 | ]
38 |
39 | intersphinx_mapping = {
40 | "python": ('http://docs.python.org/2', None),
41 | }
42 |
43 | # Add any paths that contain templates here, relative to this directory.
44 | templates_path = ['_templates']
45 |
46 | # The suffix of source filenames.
47 | source_suffix = '.rst'
48 |
49 | # The encoding of source files.
50 | #source_encoding = 'utf-8-sig'
51 |
52 | # The master toctree document.
53 | master_doc = 'index'
54 |
55 | # General information about the project.
56 | project = u'promises'
57 | copyright = u"2014, Christopher O'Brien"
58 |
59 | # The version info for the project you're documenting, acts as replacement for
60 | # |version| and |release|, also used in various other places throughout the
61 | # built documents.
62 | #
63 | # The short X.Y version.
64 | version = '0.9'
65 | # The full version, including alpha/beta/rc tags.
66 | release = '0.9.0'
67 |
68 | # The language for content autogenerated by Sphinx. Refer to documentation
69 | # for a list of supported languages.
70 | language = 'English'
71 |
72 | # There are two options for replacing |today|: either, you set today to some
73 | # non-false value, then it is used:
74 | #today = ''
75 | # Else, today_fmt is used as the format for a strftime call.
76 | #today_fmt = '%B %d, %Y'
77 |
78 | # List of patterns, relative to source directory, that match files and
79 | # directories to ignore when looking for source files.
80 | exclude_patterns = ['_build']
81 |
82 | # The reST default role (used for this markup: `text`) to use for all
83 | # documents.
84 | #default_role = None
85 |
86 | # If true, '()' will be appended to :func: etc. cross-reference text.
87 | add_function_parentheses = True
88 |
89 | # If true, the current module name will be prepended to all description
90 | # unit titles (such as .. function::).
91 | #add_module_names = True
92 |
93 | # If true, sectionauthor and moduleauthor directives will be shown in the
94 | # output. They are ignored by default.
95 | #show_authors = False
96 |
97 | # The name of the Pygments (syntax highlighting) style to use.
98 | pygments_style = 'sphinx'
99 |
100 | # A list of ignored prefixes for module index sorting.
101 | #modindex_common_prefix = []
102 |
103 | # If true, keep warnings as "system message" paragraphs in the built documents.
104 | #keep_warnings = False
105 |
106 |
107 | # -- Options for HTML output ----------------------------------------------
108 |
109 | # The theme to use for HTML and HTML Help pages. See the documentation for
110 | # a list of builtin themes.
111 | html_theme = 'default'
112 |
113 | # Theme options are theme-specific and customize the look and feel of a theme
114 | # further. For a list of options available for each theme, see the
115 | # documentation.
116 | #html_theme_options = {}
117 |
118 | # Add any paths that contain custom themes here, relative to this directory.
119 | #html_theme_path = []
120 |
121 | # The name for this set of Sphinx documents. If None, it defaults to
122 | # " v documentation".
123 | #html_title = None
124 |
125 | # A shorter title for the navigation bar. Default is the same as html_title.
126 | #html_short_title = None
127 |
128 | # The name of an image file (relative to this directory) to place at the top
129 | # of the sidebar.
130 | #html_logo = None
131 |
132 | # The name of an image file (within the static path) to use as favicon of the
133 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
134 | # pixels large.
135 | #html_favicon = None
136 |
137 | # Add any paths that contain custom static files (such as style sheets) here,
138 | # relative to this directory. They are copied after the builtin static files,
139 | # so a file named "default.css" will overwrite the builtin "default.css".
140 | html_static_path = []
141 |
142 | # Add any extra paths that contain custom files (such as robots.txt or
143 | # .htaccess) here, relative to this directory. These files are copied
144 | # directly to the root of the documentation.
145 | #html_extra_path = []
146 |
147 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
148 | # using the given strftime format.
149 | #html_last_updated_fmt = '%b %d, %Y'
150 |
151 | # If true, SmartyPants will be used to convert quotes and dashes to
152 | # typographically correct entities.
153 | #html_use_smartypants = True
154 |
155 | # Custom sidebar templates, maps document names to template names.
156 | #html_sidebars = {}
157 |
158 | # Additional templates that should be rendered to pages, maps page names to
159 | # template names.
160 | #html_additional_pages = {}
161 |
162 | # If false, no module index is generated.
163 | html_domain_indices = True
164 |
165 | # If false, no index is generated.
166 | html_use_index = True
167 |
168 | # If true, the index is split into individual pages for each letter.
169 | #html_split_index = False
170 |
171 | # If true, links to the reST sources are added to the pages.
172 | #html_show_sourcelink = True
173 |
174 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
175 | #html_show_sphinx = True
176 |
177 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
178 | #html_show_copyright = True
179 |
180 | # If true, an OpenSearch description file will be output, and all pages will
181 | # contain a tag referring to it. The value of this option must be the
182 | # base URL from which the finished HTML is served.
183 | #html_use_opensearch = ''
184 |
185 | # This is the file name suffix for HTML files (e.g. ".xhtml").
186 | #html_file_suffix = None
187 |
188 | # Output file base name for HTML help builder.
189 | htmlhelp_basename = 'brinedoc'
190 |
191 |
192 | # -- Options for LaTeX output ---------------------------------------------
193 |
194 | latex_elements = {
195 | # The paper size ('letterpaper' or 'a4paper').
196 | #'papersize': 'letterpaper',
197 |
198 | # The font size ('10pt', '11pt' or '12pt').
199 | #'pointsize': '10pt',
200 |
201 | # Additional stuff for the LaTeX preamble.
202 | #'preamble': '',
203 | }
204 |
205 | # Grouping the document tree into LaTeX files. List of tuples
206 | # (source start file, target name, title,
207 | # author, documentclass [howto, manual, or own class]).
208 | latex_documents = [
209 | ('index', 'promises.tex', u'promises Documentation',
210 | u'Author', 'manual'),
211 | ]
212 |
213 | # The name of an image file (relative to this directory) to place at the top of
214 | # the title page.
215 | #latex_logo = None
216 |
217 | # For "manual" documents, if this is true, then toplevel headings are parts,
218 | # not chapters.
219 | #latex_use_parts = False
220 |
221 | # If true, show page references after internal links.
222 | #latex_show_pagerefs = False
223 |
224 | # If true, show URL addresses after external links.
225 | #latex_show_urls = False
226 |
227 | # Documents to append as an appendix to all manuals.
228 | #latex_appendices = []
229 |
230 | # If false, no module index is generated.
231 | #latex_domain_indices = True
232 |
233 |
234 | # -- Options for manual page output ---------------------------------------
235 |
236 | # One entry per manual page. List of tuples
237 | # (source start file, name, description, authors, manual section).
238 | man_pages = [
239 | ('index', 'promises', u'promises Documentation',
240 | [u'Author'], 1)
241 | ]
242 |
243 | # If true, show URL addresses after external links.
244 | #man_show_urls = False
245 |
246 |
247 | # -- Options for Texinfo output -------------------------------------------
248 |
249 | # Grouping the document tree into Texinfo files. List of tuples
250 | # (source start file, target name, title, author,
251 | # dir menu entry, description, category)
252 | texinfo_documents = [
253 | ('index', 'promises', u'promises Documentation',
254 | u'Author', 'promises', 'One line description of project.',
255 | 'Miscellaneous'),
256 | ]
257 |
258 | # Documents to append as an appendix to all manuals.
259 | #texinfo_appendices = []
260 |
261 | # If false, no module index is generated.
262 | #texinfo_domain_indices = True
263 |
264 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
265 | #texinfo_show_urls = 'footnote'
266 |
267 | # If true, do not generate a @detailmenu in the "Top" node's menu.
268 | #texinfo_no_detailmenu = False
269 |
270 |
271 | # -- Options for Epub output ----------------------------------------------
272 |
273 | # Bibliographic Dublin Core info.
274 | epub_title = u'promises'
275 | epub_author = u"Christopher O'Brien"
276 | epub_publisher = u"Christopher O'Brien"
277 | epub_copyright = u"2014, Christopher O'Brien"
278 |
279 | # The basename for the epub file. It defaults to the project name.
280 | #epub_basename = u'promises'
281 |
282 | # The HTML theme for the epub output. Since the default themes are not optimized
283 | # for small screen space, using the same theme for HTML and epub output is
284 | # usually not wise. This defaults to 'epub', a theme designed to save visual
285 | # space.
286 | #epub_theme = 'epub'
287 |
288 | # The language of the text. It defaults to the language option
289 | # or en if the language is not set.
290 | #epub_language = ''
291 |
292 | # The scheme of the identifier. Typical schemes are ISBN or URL.
293 | #epub_scheme = ''
294 |
295 | # The unique identifier of the text. This can be a ISBN number
296 | # or the project homepage.
297 | #epub_identifier = ''
298 |
299 | # A unique identification for the text.
300 | #epub_uid = ''
301 |
302 | # A tuple containing the cover image and cover page html template filenames.
303 | #epub_cover = ()
304 |
305 | # A sequence of (type, uri, title) tuples for the guide element of content.opf.
306 | #epub_guide = ()
307 |
308 | # HTML files that should be inserted before the pages created by sphinx.
309 | # The format is a list of tuples containing the path and title.
310 | #epub_pre_files = []
311 |
312 | # HTML files shat should be inserted after the pages created by sphinx.
313 | # The format is a list of tuples containing the path and title.
314 | #epub_post_files = []
315 |
316 | # A list of files that should not be packed into the epub file.
317 | epub_exclude_files = ['search.html']
318 |
319 | # The depth of the table of contents in toc.ncx.
320 | #epub_tocdepth = 3
321 |
322 | # Allow duplicate toc entries.
323 | #epub_tocdup = True
324 |
325 | # Choose between 'default' and 'includehidden'.
326 | #epub_tocscope = 'default'
327 |
328 | # Fix unsupported image types using the PIL.
329 | #epub_fix_images = False
330 |
331 | # Scale large images.
332 | #epub_max_image_width = 0
333 |
334 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
335 | #epub_show_urls = 'inline'
336 |
337 | # If false, no index is generated.
338 | #epub_use_index = True
339 |
340 | # autoclass_content = 'both'
341 |
342 | numpydoc_show_class_members = False
343 | numpydoc_class_members_toctree = False
344 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. promises documentation master file, created by
2 | sphinx-quickstart on Tue Feb 18 10:44:58 2014.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Promises -- Transparent proxies for future values
7 | =================================================
8 |
9 | .. toctree::
10 | :maxdepth: 4
11 |
12 | overview
13 | promises
14 | multiprocess
15 | multithread
16 | xmlrpc
17 |
18 |
19 | Indices and tables
20 | ==================
21 |
22 | * :ref:`genindex`
23 | * :ref:`modindex`
24 | * :ref:`search`
25 |
--------------------------------------------------------------------------------
/docs/multiprocess.rst:
--------------------------------------------------------------------------------
1 | Module promises.multiprocess
2 | ============================
3 |
4 | .. automodule:: promises.multiprocess
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/multithread.rst:
--------------------------------------------------------------------------------
1 | Module promises.multithread
2 | ===========================
3 |
4 | .. automodule:: promises.multithread
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/overview.rst:
--------------------------------------------------------------------------------
1 | Overview of python-promises
2 | ===========================
3 |
4 | A `Python `__ module providing container and proxy
5 | promises, supporting delayed linear and multi-processing delivery.
6 |
7 | This is dissimilar to
8 | `PEP-3148 `__, where the focus
9 | is on a robust asynchronous delivery framework. We're mostly interested
10 | in simple deferral and most of all, transparent proxies.
11 |
12 | At this stage this project is just a rough draft. I've set the version
13 | to 0.9.0 and am not promising any kind of API stability until 1.0.0 at
14 | which point I'll tag it and cut a release. Feel free to play, fork, or
15 | experiment.
16 |
17 | - `python-promises
18 | documentation `__
19 | - `python-promises on
20 | GitHub `__
21 | - python-promises not on PyPI until version 1.0.0
22 |
23 | Using promises
24 | --------------
25 |
26 | *This is very much a work in progress. I am still working out how much I
27 | want to explain, in what order, etc. It may be best to just expect that
28 | everyone knows what a promise is and not explain anything at all... -
29 | Chris*
30 |
31 | So let's start simply, assuming that while everyone is already familiar
32 | with the concept of a
33 | `promise `__ and how it
34 | affects them socially, they may not be clear on how the concept relates
35 | to programming and `computer
36 | science `__.
37 |
38 | It's very likely that you're using something very akin to a promise in
39 | your code, and just not considering it as such. At the most basic level,
40 | one could conceive of a promise as nothing more than say, a memoized
41 | nullary function. One may have thought, "this function involves network
42 | access, so let's not call it unless we absolutely need to load this
43 | data." The placeholder for the value is the promise, as is the code and
44 | any data that would be needed to deliver on it.
45 |
46 | There's no free computation involved. To get the value from a promise,
47 | the work still has to be done and delivered. But perhaps it can happen
48 | in another thread or process while you're working on collating ten
49 | thousand similar pieces.
50 |
51 | Some languages are built on the concept of the promise and lazy
52 | evaluation. Others offer it as an option, but at the syntax level. And
53 | still others at least provide an OO representation of the concept in
54 | some library. Python doesn't, by default, have any of these.
55 |
56 | In this library, the promise isn't necessarily that the passed work will
57 | be executed. The promise being made is that the answer or result of a
58 | piece of computation will be delivered *if asked for*. As such, if no
59 | code ever attempts to retrieve a promised value, it's perfectly
60 | acceptable for there to be no attempt to execute the underlying work.
61 | Put another way, promises are not the same as tasks.
62 |
63 | Lazy Container
64 | ~~~~~~~~~~~~~~
65 |
66 | A lazy container is a simple, object-oriented placeholder. It can be
67 | created by invoking the ``lazy`` function, passing a work function and
68 | any arguments it needs. When delivered, the container will call that
69 | work and collect the result as its answer. Any further invocations of
70 | deliver will return the answer without re-executing the work. However,
71 | if an exception is raised by the work during delivery the container will
72 | not be considered as delivered. In the case of a transient issue (such
73 | as a time-out), delivery can be attempted again until an answer is
74 | finally returned.
75 |
76 | ::
77 |
78 | >>> from promises import lazy, is_delivered, deliver
79 | >>> A = lazy(set, [1, 2, 3])
80 | >>> is_delivered(A)
81 | False
82 | >>> print A
83 |
84 | >>> deliver(A)
85 | set([1, 2, 3])
86 | >>> print A
87 |
88 | >>> is_delivered(A)
89 | True
90 |
91 | Lazy Proxy
92 | ~~~~~~~~~~
93 |
94 | Proxies are a way to consume promises without *looking* like you're
95 | consuming promises. You treat the proxy as though it were the answer
96 | itself. A proxy is created by invoking the ``lazy_proxy`` function, and
97 | passing a work function and any arguments it needs. If your work
98 | delivers an int, then treat the proxy like an int. If your work delivers
99 | a dictionary, then treat the proxy like it were a dictionary.
100 |
101 | ::
102 |
103 | >>> from promises import lazy_proxy, is_delivered, promise_repr
104 | >>> B = lazy_proxy(set, [1, 2, 3])
105 | >>> is_delivered(B)
106 | False
107 | >>> print promise_repr(B)
108 |
109 | >>> print B
110 | set([1, 2, 3])
111 | >>> print promise_repr(B)
112 |
113 | >>> is_delivered(B)
114 | True
115 |
116 | Proxy Problems
117 | ~~~~~~~~~~~~~~
118 |
119 | A proxy tries fairly hard to act like the delivered value, by passing
120 | along almost every conceivable call to the underlying answer.
121 |
122 | However, proxies are still their own type. As such, any code that is
123 | written which does a type check will potentially misbehave.
124 |
125 | An example of this is the builtin
126 | `set `__
127 | type. Below we show that the proxy will happily pass the
128 | `richcompare `__
129 | call along to the underlying set and affirm that A and X are equal.
130 | However, reverse the operands and X will first
131 | `check `__
132 | that the arguments to its richcompare call are another set instance.
133 | Since A is not a set (A is an instance of promises.Proxy), X's
134 | richcompare immediately returns False, indicating that X and A are not
135 | equal.
136 |
137 | ::
138 |
139 | >>> from promises import lazy_proxy, deliver
140 | >>> A = lazy_proxy(set, [1, 2, 3])
141 | >>> A
142 | set([1, 2, 3])
143 | >>> X = set([1, 2, 3])
144 | >>> X
145 | set([1, 2, 3])
146 | >>> A == X
147 | True
148 | >>> X == A
149 | False
150 | >>> X == deliver(A)
151 | True
152 |
153 | Broken Promises
154 | ~~~~~~~~~~~~~~~
155 |
156 | The default behavior of ``deliver`` on a promise will allow any raised
157 | exception to propagate up. This may be undesireable, so there are three
158 | ways to instead gather a ``BrokenPromise`` which will wrap any raised
159 | exception and be returned as the result.
160 |
161 | The functions ``breakable`` and ``breakable_proxy`` will create a
162 | container and proxy promise (respectively) for a piece of work. These
163 | functions wrap the work in a try/except clause to catch any exceptions.
164 | A promise created with these functions will be considered delivered but
165 | broken should it raise during delivery, and will not re-attempt
166 | delivery.
167 |
168 | As an alternative to creating a breakable promise, the function
169 | ``breakable_deliver`` attempts delivery on a promise generated from
170 | ``lazy`` or ``lazy_proxy``. If the promise raises during delivery, a
171 | ``BrokenPromise`` is generated and returned. However, the promise will
172 | not be considered delivered, and any future attempts at delivery will
173 | cause the work to be executed again.
174 |
175 | Requirements
176 | ------------
177 |
178 | - `Python `__ 2.6 or later (no support for Python 3
179 | unless someone else wants to hack in all the macros for the proxy
180 | code)
181 |
182 | In addition, following tools are used in building, testing, or
183 | generating documentation from the project sources.
184 |
185 | - `Setuptools `__
186 | - `Coverage.py `__
187 | - `GNU Make `__
188 | - `Pandoc `__
189 | - `Sphinx `__
190 |
191 | These are all available in most linux distributions (eg.
192 | `Fedora `__), and for OSX via
193 | `MacPorts `__.
194 |
195 | Building
196 | --------
197 |
198 | This module uses `setuptools `__,
199 | so simply run the following to build the project.
200 |
201 | .. code:: bash
202 |
203 | python setup.py build
204 |
205 | Testing
206 | ~~~~~~~
207 |
208 | Tests are written as ``unittest`` test cases. If you'd like to run the
209 | tests, simply invoke:
210 |
211 | .. code:: bash
212 |
213 | python setup.py test
214 |
215 | You may check code coverage via
216 | `coverage.py `__, invoked as:
217 |
218 | .. code:: bash
219 |
220 | # generates coverage data in .coverage
221 | coverage run --source=promises setup.py test
222 |
223 | # creates an html report from the above in htmlcov/index.html
224 | coverage html
225 |
226 | I've setup `travis-ci `__ and
227 | `coveralls.io `__ for this project, so tests are
228 | run automatically, and coverage is computed then. Results are available
229 | online:
230 |
231 | - `python-promises on
232 | Travis-CI `__
233 | - `python-promises on
234 | Coveralls.io `__
235 |
236 | Documentation
237 | ~~~~~~~~~~~~~
238 |
239 | Documentation is built using `Sphinx `__.
240 | Invoking the following will produce HTML documentation in the
241 | ``docs/_build/html`` directory.
242 |
243 | .. code:: bash
244 |
245 | cd docs
246 | make html
247 |
248 | Note that you will need the following installed to successfully build
249 | the documentation:
250 |
251 | Documentation is `also available
252 | online `__.
253 |
254 | Related
255 | -------
256 |
257 | There are multiple alternative implementations following different
258 | wavelengths of this concept. Here are some for your perusal.
259 |
260 | - `concurrent.futures `__
261 | - `Python 3.4 `__
262 | includes `PEP-3148 `__
263 | - `futureutils `__ -
264 | Introduces futures and promises into iterators
265 | - `aplus `__ - Promises/A+
266 | specification in Python
267 | - `promised `__ - Python "promise"
268 | for output of asynchronous operations, and callback chaining.
269 |
270 | Author
271 | ------
272 |
273 | Christopher O'Brien obriencj@gmail.com
274 |
275 | If this project interests you, you can read about more of my hacks and
276 | ideas on `on my blog `__.
277 |
278 | License
279 | -------
280 |
281 | This library is free software; you can redistribute it and/or modify it
282 | under the terms of the GNU Lesser General Public License as published by
283 | the Free Software Foundation; either version 3 of the License, or (at
284 | your option) any later version.
285 |
286 | This library is distributed in the hope that it will be useful, but
287 | WITHOUT ANY WARRANTY; without even the implied warranty of
288 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
289 | General Public License for more details.
290 |
291 | You should have received a copy of the GNU Lesser General Public License
292 | along with this library; if not, see http://www.gnu.org/licenses/.
293 |
--------------------------------------------------------------------------------
/docs/promises.rst:
--------------------------------------------------------------------------------
1 | Module promises
2 | ===============
3 |
4 | .. automodule:: promises
5 | :show-inheritance:
6 |
7 | General
8 | -------
9 | .. autofunction:: promises.deliver
10 | .. autofunction:: promises.is_delivered
11 | .. autofunction:: promises.is_promise
12 | .. autofunction:: promises.promise_repr
13 | .. autofunction:: promises.breakable_deliver
14 |
15 | .. autoclass:: promises.BrokenPromise
16 | :show-inheritance:
17 | .. autoclass:: promises.PromiseAlreadyDelivered
18 | :show-inheritance:
19 | .. autoclass:: promises.PromiseNotReady
20 | :show-inheritance:
21 |
22 | Container Promises
23 | -------------------
24 | .. autofunction:: promises.lazy
25 | .. autofunction:: promises.breakable
26 | .. autofunction:: promises.promise
27 |
28 | Proxy Promises
29 | --------------
30 | .. autofunction:: promises.lazy_proxy
31 | .. autofunction:: promises.breakable_proxy
32 | .. autofunction:: promises.promise_proxy
33 |
--------------------------------------------------------------------------------
/docs/xmlrpc.rst:
--------------------------------------------------------------------------------
1 | Module promises.xmlrpc
2 | ======================
3 |
4 | .. automodule:: promises.xmlrpc
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 |
10 | See Also
11 | --------
12 | :mod:`xmlrpclib`, :class:`xmlrpclib.MultiCall`, :class:`xmlrpclib.Server`
13 |
--------------------------------------------------------------------------------
/promises/__init__.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | Promises for Python
18 |
19 | Yet another module for doing promises in python! This time with
20 | transparent proxies, and other convoluted stuff that will make you
21 | wish someone smarter had worked on this.
22 |
23 | :author: Christopher O'Brien
24 | :license: LGPL v.3
25 | """
26 |
27 |
28 | from ._proxy import Proxy
29 | from ._proxy import is_proxy, is_proxy_delivered, deliver_proxy
30 | from functools import partial
31 | from sys import exc_info
32 | from threading import Event
33 |
34 |
35 | __all__ = ('Container', 'Proxy', 'BrokenPromise',
36 | 'lazy', 'lazy_proxy',
37 | 'promise', 'promise_proxy',
38 | 'breakable', 'breakable_proxy',
39 | 'breakable_deliver',
40 | 'PromiseNotReady', 'PromiseAlreadyDelivered',
41 | 'is_promise', 'is_delivered', 'deliver',
42 | 'promise_repr', )
43 |
44 |
45 | class Container(object):
46 | """
47 | Simple promise mechanism. Acts as a container to the promised work
48 | until delivered, and there-after acts as a container to the return
49 | value from executing the work. Will invoke the promised work
50 | function exactly once, but can deliver the answer multiple times.
51 | """
52 |
53 | __slots__ = ('_work', '_answer')
54 |
55 |
56 | def __init__(self, work):
57 | """
58 | promises to provide the answer from calling work. work must be
59 | either a nullary (zero-argument) callable, or a non-callable
60 | value. If work is non-callable, then this promise is
61 | considered immediately delivered, and the work value becomes
62 | the answer.
63 | """
64 |
65 | if callable(work):
66 | # TODO: check work arity. Must be nullary.
67 | self._work = work
68 | self._answer = None
69 | else:
70 | self._work = None
71 | self._answer = work
72 |
73 |
74 | def __del__(self):
75 | self._work = None
76 | self._answer = None
77 |
78 |
79 | def is_delivered(self):
80 | """
81 | True if the promised work has been called and an answer has been
82 | recorded.
83 | """
84 |
85 | return self._work is None
86 |
87 |
88 | def deliver(self):
89 | """
90 | Deliver on promised work. Will only execute the work if an answer
91 | has not already been found. If an exception is raised during
92 | the execution of work, it will be cascade up from here as
93 | well. Returns the answer to the work once known.
94 | """
95 |
96 | # in theory this could be overridden to change how delivery
97 | # occurs, but it's probably better to use special workers
98 | # instead.
99 |
100 | work = self._work
101 |
102 | if work is not None:
103 | # note that if the work raised an exception, we won't
104 | # consider ourselves as delivered, meaning we can attempt
105 | # delivery again later. This is a feature!
106 | self._answer = work()
107 | self._work = None
108 |
109 | return self._answer
110 |
111 |
112 | def __repr__(self):
113 | work = self._work
114 | answer = self._answer
115 | if work is not None:
116 | return ""
117 | elif isinstance(answer, BrokenPromise):
118 | return ""
119 | else:
120 | return "" % answer
121 |
122 |
123 | def is_promise(obj):
124 | """
125 | True if `obj` is a promise (either a proxy or a container)
126 | """
127 |
128 | return (is_proxy(obj) or
129 | (hasattr(obj, "is_delivered") and
130 | hasattr(obj, "deliver")))
131 |
132 |
133 | def is_delivered(a_promise):
134 | """
135 | True if `a_promise` is a promise and has been delivered
136 | """
137 |
138 | return is_proxy_delivered(a_promise) if is_proxy(a_promise) \
139 | else a_promise.is_delivered()
140 |
141 |
142 | def deliver(on_promise):
143 | """
144 | Attempts to deliver on a promise, and returns the resulting
145 | value. If the delivery of work causes an exception, it will be
146 | raised here.
147 |
148 | Parameters
149 | ----------
150 | on_promise : `Proxy` or `Container` promise
151 | the promise to deliver on
152 |
153 | Returns
154 | -------
155 | value
156 | the promised work if it could be successfully computed
157 | """
158 |
159 | return deliver_proxy(on_promise) if is_proxy(on_promise) \
160 | else on_promise.deliver()
161 |
162 |
163 | def lazy(work, *args, **kwds):
164 | """
165 | Creates a new container promise to find an answer for `work`.
166 |
167 | Parameters
168 | ----------
169 | work : `callable`
170 | executed when the promise is delivered
171 | *args
172 | optional arguments to pass to work
173 | **kwds
174 | optional keyword arguments to pass to work
175 |
176 | Returns
177 | -------
178 | promise : `Container`
179 | the container promise which will deliver on `work(*args, **kwds)`
180 | """
181 |
182 | if args or kwds:
183 | work = partial(work, *args, **kwds)
184 | return Container(work)
185 |
186 |
187 | def lazy_proxy(work, *args, **kwds):
188 | """
189 | Creates a new proxy promise to find an answer for `work`.
190 |
191 | Parameters
192 | ----------
193 | work : `callable`
194 | executed when the promise is delivered
195 | *args
196 | optional arguments to pass to work
197 | **kwds
198 | optional keyword arguments to pass to work
199 |
200 | Returns
201 | -------
202 | promise : `Proxy`
203 | the proxy promise which will deliver on `work(*args, **kwds)`
204 | """
205 |
206 | if args or kwds:
207 | work = partial(work, *args, **kwds)
208 | return Proxy(work)
209 |
210 |
211 | class PromiseNotReady(Exception):
212 | """
213 | Raised when attempting to deliver on a promise whose underlying
214 | delivery function hasn't been called.
215 | """
216 |
217 | pass
218 |
219 |
220 | class PromiseAlreadyDelivered(Exception):
221 | """
222 | Raised when a paired promise's delivery function is called more
223 | than once.
224 | """
225 |
226 | pass
227 |
228 |
229 | def _promise(promise_type, blocking=False):
230 | """
231 | This is the 'traditional' type of promise. It's a single-slot,
232 | write-once value.
233 | """
234 |
235 | # our shared state! trololol closures
236 | ptr = list()
237 | exc = list()
238 | event = Event() if blocking else None
239 |
240 | # for getting a value to deliver to the promise, or for raising an
241 | # exception if one was set. This is what will be called by deliver
242 | def promise_getter():
243 | if event:
244 | event.wait()
245 | if ptr:
246 | return ptr[0]
247 | elif exc:
248 | if event:
249 | event.clear()
250 | raise exc[0], exc[1], exc[2]
251 | else:
252 | raise PromiseNotReady()
253 |
254 | promise = promise_type(promise_getter)
255 |
256 | # for setting the promise's value.
257 | def promise_setter(value):
258 | if ptr:
259 | raise PromiseAlreadyDelivered()
260 | else:
261 | del exc[:]
262 | ptr[:] = (value,)
263 | if event:
264 | event.set()
265 | deliver(promise)
266 |
267 | # for setting the promise's exception
268 | def promise_seterr(exc_type, exc_val, exc_tb):
269 | if ptr:
270 | raise PromiseAlreadyDelivered()
271 | else:
272 | exc[:] = exc_type, exc_val, exc_tb
273 | if event:
274 | event.set()
275 |
276 | return (promise, promise_setter, promise_seterr)
277 |
278 |
279 | def promise(blocking=False):
280 | """
281 | Returns a tuple of a new `Container`, a unary function to deliver
282 | a value into that promise, and a ternary function to feed an
283 | exception to the promise.
284 |
285 | If `blocking` is True, then any attempt to deliver on the promise
286 | will block until/unless a value or exception has been set via the
287 | setter or seterr functions.
288 |
289 | Returns
290 | -------
291 | promise : `Container`
292 | promise acting as a placeholder for future data
293 | setter : `function(value)`
294 | function which delivers a value to fulfill the promise
295 | seterr : `function(exc_type, exc_inst, exc_tb)`
296 | function which delivers exc_info to raise on delivery of the promise
297 |
298 | Examples
299 | --------
300 | >>> prom, setter, seterr = promise()
301 | >>> setter(5)
302 | >>> deliver(prom)
303 | 5
304 | """
305 |
306 | return _promise(Container, blocking=blocking)
307 |
308 |
309 | def promise_proxy(blocking=False):
310 | """
311 | Returns a tuple of a new `Proxy`, a unary function to deliver a
312 | value into that promise, and a ternary function to feed an
313 | exception to the promise.
314 |
315 | If `blocking` is True, then any attempt to deliver on the promise
316 | (including accessing its members) will block until/unless a value
317 | or exception has been set via the setter or seterr functions.
318 |
319 | Returns
320 | -------
321 | promise : `Proxy`
322 | promise acting as a placeholder for future data
323 | setter : `function(value)`
324 | function which delivers a value to fulfill the promise
325 | seterr : `function(exc_type, exc_inst, exc_tb)`
326 | function which delivers exc_info to raise on delivery of the promise
327 |
328 | Examples
329 | --------
330 | >>> prom, setter, seterr = promise_proxy()
331 | >>> setter(5)
332 | >>> prom
333 | 5
334 | """
335 |
336 | return _promise(Proxy, blocking=blocking)
337 |
338 |
339 | class BrokenPromise(object):
340 | """
341 | Result indicating a promise was broken. See the `breakable_lazy`,
342 | `breakable_proxy`, and `breakable_deliver` functions for more
343 | information.
344 | """
345 |
346 | def __init__(self, reason=None):
347 | """
348 | Parameters
349 | ----------
350 | reason : `sys.exc_info()` or `None`
351 | the underlying reason that the promise was broken, which should
352 | be an exc_info triplet or unspecified
353 | """
354 |
355 | self.reason = reason
356 |
357 |
358 | def _breakable_work(work, *args, **kwds):
359 | """
360 | wrapper function for work to create a BrokenPromise if it fails
361 | """
362 |
363 | try:
364 | result = work(*args, **kwds)
365 | except Exception:
366 | result = BrokenPromise(reason=exc_info())
367 | return result
368 |
369 |
370 | def breakable(work, *args, **kwds):
371 | """
372 | Creates a `Container` promise to perform `work`. If delivery of
373 | the work raises an `Exception`, a `BrokenPromise` instance is
374 | created to wrap the `sys.exc_info()` and is returned in lieu of a
375 | result.
376 |
377 | Unlike a promise created by `lazy_proxy`, raising an `Exception`
378 | in the work will result in the promise being considered delivered,
379 | but broken. Further attempts at delivery will not re-execute the
380 | work, but will return the `BrokenPromise` instance from the first
381 | failure.
382 | """
383 |
384 | return lazy(_breakable_work, work, *args, **kwds)
385 |
386 |
387 | def breakable_proxy(work, *args, **kwds):
388 | """
389 | Creates a `Proxy` promise to perform `work`. If delivery of the
390 | work raises an `Exception`, a `BrokenPromise` instance is created
391 | to wrap the `sys.exc_info()` and is returned in lieu of a result.
392 |
393 | Unlike a promise created by `lazy`, raising an `Exception` in the
394 | work will result in the promise being considered delivered, but
395 | broken. Further attempts at delivery will not re-execute the work,
396 | but will return the `BrokenPromise` instance from the first
397 | failure.
398 | """
399 |
400 | return lazy_proxy(_breakable_work, work, *args, **kwds)
401 |
402 |
403 | def breakable_deliver(on_promise):
404 | """
405 | Attempts to deliver on a promise. If delivery raises an
406 | `Exception`, it is caught and the `sys.exc_info()` is stored into
407 | a `BrokenPromise` instance which is returned in lieu of a normal
408 | result.
409 |
410 | This can be called on any `Proxy` or `Container`, not just those
411 | that were created as breakable via the `breakable_lazy` and
412 | `breakable_proxy` functions.
413 |
414 | Using this function to deliver on a promise will not change the
415 | behavior of whether it is considered delivered or not when an
416 | `Exception` is raised.
417 |
418 | Only subclasses of `Exception` are caught -- `BaseException`
419 | instances are considered too important to be caught this way.
420 |
421 | Parameters
422 | ----------
423 | on_promise : `Proxy` or `Container` promise
424 | the promise to deliver on
425 |
426 | Returns
427 | -------
428 | value
429 | the promised work if it could be successfully computed, or a
430 | BrokenPromise instance wrapping any Exception that may have been
431 | raised while attempting delivery
432 | """
433 |
434 | try:
435 | result = deliver(on_promise)
436 | except Exception:
437 | result = BrokenPromise(reason=exc_info())
438 | return result
439 |
440 |
441 | def promise_repr(of_promise):
442 | """
443 | Representation of a promise. If the promise is undelivered, will
444 | not deliver on it. If it is delivered, will deliver to check if
445 | the delivery indicates that the promise was broken.
446 |
447 | Parameters
448 | ----------
449 | of_promise : `Proxy` or `Container`
450 | the promise to represent
451 |
452 | Returns
453 | -------
454 | value : str
455 | text representation of the promise
456 | """
457 |
458 | if not is_proxy(of_promise):
459 | # non-promises and container promises just get a normal repr
460 | return repr(of_promise)
461 |
462 | elif not is_delivered(of_promise):
463 | return ""
464 |
465 | elif isinstance(breakable_deliver(of_promise), BrokenPromise):
466 | return ""
467 |
468 | else:
469 | return "" % deliver(of_promise)
470 |
471 |
472 | #
473 | # The end.
474 |
--------------------------------------------------------------------------------
/promises/multiprocess.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | Multi-process Promises for Python
18 |
19 | :author: Christopher O'Brien
20 | :license: LGPL v.3
21 | """
22 |
23 |
24 | from . import promise, promise_proxy
25 | from multiprocessing.pool import Pool
26 |
27 |
28 | __all__ = ('ProcessExecutor', 'ProxyProcessExecutor')
29 |
30 |
31 | def _perform_work(*args, **kwds):
32 | """
33 | This function is what the worker processes will use to collect the
34 | result from work (whether via return or raise)
35 |
36 | Returns
37 | -------
38 | value : `tuple`
39 | `(True, result)` if `work()` succeeds, else `(False, exc_info)`
40 | if an exception was raised
41 | """
42 |
43 | work, args = args
44 |
45 | try:
46 | return (True, work(*args, **kwds))
47 | except Exception as exc:
48 | # we are discarding the stack trace as it won't survive
49 | # pickling (which is how it will be passed back to the
50 | # handler from the worker process/thread)
51 | return (False, (type(exc), exc, None))
52 |
53 |
54 | class ProcessExecutor(object):
55 | """
56 | Create promises which will deliver in a separate process.
57 | """
58 |
59 | def __init__(self, processes=None):
60 | self._processes = processes
61 | self._pool = None
62 |
63 |
64 | def __enter__(self):
65 | return self
66 |
67 |
68 | def __exit__(self, exc_type, _exc_val, _exc_tb):
69 | """
70 | Using the managed interface forces blocking delivery at the end of
71 | the managed segment.
72 | """
73 |
74 | self.deliver()
75 | return (exc_type is None)
76 |
77 |
78 | def _promise(self):
79 | """
80 | override to use a different promise mechanism
81 | """
82 |
83 | return promise(blocking=True)
84 |
85 |
86 | def _get_pool(self):
87 | """
88 | override to provide a different pool implementation
89 | """
90 |
91 | if not self._pool:
92 | self._pool = Pool(processes=self._processes)
93 | return self._pool
94 |
95 |
96 | def future(self, work, *args, **kwds):
97 | """
98 | Promise to deliver on the results of work in the future.
99 |
100 | Parameters
101 | ----------
102 | work : `callable`
103 | This is the work which will be performed to deliver on the
104 | future.
105 | *args : `optional positional parameters`
106 | arguments to the `work` function
107 | **kwds : `optional named parameters`
108 | keyword arguments to the `work` function
109 |
110 | Returns
111 | -------
112 | value : `promise`
113 | a promise acting as a placeholder for the result of
114 | evaluating `work(*args, **kwds)`. Note that calling `deliver`
115 | on this promise will potentially block until the underlying
116 | result is available.
117 | """
118 |
119 | promised, setter, seterr = self._promise()
120 |
121 | def callback(value):
122 | # value is collected as the result of the _perform_work
123 | # function at the top of this module
124 | success, result = value
125 | if success:
126 | setter(result)
127 | else:
128 | seterr(*result)
129 |
130 | # queue up the work in our pool
131 | pool = self._get_pool()
132 | pool.apply_async(_perform_work, [work, args], kwds, callback)
133 |
134 | return promised
135 |
136 |
137 | def terminate(self):
138 | """
139 | Breaks all the remaining undelivered promises, halts execution of
140 | any parallel work being performed.
141 |
142 | Any promise which had not managed to be delivered will never
143 | be delivered after calling `terminate`. Attempting to call
144 | `deliver` on them will result in a deadlock.
145 | """
146 |
147 | # TODO: is there a way for us to cause all undelivered
148 | # promises to raise an exception of some sort when this
149 | # happens? That would be better than deadlocking while waiting
150 | # for delivery.
151 |
152 | if self._pool is not None:
153 | self._pool.terminate()
154 | self._pool = None
155 |
156 |
157 | def deliver(self):
158 | """
159 | Deliver on all underlying promises. Blocks until complete.
160 | """
161 |
162 | if self._pool is not None:
163 | self._pool.close()
164 | self._pool.join()
165 | self._pool = None
166 |
167 |
168 | class ProxyProcessExecutor(ProcessExecutor):
169 | """
170 | Create transparent proxy promises which will deliver in a separate
171 | process.
172 | """
173 |
174 | def _promise(self):
175 | return promise_proxy(blocking=True)
176 |
177 |
178 | #
179 | # The end.
180 |
--------------------------------------------------------------------------------
/promises/multithread.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | Multi-thread Promises for Python
18 |
19 | :author: Christopher O'Brien
20 | :license: LGPL v.3
21 | """
22 |
23 |
24 | from . import promise_proxy
25 | from .multiprocess import ProcessExecutor
26 | from multiprocessing.pool import ThreadPool
27 |
28 |
29 | __all__ = ('ThreadExecutor', 'ProxyThreadExecutor', )
30 |
31 |
32 | class ThreadExecutor(ProcessExecutor):
33 | """
34 | A way to provide multiple promises which will be delivered in a
35 | separate threads
36 | """
37 |
38 | def _get_pool(self):
39 | if not self._pool:
40 | self._pool = ThreadPool(processes=self._processes)
41 | return self._pool
42 |
43 |
44 | class ProxyThreadExecutor(ThreadExecutor):
45 | """
46 | Creates transparent proxy promises, which will deliver in a
47 | separate thread
48 | """
49 |
50 | def _promise(self):
51 | return promise_proxy(blocking=True)
52 |
53 |
54 | #
55 | # The end.
56 |
--------------------------------------------------------------------------------
/promises/proxy.c:
--------------------------------------------------------------------------------
1 | /*
2 | This library is free software; you can redistribute it and/or modify
3 | it under the terms of the GNU Lesser General Public License as
4 | published by the Free Software Foundation; either version 3 of the
5 | License, or (at your option) any later version.
6 |
7 | This library is distributed in the hope that it will be useful, but
8 | WITHOUT ANY WARRANTY; without even the implied warranty of
9 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10 | Lesser General Public License for more details.
11 |
12 | You should have received a copy of the GNU Lesser General Public
13 | License along with this library; if not, see
14 | .
15 | */
16 |
17 |
18 | /**
19 | Here we have a transparent (as much as is possible, anyway) proxy
20 | type. A lot of this is "borrowed" from CPython's own weakref proxy.
21 |
22 | author: Christopher O'Brien
23 | license: LGPL v.3
24 | */
25 |
26 |
27 | #include
28 |
29 |
30 | typedef struct _PyProxy {
31 | PyObject_HEAD
32 |
33 | PyObject *work;
34 | PyObject *answer;
35 |
36 | } PyProxy;
37 |
38 |
39 | PyTypeObject PyProxyType;
40 |
41 |
42 | #define PyProxy_Check(obj) \
43 | (Py_TYPE(obj) == &PyProxyType)
44 |
45 |
46 | PyObject *PyProxy_IsDelivered(PyProxy *proxy);
47 | PyObject *PyProxy_Deliver(PyProxy *proxy);
48 |
49 | static PyObject *proxy_promise_deliver(PyProxy *proxy);
50 |
51 |
52 | #define DELIVERX(proxy, fail) \
53 | { \
54 | if (proxy && PyProxy_Check(proxy)) { \
55 | proxy = proxy_promise_deliver((PyProxy *) proxy); \
56 | if (! proxy) { \
57 | return (fail); \
58 | } \
59 | } \
60 | }
61 |
62 |
63 | #define DELIVER(proxy) DELIVERX(proxy, NULL)
64 |
65 |
66 | #define VALUE(o) \
67 | (PyProxy_Check(o)? ((PyProxy *)(o))->answer: (o))
68 |
69 |
70 | #define WRAP_UNARY(name, actual) \
71 | static PyObject *name(PyObject *proxy) { \
72 | DELIVER(proxy); \
73 | return actual(proxy); \
74 | }
75 |
76 |
77 | #define WRAP_BINARY(name, actual) \
78 | static PyObject *name(PyObject *proxy, PyObject *a) { \
79 | DELIVER(proxy); \
80 | DELIVER(a); \
81 | return actual(proxy, a); \
82 | }
83 |
84 |
85 | #define WRAP_TERNARY(name, actual) \
86 | static PyObject *name(PyObject *proxy, PyObject *a, PyObject *b) { \
87 | DELIVER(proxy); \
88 | DELIVER(a); \
89 | DELIVER(b); \
90 | return actual(proxy, a, b); \
91 | }
92 |
93 |
94 | static PyObject *proxy_unicode(PyObject *proxy) {
95 | DELIVER(proxy);
96 | return PyObject_CallMethod(VALUE(proxy), "__unicode__", "");
97 | }
98 |
99 |
100 | static PyMethodDef proxy_methods[] = {
101 | {"__unicode__", (PyCFunction)proxy_unicode, METH_NOARGS},
102 | {NULL, NULL}
103 | };
104 |
105 |
106 | WRAP_BINARY(proxy_add, PyNumber_Add)
107 | WRAP_BINARY(proxy_sub, PyNumber_Subtract)
108 | WRAP_BINARY(proxy_mul, PyNumber_Multiply)
109 | WRAP_BINARY(proxy_div, PyNumber_Divide)
110 | WRAP_BINARY(proxy_mod, PyNumber_Remainder)
111 | WRAP_BINARY(proxy_divmod, PyNumber_Divmod)
112 | WRAP_TERNARY(proxy_pow, PyNumber_Power)
113 | WRAP_UNARY(proxy_neg, PyNumber_Negative)
114 | WRAP_UNARY(proxy_pos, PyNumber_Positive)
115 | WRAP_UNARY(proxy_abs, PyNumber_Absolute)
116 | WRAP_UNARY(proxy_invert, PyNumber_Invert)
117 | WRAP_BINARY(proxy_lshift, PyNumber_Lshift)
118 | WRAP_BINARY(proxy_rshift, PyNumber_Rshift)
119 | WRAP_BINARY(proxy_and, PyNumber_And)
120 | WRAP_BINARY(proxy_xor, PyNumber_Xor)
121 | WRAP_BINARY(proxy_or, PyNumber_Or)
122 | WRAP_UNARY(proxy_int, PyNumber_Int)
123 | WRAP_UNARY(proxy_long, PyNumber_Long)
124 | WRAP_UNARY(proxy_float, PyNumber_Float)
125 | WRAP_BINARY(proxy_iadd, PyNumber_InPlaceAdd)
126 | WRAP_BINARY(proxy_isub, PyNumber_InPlaceSubtract)
127 | WRAP_BINARY(proxy_imul, PyNumber_InPlaceMultiply)
128 | WRAP_BINARY(proxy_idiv, PyNumber_InPlaceDivide)
129 | WRAP_BINARY(proxy_imod, PyNumber_InPlaceRemainder)
130 | WRAP_TERNARY(proxy_ipow, PyNumber_InPlacePower)
131 | WRAP_BINARY(proxy_ilshift, PyNumber_InPlaceLshift)
132 | WRAP_BINARY(proxy_irshift, PyNumber_InPlaceRshift)
133 | WRAP_BINARY(proxy_iand, PyNumber_InPlaceAnd)
134 | WRAP_BINARY(proxy_ixor, PyNumber_InPlaceXor)
135 | WRAP_BINARY(proxy_ior, PyNumber_InPlaceOr)
136 | WRAP_BINARY(proxy_floor_div, PyNumber_FloorDivide)
137 | WRAP_BINARY(proxy_true_div, PyNumber_TrueDivide)
138 | WRAP_BINARY(proxy_ifloor_div, PyNumber_InPlaceFloorDivide)
139 | WRAP_BINARY(proxy_itrue_div, PyNumber_InPlaceTrueDivide)
140 | WRAP_UNARY(proxy_index, PyNumber_Index)
141 |
142 |
143 | static int proxy_nonzero(PyObject *proxy) {
144 | DELIVERX(proxy, -1);
145 | return PyObject_IsTrue(proxy);
146 | }
147 |
148 |
149 | static PyNumberMethods proxy_as_number = {
150 | .nb_add = proxy_add,
151 | .nb_subtract = proxy_sub,
152 | .nb_multiply = proxy_mul,
153 | .nb_divide = proxy_div,
154 | .nb_remainder = proxy_mod,
155 | .nb_divmod = proxy_divmod,
156 | .nb_power = proxy_pow,
157 | .nb_negative = proxy_neg,
158 | .nb_positive = proxy_pos,
159 | .nb_absolute = proxy_abs,
160 | .nb_nonzero = proxy_nonzero,
161 | .nb_invert = proxy_invert,
162 | .nb_lshift = proxy_lshift,
163 | .nb_rshift = proxy_rshift,
164 | .nb_and = proxy_and,
165 | .nb_xor = proxy_xor,
166 | .nb_or = proxy_or,
167 | .nb_coerce = NULL,
168 | .nb_int = proxy_int,
169 | .nb_long = proxy_long,
170 | .nb_float = proxy_float,
171 | .nb_oct = NULL,
172 | .nb_hex = NULL,
173 | .nb_inplace_add = proxy_iadd,
174 | .nb_inplace_subtract = proxy_isub,
175 | .nb_inplace_multiply = proxy_imul,
176 | .nb_inplace_divide = proxy_idiv,
177 | .nb_inplace_remainder = proxy_imod,
178 | .nb_inplace_power = proxy_ipow,
179 | .nb_inplace_lshift = proxy_ilshift,
180 | .nb_inplace_rshift = proxy_irshift,
181 | .nb_inplace_and = proxy_iand,
182 | .nb_inplace_xor = proxy_ixor,
183 | .nb_inplace_or = proxy_ior,
184 | .nb_floor_divide = proxy_floor_div,
185 | .nb_true_divide = proxy_true_div,
186 | .nb_inplace_floor_divide = proxy_ifloor_div,
187 | .nb_inplace_true_divide = proxy_itrue_div,
188 | .nb_index = proxy_index,
189 | };
190 |
191 |
192 | static Py_ssize_t proxy_length(PyObject *proxy) {
193 | DELIVERX(proxy, -1);
194 | return PyObject_Length(proxy);
195 | }
196 |
197 |
198 | static PyObject *proxy_get_slice(PyObject *proxy,
199 | Py_ssize_t i, Py_ssize_t j) {
200 | DELIVER(proxy);
201 | return PySequence_GetSlice(proxy, i, j);
202 | }
203 |
204 |
205 | static int proxy_set_slice(PyObject *proxy,
206 | Py_ssize_t i, Py_ssize_t j,
207 | PyObject *val) {
208 | DELIVERX(proxy, -1);
209 | return PySequence_SetSlice(proxy, i, j, val);
210 | }
211 |
212 |
213 | static int proxy_contains(PyObject *proxy, PyObject *val) {
214 | DELIVERX(proxy, -1);
215 | return PySequence_Contains(proxy, val);
216 | }
217 |
218 |
219 | static PySequenceMethods proxy_as_sequence = {
220 | .sq_length = (lenfunc)proxy_length,
221 | .sq_concat = NULL,
222 | .sq_repeat = NULL,
223 | .sq_item = NULL,
224 | .sq_slice = (ssizessizeargfunc)proxy_get_slice,
225 | .sq_ass_item = NULL,
226 | .sq_ass_slice = (ssizessizeobjargproc)proxy_set_slice,
227 | .sq_contains = (objobjproc)proxy_contains,
228 | };
229 |
230 |
231 | WRAP_BINARY(proxy_getitem, PyObject_GetItem)
232 |
233 |
234 | static int proxy_setitem(PyObject *proxy, PyObject *key, PyObject *val) {
235 | DELIVERX(proxy, -1);
236 | DELIVERX(key, -1);
237 | if (val == NULL) {
238 | return PyObject_DelItem(proxy, key);
239 | } else {
240 | return PyObject_SetItem(proxy, key, val);
241 | }
242 | }
243 |
244 |
245 | static PyMappingMethods proxy_as_mapping = {
246 | .mp_length = (lenfunc)proxy_length,
247 | .mp_subscript = proxy_getitem,
248 | .mp_ass_subscript = (objobjargproc)proxy_setitem,
249 | };
250 |
251 |
252 | static PyObject *proxy_richcompare(PyObject *proxy, PyObject *comp, int op) {
253 | DELIVER(proxy);
254 | DELIVER(comp);
255 | return PyObject_RichCompare(proxy, comp, op);
256 | }
257 |
258 |
259 | WRAP_UNARY(proxy_iter, PyObject_GetIter)
260 | WRAP_UNARY(proxy_iternext, PyIter_Next)
261 |
262 |
263 | static int promise_clear(PyProxy *proxy) {
264 | if (proxy->work) {
265 | Py_DECREF(proxy->work);
266 | proxy->work = NULL;
267 | }
268 | if (proxy->answer) {
269 | Py_DECREF(proxy->answer);
270 | proxy->answer = NULL;
271 | }
272 | return 0;
273 | }
274 |
275 |
276 | static long proxy_hash(PyObject *proxy) {
277 | DELIVERX(proxy, -1);
278 | return PyObject_Hash(proxy);
279 | }
280 |
281 |
282 | static int proxy_compare(PyObject *proxy, PyObject *val) {
283 | DELIVERX(proxy, -1);
284 | DELIVERX(val, -1);
285 | return PyObject_Compare(proxy, val);
286 | }
287 |
288 |
289 | WRAP_UNARY(proxy_repr, PyObject_Repr)
290 | WRAP_UNARY(proxy_str, PyObject_Str)
291 | WRAP_BINARY(proxy_getattr, PyObject_GetAttr)
292 |
293 |
294 | static PyObject *proxy_call(PyObject *proxy,
295 | PyObject *args, PyObject *kwds) {
296 | DELIVER(proxy);
297 | return PyObject_Call(proxy, args, kwds);
298 | }
299 |
300 |
301 | static int proxy_setattr(PyObject *proxy,
302 | PyObject *name, PyObject *val) {
303 | DELIVERX(proxy, -1);
304 | return PyObject_SetAttr(proxy, name, val);
305 | }
306 |
307 |
308 | static void promise_dealloc(PyProxy *self) {
309 | promise_clear(self);
310 | Py_TYPE(self)->tp_free((PyObject*)self);
311 | }
312 |
313 |
314 | static PyObject *promise_new(PyTypeObject *type,
315 | PyObject *args, PyObject *kwds) {
316 |
317 | PyProxy *self;
318 |
319 | self = (PyProxy *) type->tp_alloc(type, 0);
320 | if (self != NULL) {
321 | self->work = NULL;
322 | self->answer = NULL;
323 | }
324 |
325 | return (PyObject *) self;
326 | }
327 |
328 |
329 | static int promise_init(PyProxy *self,
330 | PyObject *args, PyObject *kwds) {
331 |
332 | PyObject *work = NULL;
333 |
334 | if (! PyArg_ParseTuple(args, "O", &work))
335 | return -1;
336 |
337 | promise_clear(self);
338 |
339 | if (PyCallable_Check(work)) {
340 | self->work = work;
341 | Py_INCREF(work);
342 |
343 | } else {
344 | self->answer = work;
345 | Py_INCREF(work);
346 | }
347 |
348 | return 0;
349 | }
350 |
351 |
352 | PyTypeObject PyProxyType = {
353 | PyVarObject_HEAD_INIT(&PyType_Type, 0)
354 |
355 | "promises.Proxy",
356 | sizeof(PyProxy),
357 | 0,
358 |
359 | .tp_dealloc = (destructor)promise_dealloc,
360 | .tp_print = NULL,
361 | .tp_getattr = NULL,
362 | .tp_setattr = NULL,
363 | .tp_compare = proxy_compare,
364 | .tp_repr = (reprfunc)proxy_repr,
365 | .tp_as_number = &proxy_as_number,
366 | .tp_as_sequence = &proxy_as_sequence,
367 | .tp_as_mapping = &proxy_as_mapping,
368 | .tp_hash = proxy_hash,
369 | .tp_call = proxy_call,
370 | .tp_str = proxy_str,
371 | .tp_getattro = proxy_getattr,
372 | .tp_setattro = (setattrofunc)proxy_setattr,
373 | .tp_as_buffer = NULL,
374 | .tp_flags = (Py_TPFLAGS_DEFAULT |
375 | Py_TPFLAGS_CHECKTYPES),
376 | .tp_doc = NULL,
377 | .tp_traverse = NULL,
378 | .tp_clear = (inquiry)promise_clear,
379 | .tp_richcompare = proxy_richcompare,
380 | .tp_weaklistoffset = 0,
381 | .tp_iter = (getiterfunc)proxy_iter,
382 | .tp_iternext = (iternextfunc)proxy_iternext,
383 | .tp_methods = proxy_methods,
384 |
385 | .tp_new = promise_new,
386 | .tp_init = (initproc)promise_init,
387 | };
388 |
389 |
390 | #define proxy_promise_is_delivered(proxy) \
391 | ((proxy)->work == NULL)
392 |
393 |
394 | static PyObject *proxy_promise_deliver(PyProxy *proxy) {
395 | PyObject *work;
396 | PyObject *answer = NULL;
397 |
398 | if(proxy_promise_is_delivered(proxy)) {
399 | answer = proxy->answer;
400 |
401 | } else {
402 | work = proxy->work;
403 |
404 | if (PyCallable_Check(work)) {
405 | answer = PyObject_CallObject(work, NULL);
406 |
407 | if (answer != NULL) {
408 | proxy->work = NULL;
409 | Py_DECREF(work);
410 | }
411 | } else{
412 | answer = work;
413 | }
414 |
415 | proxy->answer = answer;
416 | }
417 |
418 | return answer;
419 | }
420 |
421 |
422 | PyObject *PyProxy_IsDelivered(PyProxy *proxy) {
423 | if (proxy_promise_is_delivered(proxy)) {
424 | Py_RETURN_TRUE;
425 | } else {
426 | Py_RETURN_FALSE;
427 | }
428 | }
429 |
430 |
431 | PyObject *PyProxy_Deliver(PyProxy *proxy) {
432 | PyObject *answer;
433 |
434 | answer = proxy_promise_deliver(proxy);
435 | if (answer) {
436 | Py_INCREF(answer);
437 | }
438 | return answer;
439 | }
440 |
441 |
442 | static PyObject *is_proxy(PyObject *module, PyObject *args) {
443 | PyObject *obj = NULL;
444 |
445 | if (! PyArg_ParseTuple(args, "O", &obj))
446 | return NULL;
447 |
448 | if (PyProxy_Check(obj)) {
449 | Py_RETURN_TRUE;
450 | } else {
451 | Py_RETURN_FALSE;
452 | }
453 | }
454 |
455 |
456 | static PyObject *is_proxy_delivered(PyObject *module, PyObject *args) {
457 | PyProxy *proxy = NULL;
458 |
459 | if (! PyArg_ParseTuple(args, "O!", &PyProxyType, &proxy))
460 | return NULL;
461 |
462 | return PyProxy_IsDelivered(proxy);
463 | }
464 |
465 |
466 | static PyObject *deliver_proxy(PyObject *module, PyObject *args) {
467 | PyProxy *proxy = NULL;
468 |
469 | if (! PyArg_ParseTuple(args, "O!", &PyProxyType, &proxy))
470 | return NULL;
471 |
472 | return PyProxy_Deliver(proxy);
473 | }
474 |
475 |
476 | static PyMethodDef methods[] = {
477 |
478 | { "is_proxy", is_proxy, METH_VARARGS,
479 | "True if an object is a proxy promise" },
480 |
481 | { "is_proxy_delivered", is_proxy_delivered, METH_VARARGS,
482 | "True if the proxy has delivered on its promise" },
483 |
484 | { "deliver_proxy", deliver_proxy, METH_VARARGS,
485 | "Deliver on a proxy promise if it isn't delivered already" },
486 |
487 | { NULL, NULL, 0, NULL },
488 | };
489 |
490 |
491 | PyMODINIT_FUNC init_proxy() {
492 | PyObject *mod;
493 | PyObject *proxytype;
494 |
495 | proxytype = (PyObject *) &PyProxyType;
496 |
497 | if (PyType_Ready(&PyProxyType) < 0)
498 | return;
499 |
500 | mod = Py_InitModule("promises._proxy", methods);
501 |
502 | Py_INCREF(proxytype);
503 | PyModule_AddObject(mod, "Proxy", proxytype);
504 | }
505 |
506 |
507 | /* The end. */
508 |
--------------------------------------------------------------------------------
/promises/xmlrpc.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | XML RPC MultiCall Promises.
18 |
19 | :author: Christopher O'Brien
20 | :license: LGPL v.3
21 |
22 | Examples
23 | --------
24 | >>> from xmlrpclib import Server
25 | >>> from promises.xmlrpc import LazyMultiCall
26 | >>>
27 |
28 | """
29 |
30 |
31 | from . import lazy, lazy_proxy
32 | from xmlrpclib import MultiCall
33 |
34 |
35 | __all__ = ('LazyMultiCall', 'ProxyMultiCall', )
36 |
37 |
38 | class LazyMultiCall(object):
39 | """
40 | A wrapper to `xmlrpclib.MultiCall` which allows the programmer to
41 | receive promises for the calls as they are written, rather than
42 | having to gather and distribute the results at the end. Forcing a
43 | promise to deliver will also force this multicall to execute all
44 | of its grouped xmlrpc calls.
45 |
46 | As with all lazy calls, if nothing requests delivery of the
47 | promised result, it is possible for the work to never be executed.
48 | As such, it is inappropriate to expect the queued calls to be
49 | triggered in any particular order, or at all.
50 |
51 | When `group_calls` is greater than zero, queued requests will be
52 | collected up to that many at a time, and then a new group will be
53 | created for any further calls. Whenever a promise is delivered, it
54 | also delivers all queued calls in its group.
55 |
56 | This class supports the managed interface API, and as such can be
57 | used via the `with` keyword. The managed interface delivers on all
58 | promises when exiting.
59 | """
60 |
61 |
62 | def __init__(self, server, group_calls=0):
63 | """
64 | Parameters
65 | ----------
66 | server : `xmlrpclib.Server`
67 | connection to xmlrpc server to send the multicall to
68 | group_calls : `int`
69 | number of virtual call promises to queue for delivery in a
70 | single multicall. 0 for unlimited
71 | """
72 |
73 | # hide our members well, since MultiCall creates member calls
74 | # on-the-fly
75 | self.__server = server
76 | self.__mclist = list()
77 | self.__mc = None
78 | self.__counter = 0
79 | self.__group_calls = max(0, int(group_calls))
80 |
81 |
82 | def __enter__(self):
83 | return self
84 |
85 |
86 | def __exit__(self, exc_type, _exc_val, _exc_tb):
87 | self()
88 | return (exc_type is None)
89 |
90 |
91 | def __call__(self):
92 | """
93 | Retrieve answers for any of our currently outstanding
94 | promises.
95 | """
96 |
97 | for mc in self.__mclist:
98 | mc()
99 |
100 | # the above invalidates our current __mc for further queueing
101 | self.__mc = None
102 | self.__counter = 0
103 |
104 |
105 | def __getattr__(self, name):
106 | def promisary(*args, **kwds):
107 | # make sure we have an underlying memoized multicall
108 | multicall = self.__get_multicall()
109 |
110 | # enqueue the call in the multicall
111 | getattr(multicall, name)(*args, **kwds)
112 |
113 | # this is how we'll relate back to our answers from the
114 | # current multicall.
115 | index = self.__counter
116 | self.__counter += 1
117 |
118 | # the resulting promise will keep a reference to the
119 | # particular memoized multicall, as that is where it will
120 | # want to get its answer from.
121 | promised = self.__promise__(self.__deliver_on, multicall, index)
122 |
123 | # if this promise puts us at our threhold for grouping
124 | # calls, then it's time to start using a new mc
125 | if self.__group_calls and self.__group_calls <= self.__counter:
126 | self.__mc = None
127 | self.__counter = 0
128 |
129 | return promised
130 |
131 | promisary.func_name = name
132 | return promisary
133 |
134 |
135 | def __get_multicall(self):
136 | multicall = self.__mc
137 | if multicall is None:
138 | multicall = MemoizedMultiCall(self.__server)
139 | self.__mc = multicall
140 | self.__mclist.append(multicall)
141 | self.__counter = 0
142 |
143 | return multicall
144 |
145 |
146 | def __deliver_on(self, mc, index):
147 | assert(mc is not None)
148 |
149 | # if the promise is against the current MC, then we need to
150 | # deliver on it and clear ourselves to create a new one.
151 | # Otherwise, use the memoized answers for the already
152 | # delivered MC. Then we return the result at the given index.
153 | if mc is self.__mc:
154 | self.__mc = None
155 | self.__counter = 0
156 |
157 | # a great feature of this is that the delivery or access of
158 | # the promise will also raise the underlying fault if there
159 | # happened to be one
160 | return mc()[index]
161 |
162 |
163 | def __promise__(self, work, *args, **kwds):
164 | """
165 | override to provide alternative promise implementations
166 | """
167 |
168 | return lazy(work, *args, **kwds)
169 |
170 |
171 | class ProxyMultiCall(LazyMultiCall):
172 | """
173 | A `LazyMultiCall` whose virtual methods will return `Proxy`
174 | instead of `Container` style promises.
175 | """
176 |
177 | def __promise__(self, work, *args, **kwds):
178 | return lazy_proxy(work, *args, **kwds)
179 |
180 |
181 | class MemoizedMultiCall(MultiCall):
182 | """
183 | A Memoized MultiCall, will only perform the underlying xmlrpc call
184 | once, remembers the answers for all further requests
185 | """
186 |
187 | # Note: we don't export this because it doesn't have any
188 | # safety-net in place to prevent someone from queueing more calls
189 | # against it post-delivery.
190 |
191 | def __init__(self, server):
192 | MultiCall.__init__(self, server)
193 | self.__answers = None
194 |
195 |
196 | def __call__(self):
197 | if self.__answers is None:
198 | self.__answers = MultiCall.__call__(self)
199 | return self.__answers
200 |
201 |
202 | #
203 | # The end.
204 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python2
2 |
3 | # This library is free software; you can redistribute it and/or modify
4 | # it under the terms of the GNU Lesser General Public License as
5 | # published by the Free Software Foundation; either version 3 of the
6 | # License, or (at your option) any later version.
7 | #
8 | # This library is distributed in the hope that it will be useful, but
9 | # WITHOUT ANY WARRANTY; without even the implied warranty of
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 | # Lesser General Public License for more details.
12 | #
13 | # You should have received a copy of the GNU Lesser General Public
14 | # License along with this library; if not, see
15 | # .
16 |
17 |
18 | """
19 | Promises for Python
20 |
21 | Yet another module for doing promises in python! This time with
22 | transparent proxies, and other convoluted stuff that will make you
23 | wish someone smarter had worked on this.
24 |
25 | author: Christopher O'Brien
26 | license: LGPL v.3
27 | """
28 |
29 |
30 | from setuptools import setup, Extension
31 | import multiprocessing # NOQA : tests needs this to be imported
32 |
33 |
34 | ext = [Extension("promises._proxy", ["promises/proxy.c"]), ]
35 |
36 |
37 | setup(name = "promises",
38 | version = "0.9.0",
39 |
40 | packages = ["promises", ],
41 |
42 | ext_modules = ext,
43 |
44 | test_suite = "tests",
45 |
46 | # PyPI information
47 | author = "Christopher O'Brien",
48 | author_email = "obriencj@gmail.com",
49 | url = "https://github.com/obriencj/python-promises",
50 | license = "GNU Lesser General Public License",
51 |
52 | description = "Promises, container and transparent, with"
53 | " threading and multiprocessing support",
54 |
55 | provides = ["promises", ],
56 | requires = [],
57 | platforms = ["python2 >= 2.6", ],
58 |
59 | classifiers = [
60 | "Intended Audience :: Developers",
61 | "Programming Language :: Python :: 2",
62 | "Topic :: Software Development", ])
63 |
64 |
65 | #
66 | # The end.
67 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | Unit Tests for python-promises
18 |
19 | author: Christopher O'Brien
20 | license: LGPL v.3
21 | """
22 |
23 |
24 | import sys
25 | import unittest
26 |
27 | from itertools import izip
28 | from promises import *
29 |
30 |
31 | def create_exc_tb(exception=None):
32 | """
33 | generate an exception info triplet
34 | """
35 |
36 | if exception is None:
37 | exception = Exception("dummy exception")
38 |
39 | try:
40 | raise exception
41 | except Exception, e:
42 | return sys.exc_info()
43 |
44 |
45 | def born_to_fail():
46 | raise Exception("dummy exception")
47 |
48 |
49 | class TestContainer(unittest.TestCase):
50 | """
51 | tests for the ContainerPromise class
52 | """
53 |
54 |
55 | def lazy(self, work, *args, **kwds):
56 | return lazy(work, *args, **kwds)
57 |
58 |
59 | def promise(self, blocking=False):
60 | return promise(blocking=blocking)
61 |
62 |
63 | def breakable(self, work, *args, **kwds):
64 | return breakable(work, *args, **kwds)
65 |
66 |
67 | def assert_called_once(self, work):
68 | """
69 | helper to assert that work is only called once
70 | """
71 |
72 | called = [False]
73 | def do_work_once():
74 | self.assertFalse(called[0], "do_work_once already called")
75 | called[0] = True
76 | return work()
77 | return do_work_once
78 |
79 |
80 | def test_promise_setter(self):
81 | # setter from settable_container delivers
82 |
83 | promised, setter, seterr = self.promise()
84 |
85 | self.assertTrue(is_promise(promised))
86 | self.assertFalse(is_delivered(promised))
87 |
88 | val = { "testval": True, "a": 5, "b": tuple() }
89 | setter(val)
90 |
91 | self.assertTrue(is_delivered(promised))
92 | self.assertEqual(deliver(promised), val)
93 |
94 |
95 | def test_promise_seterr(self):
96 | # seterr from settable_container causes deliver to raise
97 |
98 | promised, setter, seterr = self.promise()
99 |
100 | self.assertTrue(is_promise(promised))
101 | self.assertFalse(is_delivered(promised))
102 |
103 | class TacoException(Exception):
104 | pass
105 |
106 | exc = TacoException()
107 | seterr(*create_exc_tb(exc))
108 |
109 | self.assertRaises(TacoException, lambda: deliver(promised))
110 |
111 | setter(8)
112 | self.assertTrue(is_delivered(promised))
113 | self.assertEqual(deliver(promised), 8)
114 |
115 |
116 | def test_promise_double_set(self):
117 |
118 | promised, setter, seterr = self.promise()
119 |
120 | self.assertFalse(is_delivered(promised))
121 |
122 | # we aren't ready to deliver, make sure that it says so
123 | foo = lambda: deliver(promised)
124 | self.assertRaises(PromiseNotReady, foo)
125 |
126 | setter(100)
127 | self.assertEqual(deliver(promised), 100)
128 |
129 | # now we try to set it again
130 | foo = lambda: setter(100)
131 | self.assertRaises(PromiseAlreadyDelivered, foo)
132 |
133 | # okay, how about setting an exception instead
134 | foo = lambda: seterr(*create_exc_tb(Exception()))
135 | self.assertRaises(PromiseAlreadyDelivered, foo)
136 |
137 |
138 | def test_memoized(self):
139 | # promised work is only executed once.
140 |
141 | work = self.assert_called_once(lambda: "Hello World")
142 | promised = self.lazy(work)
143 |
144 | self.assertEqual(deliver(promised), "Hello World")
145 | self.assertEqual(deliver(promised), "Hello World")
146 |
147 |
148 | def test_callable_int(self):
149 | # callable work returning an int
150 |
151 | promised = self.lazy(lambda: 5)
152 |
153 | self.assertTrue(is_promise(promised))
154 | self.assertFalse(is_delivered(promised))
155 |
156 | x = deliver(promised) + 1
157 | self.assertTrue(is_delivered(promised))
158 | self.assertEqual(deliver(promised), 5)
159 | self.assertEqual(x, 6)
160 |
161 |
162 | def test_non_callable_int(self):
163 | # int as promised work delivers immediately
164 |
165 | promised = self.lazy(5)
166 |
167 | self.assertTrue(is_promise(promised))
168 | self.assertTrue(is_delivered(promised))
169 |
170 | x = deliver(promised) + 1
171 | self.assertTrue(is_delivered(promised))
172 | self.assertEqual(deliver(promised), 5)
173 | self.assertEqual(x, 6)
174 |
175 |
176 | def test_repr(self):
177 | promised = self.lazy(lambda: 5)
178 | self.assertEqual("",
179 | promise_repr(promised))
180 |
181 | deliver(promised)
182 |
183 | self.assertEqual("",
184 | promise_repr(promised))
185 |
186 | promised = self.breakable(born_to_fail)
187 | self.assertEqual("",
188 | promise_repr(promised))
189 |
190 | deliver(promised)
191 |
192 | self.assertEqual("",
193 | promise_repr(promised))
194 |
195 | self.assertEqual("5", promise_repr(5))
196 |
197 |
198 | def test_broken(self):
199 | # test that a breakable that doesn't break will function
200 | # correctly
201 | promised = self.breakable(lambda: 5)
202 | self.assertTrue(is_promise(promised))
203 | self.assertFalse(is_delivered(promised))
204 |
205 | deliver(promised)
206 | self.assertTrue(is_delivered(promised))
207 | self.assertEqual(deliver(promised), 5)
208 |
209 | # create a breakable that will definitely break
210 | promised = self.breakable(born_to_fail)
211 | self.assertTrue(is_promise(promised))
212 | self.assertFalse(is_delivered(promised))
213 |
214 | # breakable promises count as delivered if they break, but
215 | # they return a BrokenPromise instance
216 | deliver(promised)
217 | self.assertTrue(is_delivered(promised))
218 | self.assertTrue(isinstance(deliver(promised), BrokenPromise))
219 |
220 |
221 | def test_breakable_deliver(self):
222 | # a promise that will definitely fail
223 | promised = self.lazy(born_to_fail)
224 | self.assertTrue(is_promise(promised))
225 | self.assertFalse(is_delivered(promised))
226 |
227 | # check that delivery does indeed raise an Exception and
228 | # doesn't cause the delivery to take.
229 | self.assertRaises(Exception, lambda: deliver(promised))
230 | self.assertFalse(is_delivered(promised))
231 |
232 | # attempt breakable delivery. Should not raise, but should
233 | # instead return a BrokenPromise. Since the promise wasn't
234 | # created breakable, it will still not count as delivered.
235 | broken = breakable_deliver(promised)
236 | self.assertTrue(isinstance(broken, BrokenPromise))
237 | self.assertFalse(is_delivered(promised))
238 |
239 |
240 | class TestProxy(TestContainer):
241 | """
242 | tests for the ProxyPromise class
243 | """
244 |
245 |
246 | def lazy(self, work, *args, **kwds):
247 | return lazy_proxy(work, *args, **kwds)
248 |
249 |
250 | def promise(self, blocking=False):
251 | return promise_proxy(blocking=blocking)
252 |
253 |
254 | def breakable(self, work, *args, **kwds):
255 | return breakable_proxy(work, *args, **kwds)
256 |
257 |
258 | def test_proxy_equality(self):
259 | # proxy equality works over a wide range of types
260 |
261 | class DummyClass(object):
262 | pass
263 |
264 | values = ( True, False, None,
265 | 999, 9.99, "test string", u"unicode string",
266 | (1, 2, 3), [1, 2, 3],
267 | {"a":1,"b":2,"c":3},
268 | object, object(), DummyClass, DummyClass(),
269 | xrange, xrange(0, 99),
270 | lambda x: x+8 )
271 |
272 | provs = (self.lazy(lambda:val) for val in values)
273 |
274 | for val,prov in izip(values, provs):
275 | self.assertEqual(prov, val)
276 | self.assertEqual(val, prov)
277 |
278 |
279 | def test_proxy_int(self):
280 | # transparent proxy of an int
281 |
282 | A = 5
283 | B = self.lazy(5)
284 | deliver(B)
285 |
286 | self.assertTrue(A == B)
287 | self.assertTrue(B == A)
288 | self.assertTrue((A + B) == 10)
289 | self.assertTrue((B + A) == 10)
290 | self.assertTrue((B * 2) == 10)
291 | self.assertTrue((2 * B) == 10)
292 | self.assertTrue((str(B)) == "5")
293 | self.assertTrue((int(B)) == 5)
294 | self.assertTrue((B % 1) == (5 % 1))
295 | self.assertTrue((B >> 1) == (5 >> 1))
296 | self.assertTrue((B << 1) == (5 << 1))
297 | self.assertTrue((B ** 2) == (5 ** 2))
298 |
299 |
300 | def test_proxy_obj(self):
301 | # transparent proxy of an object
302 |
303 | class Foo(object):
304 | A = 100
305 | def __init__(self):
306 | self.B = 200
307 | def C(self):
308 | return 300
309 | def __eq__(self, o):
310 | return (self.A == o.A and
311 | self.B == o.B and
312 | self.C() == o.C())
313 | def __ne__(self, o):
314 | return not self.__eq__(o)
315 |
316 | FA = Foo()
317 | FB = self.lazy(Foo)
318 |
319 | deliver(FB)
320 | self.assertTrue(FA == FB)
321 | self.assertTrue(FB == FA)
322 |
323 | FA.B = 201
324 | FB.B = 201
325 | self.assertTrue(FA == FB)
326 | self.assertTrue(FB == FA)
327 |
328 |
329 | def test_repr(self):
330 | promised = self.lazy(lambda: 5)
331 | self.assertEqual("",
332 | promise_repr(promised))
333 |
334 | deliver(promised)
335 |
336 | self.assertEqual("",
337 | promise_repr(promised))
338 |
339 | promised = self.breakable(born_to_fail)
340 | self.assertEqual("",
341 | promise_repr(promised))
342 |
343 | deliver(promised)
344 |
345 | self.assertEqual("",
346 | promise_repr(promised))
347 |
348 |
349 | #
350 | # The end.
351 |
--------------------------------------------------------------------------------
/tests/multiprocess.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | Unit-tests for python-promises multiprocessing support
18 |
19 | author: Christopher O'Brien
20 | license: LGPL v.3
21 | """
22 |
23 |
24 | from promises import is_promise, is_delivered, deliver
25 | from promises.multiprocess import ProcessExecutor, ProxyProcessExecutor
26 | from unittest import TestCase
27 |
28 |
29 | def work_load(x):
30 | #print "performing work_load", x
31 | return x + 1
32 |
33 |
34 | def fail_load(x):
35 | #print "fail_load raising"
36 | raise TacoException("failed on %i" % x)
37 |
38 |
39 | class TacoException(Exception):
40 | pass
41 |
42 |
43 | class TestProcessExecutor(TestCase):
44 |
45 |
46 | def executor(self):
47 | return ProcessExecutor()
48 |
49 |
50 | def test_managed(self):
51 | with self.executor() as ex:
52 | a = ex.future(work_load, -1)
53 | self.assertTrue(is_promise(a))
54 |
55 | # generate some minor workload, enough to engage a queue
56 | values = [ex.future(work_load, x) for x in xrange(0, 999)]
57 |
58 | b = ex.future(work_load, -101)
59 | self.assertFalse(is_delivered(b))
60 |
61 | # the managed interface implicitly calls the deliver() method
62 | # on the executor, so by the time we get here, everything
63 | # should be delivered.
64 |
65 | self.assertTrue(is_promise(a))
66 | self.assertTrue(is_delivered(a))
67 | self.assertEqual(deliver(a), 0)
68 |
69 | self.assertTrue(is_promise(b))
70 | self.assertTrue(is_delivered(b))
71 | self.assertEqual(deliver(b), -100)
72 |
73 | self.assertEqual([deliver(v) for v in values],
74 | list(xrange(1, 1000)))
75 |
76 |
77 | def test_blocking(self):
78 | ex = self.executor()
79 |
80 | # generate some minor workload, enough to engage a queue
81 | values = [ex.future(work_load, x) for x in xrange(0, 999)]
82 |
83 | b = ex.future(work_load, -101)
84 | self.assertFalse(is_delivered(b))
85 | self.assertEqual(deliver(b), -100)
86 |
87 | ex.deliver()
88 | #self.assertTrue(ex.is_delivered())
89 |
90 |
91 | def test_terminate(self):
92 | ex = self.executor()
93 |
94 | # generate some minor workload, enough to engage a queue
95 | values = [ex.future(work_load, x) for x in xrange(0, 999)]
96 |
97 | b = ex.future(work_load, -101)
98 | self.assertFalse(is_delivered(b))
99 |
100 | ex.terminate()
101 | self.assertFalse(is_delivered(b))
102 |
103 | #self.assertTrue(ex.is_delivered())
104 |
105 |
106 | def test_raises(self):
107 | ex = self.executor()
108 |
109 | # generate some minor workload, enough to engage a queue
110 | values = [ex.future(work_load, x) for x in xrange(0, 999)]
111 |
112 | b = ex.future(fail_load, -101)
113 |
114 | self.assertFalse(is_delivered(b))
115 | self.assertRaises(TacoException, lambda: deliver(b))
116 |
117 | ex.terminate()
118 |
119 |
120 | class TestProxyProcessExecutor(TestProcessExecutor):
121 |
122 | def executor(self):
123 | return ProxyProcessExecutor()
124 |
125 |
126 | #
127 | # The end.
128 |
--------------------------------------------------------------------------------
/tests/multithread.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | Unit-tests for python-promises multithreading support
18 |
19 | :author: Christopher O'Brien
20 | :license: LGPL v.3
21 | """
22 |
23 |
24 | from promises.multithread import ThreadExecutor, ProxyThreadExecutor
25 | from .multiprocess import TestProcessExecutor
26 |
27 |
28 | class TestThreadExecutor(TestProcessExecutor):
29 | """
30 | Create promises which will deliver in a separate thread.
31 | """
32 |
33 | def executor(self):
34 | return ThreadExecutor()
35 |
36 |
37 | class TestProxyThreadExecutor(TestProcessExecutor):
38 | """
39 | Create transparent proxy promises which will deliver in a separate
40 | thread.
41 | """
42 |
43 | def executor(self):
44 | return ProxyThreadExecutor()
45 |
46 |
47 | #
48 | # The end.
49 |
--------------------------------------------------------------------------------
/tests/xmlrpc.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 |
18 | Unit-tests for python-promises XML RPC MultiCall
19 |
20 | author: Christopher O'Brien
21 | license: LGPL v.3
22 |
23 | """
24 |
25 |
26 | from promises import *
27 | from promises.xmlrpc import *
28 | from threading import Thread
29 | from unittest import TestCase
30 | from xmlrpclib import ServerProxy
31 | from SimpleXMLRPCServer import SimpleXMLRPCServer
32 |
33 |
34 | class Dummy(object):
35 | def __init__(self):
36 | self.data = list(xrange(0,10))
37 |
38 | def get(self, index):
39 | return self.data[index]
40 |
41 | def steal(self, index):
42 | value = self.data[index]
43 | self.data[index] = None
44 | return value
45 |
46 |
47 | class XMLRPCHarness(object):
48 | """
49 | A setUp/tearDown harness that will provide an XMLRPC Server for us
50 | to test against.
51 | """
52 |
53 | HOST = "localhost"
54 | PORT = 8999
55 |
56 |
57 | def __init__(self, *args, **kwds):
58 | super(XMLRPCHarness, self).__init__(*args, **kwds)
59 | self.server = None
60 | self.thread = None
61 | self.dummy = None
62 |
63 |
64 | def setUp(self):
65 | assert(self.server is None)
66 | assert(self.thread is None)
67 |
68 | self.dummy = Dummy()
69 |
70 | self.server = SimpleXMLRPCServer((self.HOST, self.PORT),
71 | logRequests=False)
72 | self.server.register_function(self.dummy.get, "get")
73 | self.server.register_function(self.dummy.steal, "steal")
74 | self.server.register_multicall_functions()
75 |
76 | self.thread = Thread(target=self.server.serve_forever,
77 | kwargs={"poll_interval":0.2})
78 | self.thread.start()
79 |
80 |
81 | def tearDown(self):
82 | assert(self.server is not None)
83 | assert(self.thread is not None)
84 |
85 | self.server.shutdown()
86 | self.server.socket.close()
87 | self.server = None
88 |
89 | self.dummy = None
90 |
91 | self.thread.join()
92 | self.thread = None
93 |
94 |
95 | def get_client(self):
96 | assert(self.server is not None)
97 | assert(self.thread is not None)
98 |
99 | return ServerProxy("http://%s:%i" % (self.HOST, self.PORT))
100 |
101 |
102 | class TestLazyMultiCall(XMLRPCHarness, TestCase):
103 |
104 |
105 | def get_multicall(self, *args, **kwds):
106 | return LazyMultiCall(self.get_client(), *args, **kwds)
107 |
108 |
109 | def test_delivery(self):
110 | mc = self.get_multicall()
111 |
112 | a = mc.steal(1)
113 | b = mc.steal(2)
114 |
115 | # check that the delivery has not yet happened
116 | self.assertEqual(self.dummy.get(1), 1)
117 | self.assertEqual(self.dummy.get(2), 2)
118 |
119 | # deliver the first promise and check that the value is what
120 | # we expected
121 | self.assertEqual(deliver(a), 1)
122 |
123 | # check that the call happened for both queued promises, which
124 | # should have destructively altered the dummy entries for
125 | # those indexes
126 | self.assertEqual(self.dummy.get(1), None)
127 | self.assertEqual(self.dummy.get(2), None)
128 |
129 | # check that the second promise has the correct value
130 | self.assertEqual(deliver(b), 2)
131 |
132 |
133 | def test_group_calls(self):
134 | # have our multicall deliver on promises in groups of 2
135 | mc = self.get_multicall(group_calls=2)
136 |
137 | # get promises for a bunch of steal calls, each of which has
138 | # side effects we can test for on the dummy.
139 | stolen = [mc.steal(x) for x in xrange(0, 10)]
140 |
141 | dummy = self.dummy
142 |
143 | self.assertEqual(deliver(stolen[0]), 0)
144 | self.assertEqual(dummy.get(0), None)
145 | self.assertEqual(dummy.get(1), None)
146 | self.assertEqual(deliver(stolen[1]), 1)
147 |
148 | # having delivered on 0 and 1, 2 should not yet have been
149 | # delivered, so the dummy 2 should still be 2 rather than None
150 | self.assertEqual(dummy.get(2), 2)
151 | self.assertEqual(dummy.get(3), 3)
152 |
153 | self.assertEqual(deliver(stolen[4]), 4)
154 | self.assertEqual(dummy.get(4), None)
155 | self.assertEqual(dummy.get(5), None)
156 | self.assertEqual(deliver(stolen[5]), 5)
157 |
158 | # now having delivered on 4 and 5, 2 should still not yet have
159 | # been delivered, so the dummy 2 should still be 2 rather than
160 | # None
161 | self.assertEqual(dummy.get(2), 2)
162 | self.assertEqual(dummy.get(3), 3)
163 |
164 |
165 | def test_grouped_with(self):
166 |
167 | dummy = self.dummy
168 |
169 | with self.get_multicall(group_calls=3) as mc:
170 | stolen = [mc.steal(x) for x in xrange(0, 10)]
171 |
172 | # we've collected all the promises, but not delivered yet
173 | self.assertEqual(dummy.data, list(xrange(0, 10)))
174 |
175 | # now we've delivered, since the managed interface was used
176 | # and has closed. Thus the desrtuctive steal calls have all
177 | # happened
178 | self.assertEqual(dummy.data, ([None] * 10))
179 |
180 | # let's make sure the delivered data is what it should be
181 | self.assertEqual([deliver(val) for val in stolen],
182 | list(xrange(0, 10)))
183 |
184 |
185 | class TestProxyMultiCall(TestLazyMultiCall):
186 |
187 | def get_multicall(self, *args, **kwds):
188 | return ProxyMultiCall(self.get_client(), *args, **kwds)
189 |
190 |
191 | #
192 | # The end.
193 |
--------------------------------------------------------------------------------