summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore57
-rw-r--r--.travis.yml19
-rw-r--r--CHANGES.rst55
-rw-r--r--LICENSE22
-rw-r--r--MANIFEST.in3
-rw-r--r--Makefile25
-rw-r--r--README.rst32
-rw-r--r--debian/changelog11
-rw-r--r--debian/control34
-rw-r--r--debian/copyright30
-rwxr-xr-xdebian/rules7
-rw-r--r--debian/source/format1
-rw-r--r--debian/source/options1
-rw-r--r--debian/watch3
-rw-r--r--requirements-test.txt7
-rw-r--r--setup.cfg2
-rw-r--r--setup.py32
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/threadloop/__init__.py0
-rw-r--r--tests/threadloop/test_threadloop.py305
-rw-r--r--threadloop/__init__.py3
-rw-r--r--threadloop/exceptions.py11
-rw-r--r--threadloop/threadloop.py160
23 files changed, 820 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f66d097
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,57 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.xml
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..207fd61
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,19 @@
+language: python
+
+python:
+ - 2.7
+ - pypy
+ - 3.3
+ - 3.4
+ - 3.5
+ - pypy3
+
+install:
+ - make install
+
+script:
+ - make test
+ - make lint
+
+after_success:
+ - coveralls
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 0000000..6a6df36
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,55 @@
+Changelog
+=========
+
+1.0.2 (2016-04-01)
+------------------
+
+- Fix ``concurrent.Future`` exceptions not being propagated.
+
+
+1.0.1 (2015-12-17)
+------------------
+
+- Fixed a bug where instantiating ``ThreadLoop`` would change the ``IOLoop``
+ for the current thread.
+
+
+1.0.0 (2015-10-05)
+------------------
+
+- Commit to current API.
+- Better documentation.
+- Dropped support for Python 2.6.
+- Dropped support for Python 3.2.
+
+
+0.5.0 (2015-08-17)
+------------------
+
+- Synchronous functions (i.e., those not returning futures) can be submitted to
+ the loop.
+
+
+0.4.0 (2015-07-19)
+------------------
+
+- Added ``Thread.is_ready()``.
+- Switched blocking mechanism to use a ``threading.Event``.
+
+
+0.3.3 (2015-07-17)
+------------------
+
+- Fix race condition where submit can be called before Thread is fully started.
+
+
+0.3.2 (2015-07-16)
+------------------
+
+- Fix exception traceback not propagating in python2.
+
+
+0.3.1 (2015-07-13)
+------------------
+
+- Use a new IOLoop for each ThreadLoop.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..44cda91
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Grayson Koonce
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..2795cce
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+recursive-include threadloop *
+include README.rst
+include CHANGES.rst
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..2ffd67d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,25 @@
+project := threadloop
+
+flake8 := flake8
+pytest := py.test -s --tb short --cov $(project) tests
+
+test_args := --cov-report term-missing --cov-report xml
+
+.DEFAULT_GOAL := test
+
+.PHONY:install
+install:
+ pip install -r requirements-test.txt
+ python setup.py develop
+
+.PHONY: clean
+clean:
+ @find $(project) tests -name "*.pyc" -delete
+
+.PHONY:test
+test: clean lint
+ PYTHONDONTWRITEBYTECODE=1 $(pytest) $(test_args)
+
+.PHONY: lint
+lint:
+ $(flake8) $(project) tests
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..293c6d9
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,32 @@
+ThreadLoop
+==========
+
+|build-status| |coverage| |pypi|
+
+ Run Tornado Coroutines from Synchronous Python.
+
+.. code:: python
+
+
+ from threadloop import ThreadLoop
+ from tornado import gen
+
+ @gen.coroutine
+ def coroutine(greeting="Goodbye"):
+ yield gen.sleep(1)
+ raise gen.Return("%s World" % greeting)
+
+ with ThreadLoop() as threadloop:
+ future = threadloop.submit(coroutine, "Hello")
+
+ print future.result() # Hello World
+
+
+.. |build-status| image:: https://travis-ci.org/breerly/threadloop.svg?branch=master
+ :target: https://travis-ci.org/breerly/threadloop
+
+.. |coverage| image:: https://coveralls.io/repos/breerly/threadloop/badge.svg?branch=master&service=github
+ :target: https://coveralls.io/github/breerly/threadloop?branch=master
+
+.. |pypi| image:: https://badge.fury.io/py/threadloop.svg
+ :target: http://badge.fury.io/py/threadloop
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..269ae93
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,11 @@
+python-threadloop (1.0.2-2) unstable; urgency=medium
+
+ * Re-upload source-only.
+
+ -- Thomas Goirand <zigo@debian.org> Tue, 10 Sep 2024 15:00:21 +0200
+
+python-threadloop (1.0.2-1) unstable; urgency=medium
+
+ * Initial release. (Closes: #1081203)
+
+ -- Thomas Goirand <zigo@debian.org> Wed, 28 Aug 2024 11:24:34 +0200
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..eb69cd4
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,34 @@
+Source: python-threadloop
+Section: python
+Priority: optional
+Maintainer: Debian OpenStack <team+openstack@tracker.debian.org>
+Uploaders:
+ Thomas Goirand <zigo@debian.org>,
+Build-Depends:
+ debhelper-compat (= 11),
+ dh-python,
+ openstack-pkg-tools,
+ pybuild-plugin-pyproject,
+ python3-all,
+ python3-setuptools,
+Build-Depends-Indep:
+ python3-tornado,
+Standards-Version: 4.6.1
+Vcs-Browser: https://salsa.debian.org/openstack-team/libs/python-threadloop
+Vcs-Git: https://salsa.debian.org/openstack-team/libs/python-threadloop.git
+Homepage: https://github.com/TomerFi/aioswitcher
+
+Package: python3-threadloop
+Architecture: all
+Depends:
+ python3-tornado,
+ ${misc:Depends},
+ ${python3:Depends},
+Description: Tornado IOLoop Backed Concurrent Futures
+ This package provides a way to run Tornado Coroutines from Synchronous Python.
+ .
+ Tornado is a Python web framework and asynchronous networking library,
+ originally developed at FriendFeed. By using non-blocking network I/O,
+ Tornado can scale to tens of thousands of open connections, making it ideal
+ for long polling, WebSockets, and other applications that require a
+ long-lived connection to each user.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..8c02e1c
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,30 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: threadloop
+Source: https://github.com/TomerFi/aioswitcher
+
+Files: *
+Copyright: (c) 2015-2016, Grayson Koonce <breerly@gmail.com>
+License: Expat
+
+Files: debian/*
+Copyright: (c) 2024, Thomas Goirand <zigo@debian.org>
+License: Expat
+
+License: Expat
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..c00f2a1
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,7 @@
+#!/usr/bin/make -f
+
+UPSTREAM_GIT := https://github.com/TomerFi/aioswitcher.git
+include /usr/share/openstack-pkg-tools/pkgos.make
+
+%:
+ dh $@ --buildsystem=pybuild --with python3
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/debian/source/options b/debian/source/options
new file mode 100644
index 0000000..cb61fa5
--- /dev/null
+++ b/debian/source/options
@@ -0,0 +1 @@
+extend-diff-ignore = "^[^/]*[.]egg-info/"
diff --git a/debian/watch b/debian/watch
new file mode 100644
index 0000000..fb203bb
--- /dev/null
+++ b/debian/watch
@@ -0,0 +1,3 @@
+version=4
+opts="mode=git,uversionmangle=s/\.0rc/~rc/;s/\.0b1/~b1/;s/\.0b2/~b2/;s/\.0b3/~b3/" https://github.com/TomerFi/aioswitcher refs/tags/(\d[brc\d\.]+)
+
diff --git a/requirements-test.txt b/requirements-test.txt
new file mode 100644
index 0000000..f27ee02
--- /dev/null
+++ b/requirements-test.txt
@@ -0,0 +1,7 @@
+flake8
+ipdb
+pytest
+pytest-cov
+twine
+zest.releaser
+coveralls
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..81049c3
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[zest.releaser]
+create-wheel = yes
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..7b48e25
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,32 @@
+from setuptools import setup, find_packages
+
+install_requires = ['tornado']
+
+# if python < 3, we need futures backport
+try:
+ import concurrent.futures # noqa
+except ImportError:
+ install_requires.append('futures')
+
+setup(
+ name='threadloop',
+ version='1.0.2',
+ author='Grayson Koonce',
+ author_email='breerly@gmail.com',
+ description='Tornado IOLoop Backed Concurrent Futures',
+ license='MIT',
+ url='https://github.com/breerly/threadloop',
+ packages=find_packages(exclude=['tests']),
+ install_requires=install_requires,
+ classifiers=[
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Programming Language :: Python :: Implementation :: PyPy',
+ ]
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/threadloop/__init__.py b/tests/threadloop/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/threadloop/__init__.py
diff --git a/tests/threadloop/test_threadloop.py b/tests/threadloop/test_threadloop.py
new file mode 100644
index 0000000..39e38ac
--- /dev/null
+++ b/tests/threadloop/test_threadloop.py
@@ -0,0 +1,305 @@
+from __future__ import absolute_import
+
+import time
+from types import TracebackType
+from decimal import Decimal
+
+import pytest
+from tornado import gen, ioloop
+from concurrent.futures import Future, as_completed, TimeoutError
+
+from threadloop import ThreadLoop
+from threadloop.exceptions import ThreadNotStartedError
+
+
+@pytest.fixture(autouse=True)
+def clear_io_loop():
+ # clear the current IOLoop before all tests
+ ioloop.IOLoop.clear_current()
+
+
+@pytest.yield_fixture
+def threadloop():
+ with ThreadLoop() as threadloop:
+ yield threadloop
+
+
+class TestException(Exception):
+ pass
+
+
+def test_coroutine_returns_future(threadloop):
+
+ @gen.coroutine
+ def coroutine():
+ raise gen.Return("Hello World")
+
+ future = threadloop.submit(coroutine)
+
+ assert isinstance(future, Future), "expected a concurrent.futures.Future"
+
+ assert future.result() == "Hello World"
+
+
+def test_propogates_arguments(threadloop):
+
+ @gen.coroutine
+ def coroutine(message, adjective="Shady"):
+ raise gen.Return("Hello %s %s" % (adjective, message))
+
+ future = threadloop.submit(coroutine, "World")
+ assert future.result() == "Hello Shady World"
+
+ future = threadloop.submit(coroutine, "World", adjective="Cloudy")
+ assert future.result() == "Hello Cloudy World"
+
+
+def test_coroutine_exception_propagates(threadloop):
+
+ @gen.coroutine
+ def coroutine():
+ raise TestException()
+
+ with pytest.raises(TestException):
+ future = threadloop.submit(coroutine)
+ future.result()
+
+
+def test_coroutine_exception_contains_exc_info(threadloop):
+
+ @gen.coroutine
+ def coroutine():
+ raise TestException('something went wrong')
+
+ with pytest.raises(Exception) as exc_info:
+ threadloop.submit(coroutine).result()
+
+ assert 'something went wrong' in str(exc_info.value)
+ assert isinstance(exc_info.value, TestException)
+ assert isinstance(exc_info.tb, TracebackType)
+ assert (
+ "raise TestException('something went wrong')"
+ in str(exc_info.traceback[-1])
+ )
+
+
+def test_propagate_concurrent_future_exception(threadloop):
+ def func():
+ future = Future()
+ future.set_exception(AttributeError())
+ return future
+
+ with pytest.raises(AttributeError):
+ threadloop.submit(func).result()
+
+
+def test_plain_function(threadloop):
+
+ def not_a_coroutine():
+ return "Hello World"
+
+ future = threadloop.submit(not_a_coroutine)
+
+ assert isinstance(future, Future), "expected a concurrent.futures.Future"
+
+ assert future.result() == "Hello World"
+
+
+def test_plain_function_exception_propagates(threadloop):
+
+ def not_a_coroutine():
+ raise TestException()
+
+ future = threadloop.submit(not_a_coroutine)
+
+ with pytest.raises(TestException):
+ future = threadloop.submit(not_a_coroutine)
+ future.result()
+
+
+def test_plain_function_exception_contains_exc_info(threadloop):
+
+ def not_a_coroutine():
+ raise TestException('something went wrong')
+
+ with pytest.raises(Exception) as exc_info:
+ threadloop.submit(not_a_coroutine).result()
+
+ assert 'something went wrong' in str(exc_info.value)
+ assert isinstance(exc_info.value, TestException)
+ assert isinstance(exc_info.tb, TracebackType)
+ assert (
+ "raise TestException('something went wrong')"
+ in str(exc_info.traceback[-1])
+ )
+
+
+def test_use_existing_ioloop():
+ io_loop = ioloop.IOLoop.current()
+ threadloop = ThreadLoop(io_loop)
+
+ assert threadloop._io_loop is io_loop
+
+ @gen.coroutine
+ def coroutine():
+ raise gen.Return("Hello World")
+
+ with threadloop:
+ future = threadloop.submit(coroutine)
+ assert future.result() == "Hello World"
+
+
+def test_start_must_be_called_before_submit():
+ threadloop = ThreadLoop()
+
+ @gen.coroutine
+ def coroutine():
+ raise gen.Return("Hello World")
+
+ with pytest.raises(ThreadNotStartedError):
+ threadloop.submit(coroutine)
+
+
+def test_submits_coroutines_concurrently(threadloop):
+
+ @gen.coroutine
+ def coroutine1():
+ yield gen.sleep(.1)
+ raise gen.Return('coroutine1')
+
+ @gen.coroutine
+ def coroutine2():
+ yield gen.sleep(.1)
+ raise gen.Return('coroutine2')
+
+ @gen.coroutine
+ def coroutine3():
+ yield gen.sleep(.1)
+ raise gen.Return('coroutine3')
+
+ start = time.time()
+
+ future1 = threadloop.submit(coroutine1)
+ future2 = threadloop.submit(coroutine2)
+ future3 = threadloop.submit(coroutine3)
+
+ result1 = future1.result()
+ result2 = future2.result()
+ result3 = future3.result()
+
+ end = time.time() - start
+
+ # round to float with precision of 1, eg 0.3
+ took = float(round(Decimal(str(end)), 1))
+
+ # should only take ~100 ms to finish both
+ # instead of ~300ms if they were executed serially
+ assert took == .1
+
+ assert result1 == 'coroutine1'
+ assert result2 == 'coroutine2'
+ assert result3 == 'coroutine3'
+
+
+def test_as_completed(threadloop):
+
+ @gen.coroutine
+ def coroutine1():
+ yield gen.sleep(.02)
+ raise gen.Return('coroutine1')
+
+ @gen.coroutine
+ def coroutine2():
+ yield gen.sleep(.03)
+ raise gen.Return('coroutine2')
+
+ @gen.coroutine
+ def coroutine3():
+ yield gen.sleep(.01)
+ raise gen.Return('coroutine3')
+
+ @gen.coroutine
+ def coroutine4():
+ yield gen.sleep(.04)
+ raise gen.Return('coroutine4')
+
+ futures = []
+ futures.append(threadloop.submit(coroutine1))
+ futures.append(threadloop.submit(coroutine2))
+ futures.append(threadloop.submit(coroutine3))
+ futures.append(threadloop.submit(coroutine4))
+
+ i = 0
+ for future in as_completed(futures):
+ i = i + 1
+
+ # make sure futures finish in the expected order
+ if i == 1:
+ assert future.result() == "coroutine3"
+ elif i == 2:
+ assert future.result() == "coroutine1"
+ elif i == 3:
+ assert future.result() == "coroutine2"
+ elif i == 4:
+ assert future.result() == "coroutine4"
+
+ assert i == 4, "expected 4 completed futures"
+
+
+def test_timeout(threadloop):
+
+ @gen.coroutine
+ def too_long():
+ yield gen.sleep(5) # 5 sec task
+ raise gen.Return('that was too long')
+
+ start = time.time()
+ future = threadloop.submit(too_long)
+
+ with pytest.raises(TimeoutError):
+ future.result(timeout=.001)
+
+ end = time.time() - start
+ took = float(round(Decimal(str(end)), 1))
+
+ assert took <= .002
+
+
+def test_block_until_thread_is_ready():
+
+ threadloop = ThreadLoop()
+
+ assert not threadloop.is_ready()
+
+ threadloop.start()
+
+ assert threadloop.is_ready()
+
+
+def test_is_not_ready_when_ready_hasnt_been_sent():
+
+ threadloop = ThreadLoop()
+ threadloop._thread = True # fake the Thread being set
+
+ assert not threadloop.is_ready()
+
+
+def test_main_io_loop_is_not_changed():
+ threadloop = ThreadLoop()
+ threadloop.start()
+
+ # The ThreadLoop's IOLoop should not be the 'current' IOLoop in the main
+ # thread.
+ tl_loop = threadloop.submit(ioloop.IOLoop.current).result()
+ assert ioloop.IOLoop.current() is not tl_loop
+
+
+def test_ioloop_is_not_already_running():
+ threadloop = ThreadLoop()
+ threadloop.start()
+
+ @gen.coroutine
+ def f():
+ yield threadloop.submit(gen.sleep, 0.1)
+
+ ioloop.IOLoop.current().run_sync(f)
diff --git a/threadloop/__init__.py b/threadloop/__init__.py
new file mode 100644
index 0000000..a862cf0
--- /dev/null
+++ b/threadloop/__init__.py
@@ -0,0 +1,3 @@
+from __future__ import absolute_import
+
+from threadloop.threadloop import ThreadLoop # noqa
diff --git a/threadloop/exceptions.py b/threadloop/exceptions.py
new file mode 100644
index 0000000..29ccccb
--- /dev/null
+++ b/threadloop/exceptions.py
@@ -0,0 +1,11 @@
+from __future__ import absolute_import
+
+
+class ThreadLoopException(Exception):
+ """Top-level library exception."""
+ pass
+
+
+class ThreadNotStartedError(ThreadLoopException):
+ """Raised when calling submit before ThreadLoop.start() was called."""
+ pass
diff --git a/threadloop/threadloop.py b/threadloop/threadloop.py
new file mode 100644
index 0000000..8b584b6
--- /dev/null
+++ b/threadloop/threadloop.py
@@ -0,0 +1,160 @@
+from __future__ import absolute_import
+
+import sys
+from concurrent.futures import Future
+from threading import Event, Thread
+
+from tornado import ioloop
+from tornado import gen
+
+from .exceptions import ThreadNotStartedError
+
+# Python3's concurrent.futures.Future doesn't allow
+# setting exc_info... but exc_info works w/o setting explicitly
+_FUTURE_HAS_EXC_INFO = hasattr(Future, "set_exception_info")
+
+
+class ThreadLoop(object):
+ """Tornado IOLoop Backed Concurrent Futures.
+
+ Run Tornado Coroutines from Synchronous Python.
+
+ This is made possible by starting the IOLoop in another thread. When
+ coroutines are submitted, they are ran against that loop, and their
+ responses are bound to Concurrent Futures.
+
+ .. code-block:: python
+
+ from threadloop import ThreadLoop
+ from tornado import gen
+
+ @gen.coroutine
+ def coroutine(greeting="Goodbye"):
+ yield gen.sleep(1)
+ raise gen.Return("%s World" % greeting)
+
+ with ThreadLoop() as threadloop:
+
+ future = threadloop.submit(coroutine, "Hello")
+
+ print future.result() # Hello World
+
+ """
+ def __init__(self, io_loop=None):
+
+ self._thread = None
+ self._ready = Event()
+ self._io_loop = io_loop
+
+ def __enter__(self):
+ self.start()
+ return self
+
+ def __exit__(self, *args):
+ self.stop()
+
+ def start(self):
+ """Start IOLoop in daemonized thread."""
+ assert self._thread is None, 'thread already started'
+
+ # configure thread
+ self._thread = Thread(target=self._start_io_loop)
+ self._thread.daemon = True
+
+ # begin thread and block until ready
+ self._thread.start()
+ self._ready.wait()
+
+ def _start_io_loop(self):
+ """Start IOLoop then set ready threading.Event."""
+
+ def mark_as_ready():
+ self._ready.set()
+
+ if not self._io_loop:
+ self._io_loop = ioloop.IOLoop()
+
+ self._io_loop.add_callback(mark_as_ready)
+ self._io_loop.start()
+
+ def is_ready(self):
+ """Is thread & ioloop ready.
+
+ :returns bool:
+ """
+ if not self._thread:
+ return False
+
+ if not self._ready.is_set():
+ return False
+
+ return True
+
+ def stop(self):
+ """Stop IOLoop & close daemonized thread."""
+ self._io_loop.stop()
+ self._thread.join()
+
+ def submit(self, fn, *args, **kwargs):
+ """Submit Tornado Coroutine to IOLoop in daemonized thread.
+
+ :param fn: Tornado Coroutine to execute
+ :param args: Args to pass to coroutine
+ :param kwargs: Kwargs to pass to coroutine
+ :returns concurrent.futures.Future: future result of coroutine
+ """
+ if not self.is_ready():
+ raise ThreadNotStartedError(
+ "The thread has not been started yet, "
+ "make sure you call start() first"
+ )
+
+ future = Future()
+
+ def execute():
+ """Executes fn on the IOLoop."""
+ try:
+ result = gen.maybe_future(fn(*args, **kwargs))
+ except Exception:
+ # The function we ran didn't return a future and instead raised
+ # an exception. Let's pretend that it returned this dummy
+ # future with our stack trace.
+ f = gen.Future()
+ f.set_exc_info(sys.exc_info())
+ on_done(f)
+ else:
+ result.add_done_callback(on_done)
+
+ def on_done(f):
+ """Sets tornado.Future results to the concurrent.Future."""
+
+ if not f.exception():
+ future.set_result(f.result())
+ return
+
+ # if f is a tornado future, then it has exc_info()
+ if hasattr(f, 'exc_info'):
+ exception, traceback = f.exc_info()[1:]
+
+ # else it's a concurrent.future
+ else:
+ # python2's concurrent.future has exception_info()
+ if hasattr(f, 'exception_info'):
+ exception, traceback = f.exception_info()
+
+ # python3's concurrent.future just has exception()
+ else:
+ exception = f.exception()
+ traceback = None
+
+ # python2 needs exc_info set explicitly
+ if _FUTURE_HAS_EXC_INFO:
+ future.set_exception_info(exception, traceback)
+ return
+
+ # python3 just needs the exception, exc_info works fine
+ future.set_exception(exception)
+
+ self._io_loop.add_callback(execute)
+
+ return future