Codebase list python-txaio / 79395a3
Merge pull request #1 from meejah/different-api-ideas Different api ideas Tobias Oberstein 9 years ago
24 changed file(s) with 2027 addition(s) and 6 deletion(s). Raw diff Collapse all Expand all
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
-6
README.md less more
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 _build/
1
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