Merge pull request #1 from meejah/different-api-ideas
Different api ideas
Tobias Oberstein
9 years ago
0 | .PHONY: test docs pep8 | |
1 | ||
2 | default: test | |
3 | ||
4 | test: | |
5 | tox | |
6 | ||
7 | docs: | |
8 | cd doc && make html | |
9 | ||
10 | pep8: | |
11 | pep8 test/*.py txaio/*.py |
0 | # txaio | |
1 | Utilities to support code that runs unmodified on Twisted and asyncio | |
2 | ||
3 | This is like [six](http://pythonhosted.org/six/), but for wrapping over differences between Twisted and asyncio so one can write code that runs unmodified on both (aka "source code compatibility"). | |
4 | ||
5 | > Note that, with this approach, user code runs under the native event loop of either Twisted or asyncio. This is different from attaching either one's event loop to the other using some event loop adapter. |
0 | doc/index.rst⏎ |
0 | # Makefile for Sphinx documentation | |
1 | # | |
2 | ||
3 | # You can set these variables from the command line. | |
4 | SPHINXOPTS = | |
5 | SPHINXBUILD = sphinx-build | |
6 | PAPER = | |
7 | BUILDDIR = _build | |
8 | ||
9 | # User-friendly check for sphinx-build | |
10 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) | |
11 | $(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/) | |
12 | endif | |
13 | ||
14 | # Internal variables. | |
15 | PAPEROPT_a4 = -D latex_paper_size=a4 | |
16 | PAPEROPT_letter = -D latex_paper_size=letter | |
17 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | |
18 | # the i18n builder cannot share the environment and doctrees with the others | |
19 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | |
20 | ||
21 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext | |
22 | ||
23 | help: | |
24 | @echo "Please use \`make <target>' where <target> is one of" | |
25 | @echo " html to make standalone HTML files" | |
26 | @echo " dirhtml to make HTML files named index.html in directories" | |
27 | @echo " singlehtml to make a single large HTML file" | |
28 | @echo " pickle to make pickle files" | |
29 | @echo " json to make JSON files" | |
30 | @echo " htmlhelp to make HTML files and a HTML help project" | |
31 | @echo " qthelp to make HTML files and a qthelp project" | |
32 | @echo " applehelp to make an Apple Help Book" | |
33 | @echo " devhelp to make HTML files and a Devhelp project" | |
34 | @echo " epub to make an epub" | |
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" | |
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" | |
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" | |
38 | @echo " text to make text files" | |
39 | @echo " man to make manual pages" | |
40 | @echo " texinfo to make Texinfo files" | |
41 | @echo " info to make Texinfo files and run them through makeinfo" | |
42 | @echo " gettext to make PO message catalogs" | |
43 | @echo " changes to make an overview of all changed/added/deprecated items" | |
44 | @echo " xml to make Docutils-native XML files" | |
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" | |
46 | @echo " linkcheck to check all external links for integrity" | |
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" | |
48 | @echo " coverage to run coverage check of the documentation (if enabled)" | |
49 | ||
50 | clean: | |
51 | rm -rf $(BUILDDIR)/* | |
52 | ||
53 | html: | |
54 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html | |
55 | @echo | |
56 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." | |
57 | ||
58 | dirhtml: | |
59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml | |
60 | @echo | |
61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." | |
62 | ||
63 | singlehtml: | |
64 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml | |
65 | @echo | |
66 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." | |
67 | ||
68 | pickle: | |
69 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle | |
70 | @echo | |
71 | @echo "Build finished; now you can process the pickle files." | |
72 | ||
73 | json: | |
74 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json | |
75 | @echo | |
76 | @echo "Build finished; now you can process the JSON files." | |
77 | ||
78 | htmlhelp: | |
79 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp | |
80 | @echo | |
81 | @echo "Build finished; now you can run HTML Help Workshop with the" \ | |
82 | ".hhp project file in $(BUILDDIR)/htmlhelp." | |
83 | ||
84 | qthelp: | |
85 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp | |
86 | @echo | |
87 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ | |
88 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" | |
89 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/txaio.qhcp" | |
90 | @echo "To view the help file:" | |
91 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/txaio.qhc" | |
92 | ||
93 | applehelp: | |
94 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp | |
95 | @echo | |
96 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." | |
97 | @echo "N.B. You won't be able to view it unless you put it in" \ | |
98 | "~/Library/Documentation/Help or install it in your application" \ | |
99 | "bundle." | |
100 | ||
101 | devhelp: | |
102 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp | |
103 | @echo | |
104 | @echo "Build finished." | |
105 | @echo "To view the help file:" | |
106 | @echo "# mkdir -p $$HOME/.local/share/devhelp/txaio" | |
107 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/txaio" | |
108 | @echo "# devhelp" | |
109 | ||
110 | epub: | |
111 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub | |
112 | @echo | |
113 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." | |
114 | ||
115 | latex: | |
116 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
117 | @echo | |
118 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." | |
119 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ | |
120 | "(use \`make latexpdf' here to do that automatically)." | |
121 | ||
122 | latexpdf: | |
123 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
124 | @echo "Running LaTeX files through pdflatex..." | |
125 | $(MAKE) -C $(BUILDDIR)/latex all-pdf | |
126 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | |
127 | ||
128 | latexpdfja: | |
129 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | |
130 | @echo "Running LaTeX files through platex and dvipdfmx..." | |
131 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja | |
132 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | |
133 | ||
134 | text: | |
135 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text | |
136 | @echo | |
137 | @echo "Build finished. The text files are in $(BUILDDIR)/text." | |
138 | ||
139 | man: | |
140 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man | |
141 | @echo | |
142 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." | |
143 | ||
144 | texinfo: | |
145 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | |
146 | @echo | |
147 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." | |
148 | @echo "Run \`make' in that directory to run these through makeinfo" \ | |
149 | "(use \`make info' here to do that automatically)." | |
150 | ||
151 | info: | |
152 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | |
153 | @echo "Running Texinfo files through makeinfo..." | |
154 | make -C $(BUILDDIR)/texinfo info | |
155 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." | |
156 | ||
157 | gettext: | |
158 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale | |
159 | @echo | |
160 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." | |
161 | ||
162 | changes: | |
163 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes | |
164 | @echo | |
165 | @echo "The overview file is in $(BUILDDIR)/changes." | |
166 | ||
167 | linkcheck: | |
168 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck | |
169 | @echo | |
170 | @echo "Link check complete; look for any errors in the above output " \ | |
171 | "or in $(BUILDDIR)/linkcheck/output.txt." | |
172 | ||
173 | doctest: | |
174 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest | |
175 | @echo "Testing of doctests in the sources finished, look at the " \ | |
176 | "results in $(BUILDDIR)/doctest/output.txt." | |
177 | ||
178 | coverage: | |
179 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage | |
180 | @echo "Testing of coverage in the sources finished, look at the " \ | |
181 | "results in $(BUILDDIR)/coverage/python.txt." | |
182 | ||
183 | xml: | |
184 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml | |
185 | @echo | |
186 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." | |
187 | ||
188 | pseudoxml: | |
189 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml | |
190 | @echo | |
191 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." |
0 | txaio API | |
1 | ========= | |
2 | ||
3 | The API is identical whether you're using Twisted or asyncio under the | |
4 | hood. Two ``bool`` variables are available if you need to know which | |
5 | framework is in use, and two helpers to enforce one or the other framework. | |
6 | ||
7 | ||
8 | Explicitly Selecting a Framework | |
9 | -------------------------------- | |
10 | ||
11 | You can simply ``import txaio`` to get an auto-selected framework | |
12 | (Twisted if available, else asyncio/trollius). If you want to | |
13 | guarantee one or the other, you can do this: | |
14 | ||
15 | .. sourcecode:: python | |
16 | ||
17 | import txaio # automatically select framework | |
18 | txaio.use_twisted() | |
19 | txaio.use_asyncio() | |
20 | ||
21 | For most cases, a simple ``import txaio`` will be sufficient. | |
22 | ||
23 | ||
24 | Set an Event Loop / Reactor | |
25 | --------------------------- | |
26 | ||
27 | You can set ``txaio.config.loop`` to either an EventLoop instance (if | |
28 | using asyncio) or an explicit reactor (if using Twisted). By defualt, | |
29 | ``reactor`` is imported from ``twisted.internet`` on the first | |
30 | ``call_later`` invocation. For asyncio, ``asyncio.get_event_loop()`` | |
31 | is called at import time. | |
32 | ||
33 | If you've installed your reactor before ``import txaio`` you shouldn't | |
34 | need to do anything. | |
35 | ||
36 | Note that under Twisted, only the `IReactorTime`_ interface is | |
37 | required. | |
38 | ||
39 | ||
40 | Test Helpers | |
41 | ------------ | |
42 | ||
43 | Test utilities are in ``txaio.testutil``. There is a context-manager | |
44 | for testing delayed calls; see ``test_call_later.py`` for an example. | |
45 | ||
46 | .. automodule:: txaio.testutil | |
47 | ||
48 | ||
49 | txaio module | |
50 | ------------ | |
51 | ||
52 | .. py:module:: txaio | |
53 | ||
54 | .. py:data:: using_twisted | |
55 | ||
56 | ``True`` only if we're using Twisted as our underlying event framework | |
57 | ||
58 | ||
59 | .. py:data:: using_asyncio | |
60 | ||
61 | ``True`` only if we're using asyncio as our underlying event framework | |
62 | ||
63 | ||
64 | .. py:function:: use_asyncio() | |
65 | ||
66 | Force the use of ``asyncio``. | |
67 | ||
68 | ||
69 | .. py:function:: use_twisted() | |
70 | ||
71 | Force the use of Twisted. | |
72 | ||
73 | ||
74 | .. py:function:: create_future(value=None, error=None) | |
75 | ||
76 | Create and return a new framework-specific future object. On | |
77 | asyncio this returns a `Future`_, on Twisted it returns a | |
78 | `Deferred`_. | |
79 | ||
80 | :param value: if not ``None``, the future is already fulfilled, | |
81 | with the given result. | |
82 | ||
83 | :param error: if not ``None`` then the future is already failed, | |
84 | with the given error. | |
85 | :type error: class:`IFailedFuture` or Exception | |
86 | ||
87 | :raises ValueError: if both ``value`` and ``error`` are provided. | |
88 | ||
89 | :return: under Twisted a `Deferred`_, under asyncio a `Future`_ | |
90 | ||
91 | ||
92 | .. py:function:: as_future(func, *args, **kwargs) | |
93 | ||
94 | Call ``func`` with the provided arguments and keyword arguments, | |
95 | and always return a `Future`_/`Deferred`_. If ``func`` itself | |
96 | returns a future, that is directly returned. If it immediately | |
97 | succeed or failed then an already-resolved `Future`_/`Deferred`_ | |
98 | is returned instead. | |
99 | ||
100 | This allows you to write code that calls functions (e.g. possibly | |
101 | provided from user-code) and treat them uniformly. For example: | |
102 | ||
103 | .. sourcecode:: python | |
104 | ||
105 | p = txaio.as_future(some_function, 1, 2, key='word') | |
106 | txaio.add_callbacks(p, do_something, it_failed) | |
107 | ||
108 | You therefore don't have to worry if the underlying function was | |
109 | itself asynchronous or not -- your code always treats it as async. | |
110 | ||
111 | ||
112 | .. py:function:: reject(future, error=None) | |
113 | ||
114 | Resolve the given future as failed. This will call any errbacks | |
115 | registered against this Future/`Deferred`_. On Twisted, the errback | |
116 | is called with a bare `Failure`_ instance; on asyncio we provide | |
117 | an object that implements ``IFailedFuture`` because there is no | |
118 | equivalent in asyncio (this mimics part of the Failure API). | |
119 | ||
120 | :param future: an unresolved `Deferred`_/`Future`_ as returned by | |
121 | :meth:`create_future` | |
122 | ||
123 | :param error: The error to fail the `Deferred`_/`Future`_ with. If this | |
124 | is ``None``, ``sys.exc_info()`` is used to create an | |
125 | :class:`txaio.IFailedFuture` (or `Failure`_) | |
126 | wrapping the current exception (so in this case it | |
127 | must be called inside an ``except:`` clause). | |
128 | ||
129 | :type error: :class:`IFailedFuture` or :class:`Exception` | |
130 | ||
131 | ||
132 | .. py:function:: resolve(future, value) | |
133 | ||
134 | Resolve the given future with the provided value. This triggers | |
135 | any callbacks registered against this `Future`_/`Deferred`_. | |
136 | ||
137 | ||
138 | .. py:function:: add_callbacks(future, callback, errback) | |
139 | ||
140 | Adds the provided callback and/or errback to the given future. To | |
141 | add multiple callbacks, call this method multiple times. For | |
142 | example, to add just an errback, call ``add_callbacks(p, None, | |
143 | my_errback)`` | |
144 | ||
145 | Note that ``txaio`` doesn't do anything special with regards to | |
146 | callback or errback chaining -- it is highly recommended that you | |
147 | always return the incoming argument unmodified in your | |
148 | callback/errback so that Twisted and asyncio behave the same. For | |
149 | example: | |
150 | ||
151 | .. sourcecode:: python | |
152 | ||
153 | def callback_or_errback(value): | |
154 | # other code | |
155 | return value | |
156 | ||
157 | :raises ValueError: if both callback and errback are None | |
158 | ||
159 | ||
160 | .. py:function:: call_later(delay, func, *args, **kwargs) | |
161 | ||
162 | This calls the function ``func`` with the given parameters at the | |
163 | specified time in the future. Although asyncio doesn't directly | |
164 | support kwargs with ``loop.call_later`` we wrap it in a | |
165 | ``functools.partial``, as asyncio documentation suggests. | |
166 | ||
167 | :param delay: how many seconds in the future to make the call | |
168 | ||
169 | :returns: The underlying library object, which will at least have | |
170 | a ``.cancel()`` method on it. It's really | |
171 | `IDelayedCall`_ in Twisted and a `Handle`_ in asyncio. | |
172 | ||
173 | ||
174 | .. py:function:: gather(futures, consume_exceptions=True) | |
175 | ||
176 | Returns a new `Future`_ that waits for the results from all the | |
177 | futures provided. | |
178 | ||
179 | The `Future`_/`Deferred`_ returned will callback with a list the | |
180 | same length as ``futures`` containing either the return value from | |
181 | each future, or an :class:`IFailedFuture`/`Failure`_ instance if | |
182 | it failed. | |
183 | ||
184 | Note that on Twisted, we use `DeferredList`_ which usually | |
185 | returns a list of 2-tuples of ``(status, value)``. We do inject a | |
186 | callback that unpacks this list to be just the value (or | |
187 | `Failure`_) so that your callback can be identical on Twisted and | |
188 | asyncio. | |
189 | ||
190 | ||
191 | .. autoclass:: txaio.IFailedFuture | |
192 | ||
193 | ||
194 | .. _Autobahn|Python: http://autobahn.ws/python/ | |
195 | .. _Deferred: https://twistedmatrix.com/documents/current/api/twisted.internet.defer.Deferred.html | |
196 | .. _DeferredList: https://twistedmatrix.com/documents/current/api/twisted.internet.defer.DeferredList.html | |
197 | .. _Failure: https://twistedmatrix.com/documents/current/api/twisted.python.failure.Failure.html | |
198 | .. _IDelayedCall: https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IDelayedCall.html | |
199 | .. _IReactorTime: https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IReactorTime.html | |
200 | ||
201 | .. _Handle: https://docs.python.org/3.4/library/asyncio-eventloop.html#asyncio.Handle | |
202 | .. _Future: https://docs.python.org/3.4/library/asyncio-task.html#asyncio.Future |
0 | #!/usr/bin/env python3 | |
1 | # -*- coding: utf-8 -*- | |
2 | # | |
3 | # txaio documentation build configuration file, created by | |
4 | # sphinx-quickstart on Wed Mar 18 14:29:26 2015. | |
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 | import shlex | |
18 | ||
19 | # If extensions (or modules to document with autodoc) are in another directory, | |
20 | # add these directories to sys.path here. If the directory is relative to the | |
21 | # documentation root, use os.path.abspath to make it absolute, like shown here. | |
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 | ] | |
36 | ||
37 | # custom txaio extension configuration | |
38 | ||
39 | autodoc_member_order = 'bysource' | |
40 | autodoc_default_flags = ['members', 'show-inheritance', 'undoc-members'] | |
41 | autoclass_content = 'both' | |
42 | ||
43 | ||
44 | # Add any paths that contain templates here, relative to this directory. | |
45 | templates_path = ['_templates'] | |
46 | ||
47 | # The suffix(es) of source filenames. | |
48 | # You can specify multiple suffix as a list of string: | |
49 | # source_suffix = ['.rst', '.md'] | |
50 | source_suffix = '.rst' | |
51 | ||
52 | # The encoding of source files. | |
53 | #source_encoding = 'utf-8-sig' | |
54 | ||
55 | # The master toctree document. | |
56 | master_doc = 'index' | |
57 | ||
58 | # General information about the project. | |
59 | project = 'txaio' | |
60 | copyright = '2015, tavendo' | |
61 | author = 'tavendo' | |
62 | ||
63 | # The version info for the project you're documenting, acts as replacement for | |
64 | # |version| and |release|, also used in various other places throughout the | |
65 | # built documents. | |
66 | # | |
67 | # The short X.Y version. | |
68 | version = '0.0.0' | |
69 | # The full version, including alpha/beta/rc tags. | |
70 | release = '0.0.0' | |
71 | ||
72 | # The language for content autogenerated by Sphinx. Refer to documentation | |
73 | # for a list of supported languages. | |
74 | # | |
75 | # This is also used if you do content translation via gettext catalogs. | |
76 | # Usually you set "language" from the command line for these cases. | |
77 | language = None | |
78 | ||
79 | # There are two options for replacing |today|: either, you set today to some | |
80 | # non-false value, then it is used: | |
81 | #today = '' | |
82 | # Else, today_fmt is used as the format for a strftime call. | |
83 | #today_fmt = '%B %d, %Y' | |
84 | ||
85 | # List of patterns, relative to source directory, that match files and | |
86 | # directories to ignore when looking for source files. | |
87 | exclude_patterns = ['_build'] | |
88 | ||
89 | # The reST default role (used for this markup: `text`) to use for all | |
90 | # documents. | |
91 | #default_role = None | |
92 | ||
93 | # If true, '()' will be appended to :func: etc. cross-reference text. | |
94 | #add_function_parentheses = True | |
95 | ||
96 | # If true, the current module name will be prepended to all description | |
97 | # unit titles (such as .. function::). | |
98 | #add_module_names = True | |
99 | ||
100 | # If true, sectionauthor and moduleauthor directives will be shown in the | |
101 | # output. They are ignored by default. | |
102 | #show_authors = False | |
103 | ||
104 | # The name of the Pygments (syntax highlighting) style to use. | |
105 | pygments_style = 'sphinx' | |
106 | ||
107 | # A list of ignored prefixes for module index sorting. | |
108 | #modindex_common_prefix = [] | |
109 | ||
110 | # If true, keep warnings as "system message" paragraphs in the built documents. | |
111 | #keep_warnings = False | |
112 | ||
113 | # If true, `todo` and `todoList` produce output, else they produce nothing. | |
114 | todo_include_todos = False | |
115 | ||
116 | ||
117 | # -- Options for HTML output ---------------------------------------------- | |
118 | ||
119 | # The theme to use for HTML and HTML Help pages. See the documentation for | |
120 | # a list of builtin themes. | |
121 | html_theme = 'alabaster' | |
122 | ||
123 | # Theme options are theme-specific and customize the look and feel of a theme | |
124 | # further. For a list of options available for each theme, see the | |
125 | # documentation. | |
126 | #html_theme_options = {} | |
127 | ||
128 | # Add any paths that contain custom themes here, relative to this directory. | |
129 | #html_theme_path = [] | |
130 | ||
131 | # The name for this set of Sphinx documents. If None, it defaults to | |
132 | # "<project> v<release> documentation". | |
133 | #html_title = None | |
134 | ||
135 | # A shorter title for the navigation bar. Default is the same as html_title. | |
136 | #html_short_title = None | |
137 | ||
138 | # The name of an image file (relative to this directory) to place at the top | |
139 | # of the sidebar. | |
140 | #html_logo = None | |
141 | ||
142 | # The name of an image file (within the static path) to use as favicon of the | |
143 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 | |
144 | # pixels large. | |
145 | #html_favicon = None | |
146 | ||
147 | # Add any paths that contain custom static files (such as style sheets) here, | |
148 | # relative to this directory. They are copied after the builtin static files, | |
149 | # so a file named "default.css" will overwrite the builtin "default.css". | |
150 | html_static_path = ['_static'] | |
151 | ||
152 | # Add any extra paths that contain custom files (such as robots.txt or | |
153 | # .htaccess) here, relative to this directory. These files are copied | |
154 | # directly to the root of the documentation. | |
155 | #html_extra_path = [] | |
156 | ||
157 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, | |
158 | # using the given strftime format. | |
159 | #html_last_updated_fmt = '%b %d, %Y' | |
160 | ||
161 | # If true, SmartyPants will be used to convert quotes and dashes to | |
162 | # typographically correct entities. | |
163 | #html_use_smartypants = True | |
164 | ||
165 | # Custom sidebar templates, maps document names to template names. | |
166 | #html_sidebars = {} | |
167 | ||
168 | # Additional templates that should be rendered to pages, maps page names to | |
169 | # template names. | |
170 | #html_additional_pages = {} | |
171 | ||
172 | # If false, no module index is generated. | |
173 | #html_domain_indices = True | |
174 | ||
175 | # If false, no index is generated. | |
176 | #html_use_index = True | |
177 | ||
178 | # If true, the index is split into individual pages for each letter. | |
179 | #html_split_index = False | |
180 | ||
181 | # If true, links to the reST sources are added to the pages. | |
182 | #html_show_sourcelink = True | |
183 | ||
184 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. | |
185 | #html_show_sphinx = True | |
186 | ||
187 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. | |
188 | #html_show_copyright = True | |
189 | ||
190 | # If true, an OpenSearch description file will be output, and all pages will | |
191 | # contain a <link> tag referring to it. The value of this option must be the | |
192 | # base URL from which the finished HTML is served. | |
193 | #html_use_opensearch = '' | |
194 | ||
195 | # This is the file name suffix for HTML files (e.g. ".xhtml"). | |
196 | #html_file_suffix = None | |
197 | ||
198 | # Language to be used for generating the HTML full-text search index. | |
199 | # Sphinx supports the following languages: | |
200 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' | |
201 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' | |
202 | #html_search_language = 'en' | |
203 | ||
204 | # A dictionary with options for the search language support, empty by default. | |
205 | # Now only 'ja' uses this config value | |
206 | #html_search_options = {'type': 'default'} | |
207 | ||
208 | # The name of a javascript file (relative to the configuration directory) that | |
209 | # implements a search results scorer. If empty, the default will be used. | |
210 | #html_search_scorer = 'scorer.js' | |
211 | ||
212 | # Output file base name for HTML help builder. | |
213 | htmlhelp_basename = 'txaiodoc' | |
214 | ||
215 | # -- Options for LaTeX output --------------------------------------------- | |
216 | ||
217 | latex_elements = { | |
218 | # The paper size ('letterpaper' or 'a4paper'). | |
219 | #'papersize': 'letterpaper', | |
220 | ||
221 | # The font size ('10pt', '11pt' or '12pt'). | |
222 | #'pointsize': '10pt', | |
223 | ||
224 | # Additional stuff for the LaTeX preamble. | |
225 | #'preamble': '', | |
226 | ||
227 | # Latex figure (float) alignment | |
228 | #'figure_align': 'htbp', | |
229 | } | |
230 | ||
231 | # Grouping the document tree into LaTeX files. List of tuples | |
232 | # (source start file, target name, title, | |
233 | # author, documentclass [howto, manual, or own class]). | |
234 | latex_documents = [ | |
235 | (master_doc, 'txaio.tex', 'txaio Documentation', | |
236 | 'tavendo', 'manual'), | |
237 | ] | |
238 | ||
239 | # The name of an image file (relative to this directory) to place at the top of | |
240 | # the title page. | |
241 | #latex_logo = None | |
242 | ||
243 | # For "manual" documents, if this is true, then toplevel headings are parts, | |
244 | # not chapters. | |
245 | #latex_use_parts = False | |
246 | ||
247 | # If true, show page references after internal links. | |
248 | #latex_show_pagerefs = False | |
249 | ||
250 | # If true, show URL addresses after external links. | |
251 | #latex_show_urls = False | |
252 | ||
253 | # Documents to append as an appendix to all manuals. | |
254 | #latex_appendices = [] | |
255 | ||
256 | # If false, no module index is generated. | |
257 | #latex_domain_indices = True | |
258 | ||
259 | ||
260 | # -- Options for manual page output --------------------------------------- | |
261 | ||
262 | # One entry per manual page. List of tuples | |
263 | # (source start file, name, description, authors, manual section). | |
264 | man_pages = [ | |
265 | (master_doc, 'txaio', 'txaio Documentation', | |
266 | [author], 1) | |
267 | ] | |
268 | ||
269 | # If true, show URL addresses after external links. | |
270 | #man_show_urls = False | |
271 | ||
272 | ||
273 | # -- Options for Texinfo output ------------------------------------------- | |
274 | ||
275 | # Grouping the document tree into Texinfo files. List of tuples | |
276 | # (source start file, target name, title, author, | |
277 | # dir menu entry, description, category) | |
278 | texinfo_documents = [ | |
279 | (master_doc, 'txaio', 'txaio Documentation', | |
280 | author, 'txaio', 'One line description of project.', | |
281 | 'Miscellaneous'), | |
282 | ] | |
283 | ||
284 | # Documents to append as an appendix to all manuals. | |
285 | #texinfo_appendices = [] | |
286 | ||
287 | # If false, no module index is generated. | |
288 | #texinfo_domain_indices = True | |
289 | ||
290 | # How to display URL addresses: 'footnote', 'no', or 'inline'. | |
291 | #texinfo_show_urls = 'footnote' | |
292 | ||
293 | # If true, do not generate a @detailmenu in the "Top" node's menu. | |
294 | #texinfo_no_detailmenu = False |
0 | txaio: Twisted/asyncio helper | |
1 | ============================= | |
2 | ||
3 | ``txaio`` is a helper library for writing code that runs on both | |
4 | Twisted and asyncio. | |
5 | ||
6 | This is like `six`_, but for wrapping over differences between Twisted | |
7 | and asyncio so one can write code that runs unmodified on both (aka | |
8 | "source code compatibility"). | |
9 | ||
10 | .. _six: http://pythonhosted.org/six/ | |
11 | ||
12 | Note that, with this approach, user code runs under the native | |
13 | event loop of either Twisted or asyncio. This is different from | |
14 | attaching either one's event loop to the other using some event | |
15 | loop adapter. | |
16 | ||
17 | Brief Summary | |
18 | ------------- | |
19 | ||
20 | Instead of directly importing, instantiating and using ``Deferred`` | |
21 | (for Twisted) or ``Future`` (for asyncio) objects, ``txaio`` provides | |
22 | helper-functions to do that for you, as well as associated things like | |
23 | adding callbacks or errbacks. | |
24 | ||
25 | This obviously changes the style of your code, but then you can choose | |
26 | at runtime (or import time) which underlying event-loop to use. This | |
27 | means you can write **one** code-base that can run on Twisted *or* | |
28 | asyncio (without a Twisted dependency) as you (or your users) see fit. | |
29 | ||
30 | Code like the following can then run on *either* system: | |
31 | ||
32 | .. sourcecode:: python | |
33 | ||
34 | f0 = txaio.create_future() | |
35 | f1 = txaio.as_future(some_func, 1, 2, key='word') | |
36 | txaio.add_callbacks(f0, callback, errback) | |
37 | txaio.add_callbacks(f1, callback, errback) | |
38 | # ... | |
39 | txaio.resolve(f0, "value") | |
40 | txaio.reject(f1, RuntimeError("it failed")) | |
41 | ||
42 | ||
43 | See :ref:`restrictions` for limitations. | |
44 | ||
45 | .. toctree:: | |
46 | :maxdepth: 3 | |
47 | ||
48 | overview | |
49 | api |
0 | txaio Overview | |
1 | ============== | |
2 | ||
3 | ||
4 | Brief History | |
5 | ------------- | |
6 | ||
7 | This library has been factored out of the `Autobahn|Python`_ WAMP client | |
8 | library. The ``ApplicationSession`` object from that project therefore | |
9 | serves as a good example of how to use this library in a complex | |
10 | use-case. See | |
11 | https://github.com/tavendo/AutobahnPython/blob/master/autobahn/wamp/protocol.py#L410 | |
12 | ||
13 | We are releasing it in the hopes these utilities are useful on their | |
14 | own to other projects using event-based Python. | |
15 | ||
16 | ||
17 | Overview by Example | |
18 | ------------------- | |
19 | ||
20 | The simplest way to use ``txaio`` is to ``import txaio`` and use the | |
21 | helper functions directly. Using the library in this manner will | |
22 | automatically select the event-loop to use: Twisted if it's available, | |
23 | then asyncio and finally Trollius if those fail. | |
24 | ||
25 | If you wish to be explicit about the event-loop you want, call | |
26 | :func:`txaio.use_twisted` or :func:`txaio.use_asyncio` which will fail | |
27 | (with an ``ImportError``) if that implementation isn't available. | |
28 | ||
29 | Note that to use this library successfully you *shouldn't* call | |
30 | methods on futures -- use *only* ``txaio`` methods to operate on them. | |
31 | ||
32 | .. sourcecode:: python | |
33 | ||
34 | import txaio | |
35 | ||
36 | def cb(value): | |
37 | print("Callback:", value) | |
38 | ||
39 | def eb(fail): | |
40 | # fail will implement txaio.IFailedPromise | |
41 | print("Errback:", fail) | |
42 | fail.printTraceback() | |
43 | ||
44 | f = txaio.create_future() | |
45 | txaio.add_callbacks(f, cb, eb) | |
46 | ||
47 | # ...other things happen... | |
48 | ||
49 | try: | |
50 | answer = do_something() | |
51 | fail = None | |
52 | except Exception: | |
53 | fail = txaio.create_failure() | |
54 | ||
55 | # the point here is that you "somehow" arrange to call either | |
56 | # reject() or resolve() on every future you've created. | |
57 | ||
58 | if fail: | |
59 | txaio.reject(f, fail) | |
60 | else: | |
61 | txaio.resolve(f, answer) | |
62 | ||
63 | .. _restrictions: | |
64 | ||
65 | Restrictions and Caveats | |
66 | ------------------------ | |
67 | ||
68 | ``txaio`` is not a new event-based programming solution. It is not a | |
69 | complete box-set of async tools. | |
70 | ||
71 | It is **one piece** that *can* help you to write cross-event-loop | |
72 | asynchronous code. For example, you'll note that there's no way to run | |
73 | "the event loop" -- that's up to you. | |
74 | ||
75 | However, it is basically a "lowest common denominator" tool. There is | |
76 | a minimum of wrapping, etcetera. | |
77 | ||
78 | ||
79 | Futures and Deferreds | |
80 | --------------------- | |
81 | ||
82 | In most cases asyncio is trying to be "as thin as possible" wrapper | |
83 | around the different APIs. So, there's nothing wrapping Future or | |
84 | Deferred -- you get the bare objects. This means that | |
85 | :func:`txaio.create_future` returns you the native object, which | |
86 | you then pass to :func:`txaio.add_callbacks` | |
87 | ||
88 | Similarly, :func:`txaio.call_later` returns the underlying object | |
89 | (``IDelayedCall`` in Twisted or a ``Handle`` in asyncio). These both | |
90 | have a ``cancel()`` method, but little else in common. | |
91 | ||
92 | ||
93 | Callbacks and Errbacks | |
94 | ---------------------- | |
95 | ||
96 | Twisted and asyncio have made different design-decisions. One that | |
97 | stands out is callbacks, and callback chaining. In Twisted, the return | |
98 | value from an earlier callback is what gets passed to the next | |
99 | callback. Similarly, errbacks in Twisted can cancel the error. There | |
100 | are not equivalent facilities in ``asyncio``: if you add multiple | |
101 | callbacks, they all get the same value (or exception). | |
102 | ||
103 | When using ``txaio``, **don't depend on chaining**. This means that | |
104 | your ``callback`` and ``errback`` methods must **always return their | |
105 | input argument** so that Twisted works if you add multiple callbacks | |
106 | or errbacks (and doesn't unexpectedly cancel errors). | |
107 | ||
108 | ``txaio`` does add the concept of an ``errback`` for handling errors | |
109 | (a concept asyncio does not have) and therefore adds one helper to | |
110 | encapsulate exceptions (similar to Twisted's `Failure`_ object) which | |
111 | only exists in the asyncio implementation. | |
112 | ||
113 | There is no ``inlineCallbacks`` or ``coroutine`` decorator | |
114 | support. Don't use these. | |
115 | ||
116 | ||
117 | Real Examples | |
118 | ------------- | |
119 | ||
120 | You are encouraged to look at `Autobahn|Python`_ for an example of a | |
121 | system that can run on both Twisted and asyncio. In particular, look | |
122 | at the difference between ``autobahn/twisted/websocket.py`` and | |
123 | ``autobahn/asyncio/websocket.py`` and the compatibility super-class in | |
124 | ``autobahn/wamp/protocol.py`` which is the piece that uses ``txaio`` | |
125 | to provide an event-loop agnostic implementation that both the Twisted | |
126 | and asyncio concrete ``ApplicationSession`` objects inherit from. | |
127 | ||
128 | ``autobahn.wamp.protocol.ApplicationSession`` is glued to a particular | |
129 | event-loop via ``autobahn.twisted.wamp.ApplicationSession`` which | |
130 | takes advantage of ``txaio.tx.LoopMixin`` to provde the | |
131 | helpers-methods attached to ``self``. | |
132 | ||
133 | In this manner, code in the generic implementation simply always calls | |
134 | ``txaio`` methods via ``self.create_future()`` or similar and users of | |
135 | `Autobahn|Python`_ can choose between asyncio and Twisted as they prefer | |
136 | by either ``from autobahn.twisted.wamp import ApplicationSession`` or | |
137 | ``from autobahn.asyncio.wamp import ApplicationSession`` | |
138 | ||
139 | ||
140 | Cross-API Magic | |
141 | --------------- | |
142 | ||
143 | If you wish to write Twisted-like code that uses ``asyncio`` as its | |
144 | event-loop, you should look at `txtulip | |
145 | <https://github.com/itamarst/txtulip>`_. I do not know of a project | |
146 | that lets you write asyncio-like code that runs on Twisted's | |
147 | event-loop. | |
148 | ||
149 | ||
150 | .. _Autobahn|Python: http://autobahn.ws/python/ | |
151 | .. _Failure: https://twistedmatrix.com/documents/current/api/twisted.python.failure.Failure.html |
0 | from __future__ import print_function | |
1 | import txaio.aio as txaio | |
2 | ||
3 | # note: all the code below is *identical* to the use_asyncio.py | |
4 | # example, except for the import line above and event-loop running | |
5 | ||
6 | def cb(value): | |
7 | print("Callback:", value) | |
8 | ||
9 | def eb(fail): | |
10 | # fail will implement txaio.IFailedPromise | |
11 | print("Errback:", fail) | |
12 | fail.printTraceback() | |
13 | ||
14 | f = txaio.create_future() | |
15 | txaio.add_future_callbacks(f, cb, eb) | |
16 | ||
17 | # ...other things happen... | |
18 | ||
19 | def do_something(): | |
20 | if False: | |
21 | return "at least it's something" | |
22 | raise RuntimeError("sadness") | |
23 | ||
24 | try: | |
25 | answer = do_something() | |
26 | fail = None | |
27 | except Exception: | |
28 | fail = txaio.create_failure() | |
29 | ||
30 | if fail: | |
31 | txaio.reject_future(f, fail) | |
32 | else: | |
33 | txaio.resolve_future(f, answer) | |
34 | ||
35 | # Arrange for our event-loop to run. | |
36 | try: | |
37 | import asyncio | |
38 | except ImportError: | |
39 | import trollius as asyncio | |
40 | loop = asyncio.get_event_loop() | |
41 | loop.stop() | |
42 | loop.run_forever() |
0 | from __future__ import print_function | |
1 | import txaio.tx as txaio | |
2 | ||
3 | # note: all the code below is *identical* to the use_asyncio.py | |
4 | # example, except for the import line above and event-loop running | |
5 | ||
6 | def cb(value): | |
7 | print("Callback:", value) | |
8 | ||
9 | def eb(fail): | |
10 | # fail will implement txaio.IFailedPromise | |
11 | print("Errback:", fail) | |
12 | fail.printTraceback() | |
13 | ||
14 | f = txaio.create_future() | |
15 | txaio.add_future_callbacks(f, cb, eb) | |
16 | ||
17 | # ...other things happen... | |
18 | ||
19 | def do_something(): | |
20 | if False: | |
21 | return "at least it's something" | |
22 | raise RuntimeError("sadness") | |
23 | ||
24 | try: | |
25 | answer = do_something() | |
26 | fail = None | |
27 | except Exception: | |
28 | fail = txaio.create_failure() | |
29 | ||
30 | if fail: | |
31 | txaio.reject_future(f, fail) | |
32 | else: | |
33 | txaio.resolve_future(f, answer) | |
34 | ||
35 | # note that in this simple example (because Twisted will resolve | |
36 | # callbacks immediately when results are already available) we don't | |
37 | # actually need to enter the event-loop. In a "real" program you'd | |
38 | # have to arrange to call ``reactor.run()`` or ``react()`` at some | |
39 | # point. |
0 | ############################################################################## | |
1 | # | |
2 | # Copyright (C) 2011-2015 Tavendo GmbH | |
3 | # | |
4 | # This program is free software: you can redistribute it and/or modify | |
5 | # it under the terms of the GNU Affero General Public License, version 3, | |
6 | # as published by the Free Software Foundation. | |
7 | # | |
8 | # This program is distributed in the hope that it will be useful, | |
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | # GNU Affero General Public License for more details. | |
12 | # | |
13 | # You should have received a copy of the GNU Affero General Public License | |
14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
15 | # | |
16 | ############################################################################## | |
17 | ||
18 | from __future__ import absolute_import | |
19 | ||
20 | import sys | |
21 | from setuptools import setup, find_packages | |
22 | ||
23 | # XXX FIXME | |
24 | verstr = '0.0.0' | |
25 | docstr = 'FIXME' | |
26 | ||
27 | setup ( | |
28 | name='taxio', | |
29 | version=verstr, | |
30 | description='compatibility API between asyncio/Twisted/Trollius', | |
31 | long_description=docstr, | |
32 | author='Tavendo GmbH', | |
33 | author_email='FIXME', | |
34 | url='FIXME', | |
35 | platforms=('Any'), | |
36 | install_requires=[ | |
37 | 'six' | |
38 | ], | |
39 | extras_require={ | |
40 | 'dev': [ | |
41 | 'pytest>=2.6.4', # FIXME | |
42 | 'pytest-cov>=1.8.1', # FIXME | |
43 | 'pep8>=1.6.2', # MIT | |
44 | ||
45 | 'Sphinx>=1.2.3', # BSD | |
46 | 'alabaster>=0.6.3', # BSD | |
47 | ], | |
48 | 'twisted': [ | |
49 | 'twisted', | |
50 | ] | |
51 | }, | |
52 | packages=['txaio'], | |
53 | # include_package_data=True, | |
54 | # data_files=[('.', ['LICENSE'])], | |
55 | zip_safe=False, | |
56 | # http://pypi.python.org/pypi?%3Aaction=list_classifiers | |
57 | # | |
58 | classifiers=[ | |
59 | "License :: OSI Approved :: MIT License", | |
60 | "Development Status :: 3 - Alpha", | |
61 | "Environment :: Console", | |
62 | "Framework :: Twisted", | |
63 | "Intended Audience :: Developers", | |
64 | "Operating System :: OS Independent", | |
65 | "Programming Language :: Python", | |
66 | "Topic :: Software Development :: Libraries", | |
67 | "Topic :: Software Development :: Libraries :: Application Frameworks", | |
68 | ], | |
69 | keywords='asyncio twisted coroutine', | |
70 | ) |
0 | import txaio | |
1 | ||
2 | from util import run_once | |
3 | ||
4 | ||
5 | def test_as_future_immediate(): | |
6 | ''' | |
7 | Returning an immediate value from as_future | |
8 | ''' | |
9 | errors = [] | |
10 | results = [] | |
11 | calls = [] | |
12 | ||
13 | def method(*args, **kw): | |
14 | calls.append((args, kw)) | |
15 | return 42 | |
16 | f = txaio.as_future(method, 1, 2, 3, key='word') | |
17 | ||
18 | def cb(x): | |
19 | results.append(x) | |
20 | ||
21 | def errback(f): | |
22 | errors.append(f) | |
23 | ||
24 | txaio.add_callbacks(f, cb, errback) | |
25 | ||
26 | run_once() | |
27 | ||
28 | assert len(results) == 1 | |
29 | assert len(errors) == 0 | |
30 | assert results[0] == 42 | |
31 | assert calls[0] == ((1, 2, 3), dict(key='word')) | |
32 | ||
33 | ||
34 | def test_as_future_exception(): | |
35 | ''' | |
36 | Raises an exception from as_future | |
37 | ''' | |
38 | errors = [] | |
39 | results = [] | |
40 | calls = [] | |
41 | exception = RuntimeError("sadness") | |
42 | ||
43 | def method(*args, **kw): | |
44 | calls.append((args, kw)) | |
45 | raise exception | |
46 | f = txaio.as_future(method, 1, 2, 3, key='word') | |
47 | ||
48 | def cb(x): | |
49 | results.append(x) | |
50 | ||
51 | def errback(f): | |
52 | errors.append(f) | |
53 | ||
54 | txaio.add_callbacks(f, cb, errback) | |
55 | ||
56 | run_once() | |
57 | ||
58 | assert len(results) == 0 | |
59 | assert len(errors) == 1 | |
60 | assert errors[0].value == exception | |
61 | assert calls[0] == ((1, 2, 3), dict(key='word')) | |
62 | ||
63 | ||
64 | def test_as_future_recursive(): | |
65 | ''' | |
66 | Returns another Future from as_future | |
67 | ''' | |
68 | errors = [] | |
69 | results = [] | |
70 | calls = [] | |
71 | f1 = txaio.create_future_success(42) | |
72 | ||
73 | def method(*args, **kw): | |
74 | calls.append((args, kw)) | |
75 | return f1 | |
76 | f0 = txaio.as_future(method, 1, 2, 3, key='word') | |
77 | ||
78 | def cb(x): | |
79 | results.append(x) | |
80 | ||
81 | def errback(f): | |
82 | errors.append(f) | |
83 | ||
84 | txaio.add_callbacks(f0, cb, errback) | |
85 | ||
86 | run_once() | |
87 | ||
88 | assert len(results) == 1 | |
89 | assert len(errors) == 0 | |
90 | assert results[0] == 42 | |
91 | assert calls[0] == ((1, 2, 3), dict(key='word')) |
0 | import pytest | |
1 | import txaio | |
2 | from txaio.testutil import replace_loop | |
3 | ||
4 | ||
5 | def test_call_later(): | |
6 | ''' | |
7 | Wait for two Futures. | |
8 | ''' | |
9 | ||
10 | # set up a test reactor or event-loop depending on asyncio or | |
11 | # Twisted | |
12 | twisted = False | |
13 | try: | |
14 | from twisted.internet.task import Clock | |
15 | new_loop = Clock() | |
16 | twisted = True | |
17 | except ImportError: | |
18 | # Trollius doesn't come with this, so won't work on py2 | |
19 | pytest.importorskip('asyncio.test_utils') | |
20 | ||
21 | def time_gen(): | |
22 | when = yield | |
23 | assert when == 1 | |
24 | # even though we only do one call, I guess TestLoop needs | |
25 | # a "trailing" yield? "or something" | |
26 | when = yield 0 | |
27 | print("Hmmm", when) | |
28 | from asyncio.test_utils import TestLoop | |
29 | new_loop = TestLoop(time_gen) | |
30 | ||
31 | calls = [] | |
32 | with replace_loop(new_loop) as fake_loop: | |
33 | def foo(*args, **kw): | |
34 | calls.append((args, kw)) | |
35 | ||
36 | delay = txaio.call_later(1, foo, 5, 6, 7, foo="bar") | |
37 | assert len(calls) == 0 | |
38 | assert hasattr(delay, 'cancel') | |
39 | if twisted: | |
40 | fake_loop.advance(2) | |
41 | else: | |
42 | # XXX maybe we monkey-patch a ".advance()" onto asyncio | |
43 | # loops that does both of these? | |
44 | fake_loop.advance_time(2) | |
45 | fake_loop._run_once() | |
46 | ||
47 | assert len(calls) == 1 | |
48 | assert calls[0][0] == (5, 6, 7) | |
49 | assert calls[0][1] == dict(foo="bar") |
0 | import txaio | |
1 | ||
2 | from util import run_once | |
3 | ||
4 | ||
5 | def test_callback(): | |
6 | f = txaio.create_future() | |
7 | results = [] | |
8 | ||
9 | def cb(f): | |
10 | results.append(f) | |
11 | txaio.add_callbacks(f, cb, None) | |
12 | txaio.resolve(f, "it worked") | |
13 | ||
14 | run_once() | |
15 | ||
16 | assert len(results) == 1 | |
17 | assert results[0] == "it worked" | |
18 | ||
19 | ||
20 | def test_chained_callback(): | |
21 | """ | |
22 | Chain two callbacks where the first one alters the value. | |
23 | """ | |
24 | calls = [] | |
25 | ||
26 | def callback0(arg): | |
27 | calls.append(arg) | |
28 | return arg + " pray I do not alter it futher" | |
29 | ||
30 | def callback1(arg): | |
31 | calls.append(arg) | |
32 | ||
33 | f = txaio.create_future() | |
34 | txaio.add_callbacks(f, callback0, None) | |
35 | txaio.add_callbacks(f, callback1, None) | |
36 | txaio.resolve(f, "the deal") | |
37 | ||
38 | run_once() | |
39 | ||
40 | assert len(calls) == 2 | |
41 | assert calls[0] == "the deal" | |
42 | assert calls[1] == "the deal pray I do not alter it futher" | |
43 | ||
44 | ||
45 | def test_immediate_result(): | |
46 | f = txaio.create_future_success("it worked") | |
47 | results = [] | |
48 | ||
49 | def cb(f): | |
50 | results.append(f) | |
51 | txaio.add_callbacks(f, cb, None) | |
52 | ||
53 | run_once() | |
54 | ||
55 | assert len(results) == 1 | |
56 | assert results[0] == "it worked" |
0 | from six import StringIO | |
1 | import txaio | |
2 | ||
3 | from util import run_once | |
4 | ||
5 | ||
6 | def test_errback(): | |
7 | f = txaio.create_future() | |
8 | exception = RuntimeError("it failed") | |
9 | errors = [] | |
10 | ||
11 | def err(f): | |
12 | errors.append(f) | |
13 | txaio.add_callbacks(f, None, err) | |
14 | try: | |
15 | raise exception | |
16 | except: | |
17 | fail = txaio.create_failure() | |
18 | txaio.reject(f, fail) | |
19 | ||
20 | run_once() | |
21 | ||
22 | assert len(errors) == 1 | |
23 | assert isinstance(errors[0], txaio.IFailedFuture) | |
24 | assert exception == errors[0].value | |
25 | assert type(exception) == errors[0].type | |
26 | assert errors[0].tb is not None | |
27 | tb = StringIO() | |
28 | errors[0].printTraceback(file=tb) | |
29 | assert 'RuntimeError' in tb.getvalue() | |
30 | assert 'it failed' in tb.getvalue() | |
31 | assert errors[0].getErrorMessage() == 'it failed' | |
32 | assert 'it failed' in str(errors[0]) | |
33 | ||
34 | ||
35 | def test_errback_without_except(): | |
36 | ''' | |
37 | Create a failure without an except block | |
38 | ''' | |
39 | f = txaio.create_future() | |
40 | exception = RuntimeError("it failed") | |
41 | errors = [] | |
42 | ||
43 | def err(f): | |
44 | errors.append(f) | |
45 | txaio.add_callbacks(f, None, err) | |
46 | fail = txaio.create_failure(exception) | |
47 | txaio.reject(f, fail) | |
48 | ||
49 | run_once() | |
50 | ||
51 | assert len(errors) == 1 | |
52 | assert isinstance(errors[0], txaio.IFailedFuture) | |
53 | assert exception == errors[0].value | |
54 | assert type(exception) == errors[0].type | |
55 | tb = StringIO() | |
56 | errors[0].printTraceback(file=tb) | |
57 | assert 'RuntimeError' in tb.getvalue() | |
58 | assert 'it failed' in tb.getvalue() | |
59 | assert errors[0].getErrorMessage() == 'it failed' | |
60 | assert 'it failed' in str(errors[0]) | |
61 | ||
62 | ||
63 | def test_errback_reject_no_args(): | |
64 | """ | |
65 | txaio.reject() with no args | |
66 | """ | |
67 | ||
68 | f = txaio.create_future() | |
69 | exception = RuntimeError("it failed") | |
70 | errors = [] | |
71 | ||
72 | def err(f): | |
73 | errors.append(f) | |
74 | txaio.add_callbacks(f, None, err) | |
75 | try: | |
76 | raise exception | |
77 | except: | |
78 | txaio.reject(f) | |
79 | ||
80 | run_once() | |
81 | ||
82 | assert len(errors) == 1 | |
83 | assert isinstance(errors[0], txaio.IFailedFuture) | |
84 | assert exception == errors[0].value | |
85 | assert type(exception) == errors[0].type | |
86 | assert errors[0].tb is not None | |
87 | tb = StringIO() | |
88 | errors[0].printTraceback(file=tb) | |
89 | assert 'RuntimeError' in tb.getvalue() | |
90 | assert 'it failed' in tb.getvalue() | |
91 | assert errors[0].getErrorMessage() == 'it failed' | |
92 | assert 'it failed' in str(errors[0]) | |
93 | ||
94 | ||
95 | def test_immediate_failure(): | |
96 | exception = RuntimeError("it failed") | |
97 | try: | |
98 | raise exception | |
99 | except: | |
100 | f0 = txaio.create_future_error() | |
101 | fail = txaio.create_failure() | |
102 | ||
103 | errors = [] | |
104 | results = [] | |
105 | f1 = txaio.create_future_error(fail) | |
106 | ||
107 | def cb(x): | |
108 | results.append(x) | |
109 | ||
110 | def errback(f): | |
111 | errors.append(f) | |
112 | ||
113 | txaio.add_callbacks(f0, cb, errback) | |
114 | txaio.add_callbacks(f1, cb, errback) | |
115 | ||
116 | run_once() | |
117 | run_once() | |
118 | run_once() | |
119 | ||
120 | assert len(results) == 0 | |
121 | assert len(errors) == 2 | |
122 | assert isinstance(errors[0], txaio.IFailedFuture) | |
123 | assert isinstance(errors[1], txaio.IFailedFuture) | |
124 | assert errors[0].value == exception | |
125 | assert errors[1].value == exception | |
126 | # should be distinct FailedPromise instances | |
127 | assert id(errors[0]) != id(errors[1]) |
0 | import txaio | |
1 | ||
2 | from util import await | |
3 | ||
4 | ||
5 | def test_gather_two(): | |
6 | ''' | |
7 | Wait for two Futures. | |
8 | ''' | |
9 | ||
10 | errors = [] | |
11 | results = [] | |
12 | calls = [] | |
13 | ||
14 | def foo(): | |
15 | def codependant(*args, **kw): | |
16 | calls.append((args, kw)) | |
17 | return 42 | |
18 | return txaio.as_future(codependant) | |
19 | ||
20 | def method(*args, **kw): | |
21 | calls.append((args, kw)) | |
22 | return "OHAI" | |
23 | f0 = txaio.as_future(method, 1, 2, 3, key='word') | |
24 | f1 = txaio.as_future(foo) | |
25 | ||
26 | f2 = txaio.gather([f0, f1]) | |
27 | ||
28 | def done(arg): | |
29 | results.append(arg) | |
30 | ||
31 | def error(fail): | |
32 | errors.append(fail) | |
33 | # fail.printTraceback() | |
34 | txaio.add_callbacks(f2, done, error) | |
35 | ||
36 | await(f0) | |
37 | await(f1) | |
38 | await(f2) | |
39 | ||
40 | assert len(results) == 1 | |
41 | assert len(errors) == 0 | |
42 | assert results[0] == ['OHAI', 42] or results[0] == [42, 'OHAI'] | |
43 | assert len(calls) == 2 | |
44 | assert calls[0] == ((1, 2, 3), dict(key='word')) | |
45 | assert calls[1] == (tuple(), dict()) | |
46 | ||
47 | ||
48 | def test_gather_no_consume(): | |
49 | ''' | |
50 | consume_exceptions=False | |
51 | ''' | |
52 | ||
53 | errors = [] | |
54 | results = [] | |
55 | calls = [] | |
56 | ||
57 | f0 = txaio.create_future_error(error=RuntimeError("f0 failed")) | |
58 | f1 = txaio.create_future_error(error=RuntimeError("f1 failed")) | |
59 | ||
60 | f2 = txaio.gather([f0, f1], consume_exceptions=False) | |
61 | ||
62 | def done(arg): | |
63 | results.append(arg) | |
64 | ||
65 | def error(fail): | |
66 | errors.append(fail) | |
67 | # fail.printTraceback() | |
68 | txaio.add_callbacks(f0, done, error) | |
69 | txaio.add_callbacks(f1, done, error) | |
70 | txaio.add_callbacks(f2, done, error) | |
71 | ||
72 | # FIXME more testing annoyance; the propogated errors are raise | |
73 | # out of "run_until_complete()" as well; fix util.py | |
74 | for f in [f0, f1, f2]: | |
75 | try: | |
76 | await(f) | |
77 | except: | |
78 | pass | |
79 | ||
80 | assert len(results) == 0 | |
81 | assert len(errors) == 3 | |
82 | assert len(calls) == 0 |
0 | try: | |
1 | import asyncio | |
2 | from asyncio.test_utils import run_once as _run_once | |
3 | ||
4 | def run_once(): | |
5 | return _run_once(asyncio.get_event_loop()) | |
6 | ||
7 | except ImportError as e: | |
8 | try: | |
9 | import trollius as asyncio | |
10 | except ImportError: | |
11 | asyncio = None | |
12 | ||
13 | def run_once(): | |
14 | ''' | |
15 | copied from asyncio.testutils because trollius has no | |
16 | "testutils" | |
17 | ''' | |
18 | # in Twisted, this method is a no-op | |
19 | if asyncio is None: | |
20 | return | |
21 | ||
22 | # just like modern asyncio.testutils.run_once does it... | |
23 | loop = asyncio.get_event_loop() | |
24 | loop.stop() | |
25 | loop.run_forever() | |
26 | asyncio.gather(*asyncio.Task.all_tasks()) | |
27 | ||
28 | ||
29 | try: | |
30 | # XXX fixme hack better way to detect twisted | |
31 | # (has to work on py3 where asyncio exists always, though) | |
32 | import twisted # noqa | |
33 | ||
34 | def await(_): | |
35 | return | |
36 | ||
37 | except ImportError: | |
38 | def await(future): | |
39 | asyncio.get_event_loop().run_until_complete(future) |
0 | [tox] | |
1 | envlist = | |
2 | py{27,py}-{twisted,asyncio}, | |
3 | py{34}-asyncio, | |
4 | py34-twisted, | |
5 | flake8 | |
6 | ||
7 | ||
8 | [testenv] | |
9 | deps = | |
10 | six | |
11 | mock | |
12 | pytest | |
13 | pytest-cov | |
14 | py{26,27,34,py}-twisted: twisted | |
15 | py{26,27,34,py}-twisted: pytest-twisted | |
16 | py33-asyncio: asyncio | |
17 | py27-asyncio: trollius | |
18 | py26-asyncio: trollius | |
19 | pypy-asyncio: trollius | |
20 | changedir=test | |
21 | commands = | |
22 | py.test -s --basetemp={envtmpdir} --cov txaio --cov-report term-missing | |
23 | # coverage report --show-missing | |
24 | # coverage html | |
25 | ||
26 | [testenv:flake8] | |
27 | deps = | |
28 | flake8 | |
29 | changedir=. | |
30 | commands = | |
31 | flake8 txaio/ test/ |
0 | from txaio.interfaces import IFailedFuture | |
1 | ||
2 | # This is the API | |
3 | # see tx.py for Twisted implementation | |
4 | # see aio.py for asyncio/trollius implementation | |
5 | ||
6 | ||
7 | class _Config: | |
8 | """ | |
9 | This holds all valid configuration options, accessed as | |
10 | class-level variables. For example, if you were using asyncio: | |
11 | ||
12 | .. sourcecode:: python | |
13 | ||
14 | txaio.config.loop = asyncio.get_event_loop() | |
15 | ||
16 | ``loop`` is populated automatically (while importing one of the | |
17 | framework-specific libraries) but can be changed before any call | |
18 | into this library. Currently, it's only used by :meth:`call_later` | |
19 | If using asyncio, you must set this to an event-loop (by default, | |
20 | we use asyncio.get_event_loop). If using Twisted, set this to a | |
21 | reactor instance (by default we "from twisted.internet import | |
22 | reactor" on the first call to call_later) | |
23 | """ | |
24 | #: the event-loop object to use | |
25 | loop = None | |
26 | ||
27 | ||
28 | __all__ = ( | |
29 | 'using_twisted', # True if we're using Twisted | |
30 | 'using_asyncio', # True if we're using asyncio | |
31 | 'use_twisted', # sets the library to use Twisted, or exception | |
32 | 'use_asyncio', # sets the library to use asyncio, or exception | |
33 | ||
34 | 'config', # the config instance, access via attributes | |
35 | ||
36 | 'create_future', # create a Future (can be already resolved/errored) | |
37 | 'as_future', # call a method, and always return a Future | |
38 | 'reject', # errback a Future | |
39 | 'resolve', # callback a Future | |
40 | 'add_callbacks', # add callback and/or errback | |
41 | 'gather', # return a Future waiting for several other Futures | |
42 | ||
43 | 'IFailedFuture', # describes API for arg to errback()s | |
44 | ) | |
45 | ||
46 | ||
47 | def use_twisted(): | |
48 | from txaio import tx | |
49 | import txaio | |
50 | txaio.using_twisted = True | |
51 | txaio.using_asyncio = False | |
52 | for method_name in __all__: | |
53 | if method_name in ['use_twisted', 'use_asyncio']: | |
54 | continue | |
55 | twisted_method = getattr(tx, method_name) | |
56 | setattr(txaio, method_name, twisted_method) | |
57 | ||
58 | ||
59 | def use_asyncio(): | |
60 | from txaio import aio | |
61 | import txaio | |
62 | txaio.using_twisted = False | |
63 | txaio.using_asyncio = True | |
64 | for method_name in __all__: | |
65 | if method_name in ['use_twisted', 'use_asyncio']: | |
66 | continue | |
67 | twisted_method = getattr(aio, method_name) | |
68 | setattr(txaio, method_name, twisted_method) | |
69 | ||
70 | ||
71 | try: | |
72 | from txaio.tx import * # noqa | |
73 | using_twisted = True | |
74 | except ImportError: | |
75 | try: | |
76 | from txaio.aio import * # noqa | |
77 | using_asyncio = True | |
78 | except ImportError: | |
79 | raise ImportError("Neither asyncio nor Twisted found.") |
0 | from __future__ import absolute_import, print_function | |
1 | ||
2 | import sys | |
3 | import traceback | |
4 | import functools | |
5 | ||
6 | from txaio.interfaces import IFailedFuture | |
7 | from txaio import _Config | |
8 | ||
9 | try: | |
10 | import asyncio | |
11 | from asyncio import iscoroutine | |
12 | from asyncio import Future | |
13 | ||
14 | except ImportError: | |
15 | # Trollius >= 0.3 was renamed | |
16 | # noinspection PyUnresolvedReferences | |
17 | import trollius as asyncio | |
18 | from trollius import iscoroutine | |
19 | from trollius import Future | |
20 | ||
21 | ||
22 | config = _Config() | |
23 | config.loop = asyncio.get_event_loop() | |
24 | ||
25 | using_twisted = False | |
26 | using_asyncio = True | |
27 | ||
28 | ||
29 | class FailedFuture(IFailedFuture): | |
30 | """ | |
31 | This provides an object with any features from Twisted's Failure | |
32 | that we might need in Autobahn classes that use FutureMixin. | |
33 | ||
34 | We need to encapsulate information from exceptions so that | |
35 | errbacks still have access to the traceback (in case they want to | |
36 | print it out) outside of "except" blocks. | |
37 | """ | |
38 | ||
39 | def __init__(self, type_, value, traceback): | |
40 | """ | |
41 | These are the same parameters as returned from ``sys.exc_info()`` | |
42 | ||
43 | :param type_: exception type | |
44 | :param value: the Exception instance | |
45 | :param traceback: a traceback object | |
46 | """ | |
47 | self._type = type_ | |
48 | self._value = value | |
49 | self._traceback = traceback | |
50 | ||
51 | @property | |
52 | def type(self): | |
53 | return self._type | |
54 | ||
55 | @property | |
56 | def value(self): | |
57 | return self._value | |
58 | ||
59 | @property | |
60 | def tb(self): | |
61 | return self._traceback | |
62 | ||
63 | def printTraceback(self, file=None): | |
64 | """ | |
65 | Prints the complete traceback to stderr, or to the provided file | |
66 | """ | |
67 | # print_exception handles None for file | |
68 | traceback.print_exception(self.type, self.value, self._traceback, | |
69 | file=file) | |
70 | ||
71 | def getErrorMessage(self): | |
72 | """ | |
73 | Returns the str() of the underlying exception. | |
74 | """ | |
75 | return str(self.value) | |
76 | ||
77 | def __str__(self): | |
78 | return self.getErrorMessage() | |
79 | ||
80 | ||
81 | # API methods for txaio, exported via the top-level __init__.py | |
82 | ||
83 | ||
84 | def create_future(): | |
85 | return Future() | |
86 | ||
87 | ||
88 | def create_future_success(result): | |
89 | f = Future() | |
90 | f.set_result(result) | |
91 | return f | |
92 | ||
93 | ||
94 | def create_future_error(error=None): | |
95 | if error is None: | |
96 | error = create_failure() | |
97 | elif isinstance(error, Exception): | |
98 | error = FailedFuture(type(error), error, None) | |
99 | else: | |
100 | assert isinstance(error, IFailedFuture) | |
101 | f = Future() | |
102 | f.set_exception(error.value) | |
103 | return f | |
104 | ||
105 | ||
106 | # XXX maybe rename to call()? | |
107 | def as_future(fun, *args, **kwargs): | |
108 | try: | |
109 | res = fun(*args, **kwargs) | |
110 | except Exception: | |
111 | return create_future_error(create_failure()) | |
112 | else: | |
113 | if isinstance(res, Future): | |
114 | return res | |
115 | elif iscoroutine(res): | |
116 | return asyncio.Task(res) | |
117 | else: | |
118 | return create_future_success(res) | |
119 | ||
120 | ||
121 | def call_later(delay, fun, *args, **kwargs): | |
122 | # loop.call_later doesns't support kwargs | |
123 | real_call = functools.partial(fun, *args, **kwargs) | |
124 | return config.loop.call_later(delay, real_call) | |
125 | ||
126 | ||
127 | def resolve(future, result): | |
128 | future.set_result(result) | |
129 | ||
130 | ||
131 | def reject(future, error=None): | |
132 | if error is None: | |
133 | error = create_failure() # will be error if we're not in an "except" | |
134 | elif isinstance(error, Exception): | |
135 | error = FailedFuture(type(error), error, None) | |
136 | else: | |
137 | assert isinstance(error, IFailedFuture) | |
138 | future.set_exception(error.value) | |
139 | ||
140 | ||
141 | def create_failure(exception=None): | |
142 | """ | |
143 | This returns an object implementing IFailedFuture. | |
144 | ||
145 | If exception is None (the default) we MUST be called within an | |
146 | "except" block (such that sys.exc_info() returns useful | |
147 | information). | |
148 | """ | |
149 | if exception: | |
150 | return FailedFuture(type(exception), exception, None) | |
151 | return FailedFuture(*sys.exc_info()) | |
152 | ||
153 | ||
154 | def add_callbacks(future, callback, errback): | |
155 | """ | |
156 | callback or errback may be None, but at least one must be | |
157 | non-None. | |
158 | ||
159 | XXX beware the "f._result" hack to get "chainable-callback" type | |
160 | behavior. | |
161 | """ | |
162 | def done(f): | |
163 | try: | |
164 | res = f.result() | |
165 | if callback: | |
166 | x = callback(res) | |
167 | if x is not None: | |
168 | f._result = x | |
169 | except Exception: | |
170 | if errback: | |
171 | errback(create_failure()) | |
172 | return future.add_done_callback(done) | |
173 | ||
174 | ||
175 | def gather(futures, consume_exceptions=True): | |
176 | """ | |
177 | This returns a Future that waits for all the Futures in the list | |
178 | ``futures`` | |
179 | ||
180 | :param futures: a list of Futures (or coroutines?) | |
181 | ||
182 | :param consume_exceptions: if True, any errors are eaten and | |
183 | returned in the result list. | |
184 | """ | |
185 | ||
186 | # from the asyncio docs: "If return_exceptions is True, exceptions | |
187 | # in the tasks are treated the same as successful results, and | |
188 | # gathered in the result list; otherwise, the first raised | |
189 | # exception will be immediately propagated to the returned | |
190 | # future." | |
191 | return asyncio.gather(*futures, return_exceptions=consume_exceptions) |
0 | import abc | |
1 | import six | |
2 | ||
3 | ||
4 | @six.add_metaclass(abc.ABCMeta) | |
5 | class IFailedFuture(object): | |
6 | """ | |
7 | This defines the interface for a common object encapsulating a | |
8 | failure from either an asyncio task/coroutine or a Twisted | |
9 | Deferred. | |
10 | ||
11 | An instance implementing this interface is given to any | |
12 | ``errback`` callables you provde via :meth:`txaio.add_callbacks` | |
13 | ||
14 | It is a subset of Twisted's Failure interface, because on Twisted | |
15 | backends it actually *is* a Failure. | |
16 | """ | |
17 | ||
18 | @abc.abstractproperty | |
19 | def type(self): | |
20 | """ | |
21 | The type of the exception. Same as the first item returned from | |
22 | ``sys.exc_info()`` | |
23 | """ | |
24 | ||
25 | @abc.abstractproperty | |
26 | def value(self): | |
27 | """ | |
28 | An actual Exception instance. Same as the second item returned from | |
29 | ``sys.exc_info()`` | |
30 | """ | |
31 | ||
32 | @abc.abstractproperty | |
33 | def tb(self): | |
34 | """ | |
35 | A traceback object from the exception. Same as the third item | |
36 | returned from ``sys.exc_info()`` | |
37 | """ | |
38 | ||
39 | @abc.abstractmethod | |
40 | def printTraceback(self, file=None): | |
41 | """ | |
42 | Prints the exception and its traceback to the given ``file``. If | |
43 | that is ``None`` (the default) then it is printed to | |
44 | ``sys.stderr``. | |
45 | ||
46 | XXX this is camelCase because Twisted is; can we change somehow? | |
47 | """ | |
48 | ||
49 | @abc.abstractmethod | |
50 | def getErrorMessage(self): | |
51 | """ | |
52 | Return a string describing the error. | |
53 | ||
54 | XXX this is camelCase because Twisted is; can we change somehow? | |
55 | """ | |
56 | ||
57 | # XXX anything else make sense? Do we ape the *entire* Failure API? |
0 | import txaio | |
1 | from contextlib import contextmanager | |
2 | ||
3 | ||
4 | @contextmanager | |
5 | def replace_loop(new_loop): | |
6 | """ | |
7 | This is a context-manager that sets the txaio event-loop to the | |
8 | one supplied temporarily. It's up to you to ensure you pass an | |
9 | event_loop or a reactor instance depending upon asyncio/Twisted. | |
10 | ||
11 | Use like so: | |
12 | ||
13 | .. sourcecode:: python | |
14 | ||
15 | from twisted.internet import task | |
16 | with replace_loop(task.Clock()) as fake_reactor: | |
17 | f = txaio.call_later(5, foo) | |
18 | fake_reactor.advance(10) | |
19 | # ...etc | |
20 | """ | |
21 | ||
22 | # setup | |
23 | orig = txaio.config.loop | |
24 | txaio.config.loop = new_loop | |
25 | ||
26 | yield new_loop | |
27 | ||
28 | # cleanup | |
29 | txaio.config.loop = orig |
0 | from twisted.python.failure import Failure | |
1 | from twisted.internet.defer import maybeDeferred, Deferred, DeferredList | |
2 | from twisted.internet.defer import succeed, fail | |
3 | from twisted.internet.interfaces import IReactorTime | |
4 | ||
5 | from txaio.interfaces import IFailedFuture | |
6 | from txaio import _Config | |
7 | ||
8 | using_twisted = True | |
9 | using_asyncio = False | |
10 | ||
11 | config = _Config() | |
12 | ||
13 | ||
14 | class FailedFuture(IFailedFuture): | |
15 | pass | |
16 | ||
17 | ||
18 | FailedFuture.register(Failure) | |
19 | ||
20 | ||
21 | def create_future(result=None, error=None): | |
22 | if result is not None and error is not None: | |
23 | raise ValueError("Cannot have both result and error.") | |
24 | ||
25 | f = Deferred() | |
26 | if result is not None: | |
27 | resolve(f, result) | |
28 | elif error is not None: | |
29 | reject(f, error) | |
30 | return f | |
31 | ||
32 | ||
33 | # maybe delete, just use create_future() | |
34 | def create_future_success(result): | |
35 | return succeed(result) | |
36 | ||
37 | ||
38 | # maybe delete, just use create_future() | |
39 | def create_future_error(error=None): | |
40 | return fail(create_failure(error)) | |
41 | ||
42 | ||
43 | # maybe rename to call()? | |
44 | def as_future(fun, *args, **kwargs): | |
45 | return maybeDeferred(fun, *args, **kwargs) | |
46 | ||
47 | ||
48 | def call_later(delay, fun, *args, **kwargs): | |
49 | return IReactorTime(_get_loop()).callLater(delay, fun, *args, **kwargs) | |
50 | ||
51 | ||
52 | def resolve(future, result=None): | |
53 | future.callback(result) | |
54 | ||
55 | ||
56 | def reject(future, error=None): | |
57 | if error is None: | |
58 | error = create_failure() | |
59 | elif isinstance(error, Exception): | |
60 | error = Failure(error) | |
61 | else: | |
62 | assert isinstance(error, IFailedFuture) | |
63 | future.errback(error) | |
64 | ||
65 | ||
66 | def create_failure(exception=None): | |
67 | """ | |
68 | Create a Failure instance. | |
69 | ||
70 | if ``exception`` is None (the default), we MUST be inside an | |
71 | "except" block. This encapsulates the exception into an object | |
72 | that implements IFailedFuture | |
73 | """ | |
74 | if exception: | |
75 | return Failure(exception) | |
76 | return Failure() | |
77 | ||
78 | ||
79 | def add_callbacks(future, callback, errback): | |
80 | """ | |
81 | callback or errback may be None, but at least one must be | |
82 | non-None. | |
83 | """ | |
84 | assert future is not None | |
85 | if callback is None: | |
86 | assert errback is not None | |
87 | future.addErrback(errback) | |
88 | else: | |
89 | # Twisted allows errback to be None here | |
90 | future.addCallbacks(callback, errback) | |
91 | return future | |
92 | ||
93 | ||
94 | def gather(futures, consume_exceptions=True): | |
95 | def completed(res): | |
96 | rtn = [] | |
97 | for (ok, value) in res: | |
98 | rtn.append(value) | |
99 | if not ok and not consume_exceptions: | |
100 | value.raiseException() | |
101 | return rtn | |
102 | ||
103 | # XXX if consume_exceptions is False in asyncio.gather(), it will | |
104 | # abort on the first raised exception -- should we set | |
105 | # fireOnOneErrback=True (if consume_exceptions=False?) -- but then | |
106 | # we'll have to wrap the errback() to extract the "real" failure | |
107 | # from the FirstError that gets thrown if you set that ... | |
108 | ||
109 | dl = DeferredList(list(futures), consumeErrors=consume_exceptions) | |
110 | # we unpack the (ok, value) tuples into just a list of values, so | |
111 | # that the callback() gets the same value in asyncio and Twisted. | |
112 | add_callbacks(dl, completed, None) | |
113 | return dl | |
114 | ||
115 | ||
116 | # methods internal to this implementation | |
117 | ||
118 | ||
119 | def _get_loop(): | |
120 | if config.loop is None: | |
121 | from twisted.internet import reactor | |
122 | config.loop = reactor | |
123 | return config.loop |