├── .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 | [![Build Status](https://travis-ci.org/obriencj/python-promises.png?branch=master)](https://travis-ci.org/obriencj/python-promises) 5 | [![Coverage Status](https://coveralls.io/repos/obriencj/python-promises/badge.png?branch=master)](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 | --------------------------------------------------------------------------------