diff --git a/.travis.yml b/.travis.yml index a47dceb9..e94f5cd3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,12 +16,12 @@ env: install: # always install unittest2 on py26 even if $DEPS is unset - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then travis_retry pip install unittest2; fi - - if [[ $TRAVIS_PYTHON_VERSION == 2* && $DEPS == true ]]; then travis_retry pip install futures mock Monotime==1.0; fi - - if [[ $TRAVIS_PYTHON_VERSION == 'pypy' && $DEPS == true ]]; then travis_retry pip install futures mock; fi + - if [[ $TRAVIS_PYTHON_VERSION == 2* && $DEPS == true ]]; then travis_retry pip install futures mock Monotime==1.0 singledispatch; fi + - if [[ $TRAVIS_PYTHON_VERSION == 'pypy' && $DEPS == true ]]; then travis_retry pip install futures mock singledispatch; fi # TODO(bdarnell): pycares tests are currently disabled on travis due to ipv6 issues. #- if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* && $DEPS == true ]]; then travis_retry pip install pycares; fi - if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* && $DEPS == true ]]; then travis_retry pip install pycurl; fi - - if [[ $TRAVIS_PYTHON_VERSION == '3.2' && $DEPS == true ]]; then travis_retry pip install mock; fi + - if [[ $TRAVIS_PYTHON_VERSION == '3.2' && $DEPS == true ]]; then travis_retry pip install mock singledispatch; fi # Twisted runs on 2.x and 3.3+, but is flaky on pypy. - if [[ $TRAVIS_PYTHON_VERSION != '3.2' && $TRAVIS_PYTHON_VERSION != 'pypy'* && $DEPS == true ]]; then travis_retry travis_retry pip install Twisted; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.4' && $DEPS == true ]]; then travis_retry travis_retry pip install sphinx==1.2.2 sphinx_rtd_theme; fi diff --git a/tornado/gen.py b/tornado/gen.py index 7333bd6d..f41cbafd 100644 --- a/tornado/gen.py +++ b/tornado/gen.py @@ -60,6 +60,14 @@ from tornado.ioloop import IOLoop from tornado.log import app_log from tornado import stack_context +try: + from functools import singledispatch # py34+ +except ImportError as e: + try: + from singledispatch import singledispatch # backport + except ImportError: + singledispatch = None + class KeyReuseError(Exception): pass @@ -900,3 +908,6 @@ def convert_yielded(yielded): return yielded else: raise BadYieldError("yielded unknown object %r" % (yielded,)) + +if singledispatch is not None: + convert_yielded = singledispatch(convert_yielded) diff --git a/tornado/platform/asyncio.py b/tornado/platform/asyncio.py index dd6722a4..5842d108 100644 --- a/tornado/platform/asyncio.py +++ b/tornado/platform/asyncio.py @@ -12,6 +12,8 @@ unfinished callbacks on the event loop that fail when it resumes) from __future__ import absolute_import, division, print_function, with_statement import functools +import tornado.concurrent +from tornado.gen import convert_yielded from tornado.ioloop import IOLoop from tornado import stack_context @@ -138,3 +140,11 @@ class AsyncIOLoop(BaseAsyncIOLoop): def initialize(self): super(AsyncIOLoop, self).initialize(asyncio.new_event_loop(), close_loop=True) + + +if hasattr(convert_yielded, 'register'): + @convert_yielded.register(asyncio.Future) + def _(af): + tf = tornado.concurrent.Future() + tornado.concurrent.chain_future(af, tf) + return tf diff --git a/tornado/test/asyncio_test.py b/tornado/test/asyncio_test.py new file mode 100644 index 00000000..cb990748 --- /dev/null +++ b/tornado/test/asyncio_test.py @@ -0,0 +1,68 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function, with_statement + +import sys +import textwrap + +from tornado import gen +from tornado.testing import AsyncTestCase, gen_test +from tornado.test.util import unittest + +try: + from tornado.platform.asyncio import asyncio, AsyncIOLoop +except ImportError: + asyncio = None + +skipIfNoSingleDispatch = unittest.skipIf( + gen.singledispatch is None, "singledispatch module not present") + +@unittest.skipIf(asyncio is None, "asyncio module not present") +class AsyncIOLoopTest(AsyncTestCase): + def get_new_ioloop(self): + io_loop = AsyncIOLoop() + asyncio.set_event_loop(io_loop.asyncio_loop) + return io_loop + + def test_asyncio_callback(self): + # Basic test that the asyncio loop is set up correctly. + asyncio.get_event_loop().call_soon(self.stop) + self.wait() + + @skipIfNoSingleDispatch + @gen_test + def test_asyncio_future(self): + # Test that we can yield an asyncio future from a tornado coroutine. + # Without 'yield from', we must wrap coroutines in asyncio.async. + x = yield asyncio.async( + asyncio.get_event_loop().run_in_executor(None, lambda: 42)) + self.assertEqual(x, 42) + + @unittest.skipIf(sys.version_info < (3, 3), + 'PEP 380 not available') + @skipIfNoSingleDispatch + @gen_test + def test_asyncio_yield_from(self): + # Test that we can use asyncio coroutines with 'yield from' + # instead of asyncio.async(). This requires python 3.3 syntax. + global_namespace = dict(globals(), **locals()) + local_namespace = {} + exec(textwrap.dedent(""" + @gen.coroutine + def f(): + event_loop = asyncio.get_event_loop() + x = yield from event_loop.run_in_executor(None, lambda: 42) + return x + """), global_namespace, local_namespace) + result = yield local_namespace['f']() + self.assertEqual(result, 42) diff --git a/tornado/test/runtests.py b/tornado/test/runtests.py index ba08c1c7..8ab5f151 100644 --- a/tornado/test/runtests.py +++ b/tornado/test/runtests.py @@ -22,6 +22,7 @@ TEST_MODULES = [ 'tornado.httputil.doctests', 'tornado.iostream.doctests', 'tornado.util.doctests', + 'tornado.test.asyncio_test', 'tornado.test.auth_test', 'tornado.test.concurrent_test', 'tornado.test.curl_httpclient_test', diff --git a/tox.ini b/tox.ini index 51233eb2..7aec7faf 100644 --- a/tox.ini +++ b/tox.ini @@ -30,8 +30,8 @@ envlist = {py2,py3}-select, {py2,py26,py3}-full-twisted, py2-twistedlayered, - {py3,py33}-asyncio, - {py26,py2}-trollius, + {py3,py33}-full-asyncio, + {py26,py2}-full-trollius, # Alternate Resolvers. {py2,py3}-full-{threadedresolver}, @@ -81,6 +81,8 @@ deps = {py2,py26,py27,pypy}-full: futures # mock became standard in py33 {py2,py26,py27,pypy,py3,py32,pypy3}-full: mock + # singledispatch became standard in py34 + {py2,py26,py27,pypy,py3,py32,py33}-full: singledispatch py33-asyncio: asyncio trollius: trollius py2-monotonic: Monotime