Requires changes to be included in 3.5b3.
import functools
import platform
+import textwrap
import traceback
import sys
self._callbacks = []
+ # Implement the Python 3.5 Awaitable protocol if possible
+ # (we can't use return and yield together until py33).
+ if sys.version_info >= (3, 3):
+ exec(textwrap.dedent("""
+ def __await__(self):
+ return (yield self)
+ """))
+
def cancel(self):
"""Cancel the operation, if possible.
import functools
import itertools
import sys
+import textwrap
import types
-import weakref
from tornado.concurrent import Future, TracebackFuture, is_future, chain_future
from tornado.ioloop import IOLoop
singledispatch = None
+try:
+ from collections.abc import Generator as GeneratorType # py35+
+except ImportError:
+ from types import GeneratorType
+
+try:
+ from inspect import isawaitable # py35+
+except ImportError:
+ def isawaitable(x): return False
+
+
class KeyReuseError(Exception):
pass
argument, so we cannot simply implement ``@engine`` in terms of
``@coroutine``.
"""
+ # On Python 3.5, set the coroutine flag on our generator, to allow it
+ # to be used with 'await'.
+ if hasattr(types, 'coroutine'):
+ func = types.coroutine(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
future = TracebackFuture()
future.set_exc_info(sys.exc_info())
return future
else:
- if isinstance(result, types.GeneratorType):
+ if isinstance(result, GeneratorType):
# Inline the first iteration of Runner.run. This lets us
# avoid the cost of creating a Runner when the coroutine
# never actually yields, which in turn allows us to
callback(None)
return wrapper
+if sys.version_info >= (3, 3):
+ exec(textwrap.dedent("""
+ @coroutine
+ def _wrap_awaitable(x):
+ return (yield from x)
+ """))
+else:
+ def _wrap_awaitable(x):
+ raise NotImplementedError()
+
def convert_yielded(yielded):
"""Convert a yielded object into a `.Future`.
return multi_future(yielded)
elif is_future(yielded):
return yielded
+ elif isawaitable(yielded):
+ return _wrap_awaitable(yielded)
else:
raise BadYieldError("yielded unknown object %r" % (yielded,))
except ImportError:
futures = None
-skipBefore33 = unittest.skipIf(sys.version_info < (3, 3), 'PEP 380 not available')
+skipBefore33 = unittest.skipIf(sys.version_info < (3, 3), 'PEP 380 (yield from) not available')
+skipBefore35 = unittest.skipIf(sys.version_info < (3, 5), 'PEP 492 (async/await) not available')
skipNotCPython = unittest.skipIf(platform.python_implementation() != 'CPython',
'Not CPython implementation')
self.assertEqual(result, 42)
self.finished = True
+ @skipBefore35
+ @gen_test
+ def test_async_await(self):
+ # This test verifies that an async function can await a
+ # yield-based gen.coroutine, and that a gen.coroutine
+ # (the test method itself) can yield an async function.
+ global_namespace = dict(globals(), **locals())
+ local_namespace = {}
+ exec(textwrap.dedent("""
+ async def f():
+ await gen.Task(self.io_loop.add_callback)
+ return 42
+ """), global_namespace, local_namespace)
+ result = yield local_namespace['f']()
+ self.assertEqual(result, 42)
+ self.finished = True
+
@gen_test
def test_sync_return_no_value(self):
@gen.coroutine
self.finish('ok')
+class NativeCoroutineHandler(RequestHandler):
+ if sys.version_info > (3, 5):
+ exec(textwrap.dedent("""
+ async def get(self):
+ await gen.Task(IOLoop.current().add_callback)
+ self.write("ok")
+ """))
+
+
class GenWebTest(AsyncHTTPTestCase):
def get_app(self):
return Application([
('/yield_exception', GenYieldExceptionHandler),
('/undecorated_coroutine', UndecoratedCoroutinesHandler),
('/async_prepare_error', AsyncPrepareErrorHandler),
+ ('/native_coroutine', NativeCoroutineHandler),
])
def test_sequence_handler(self):
response = self.fetch('/async_prepare_error')
self.assertEqual(response.code, 403)
+ @skipBefore35
+ def test_native_coroutine_handler(self):
+ response = self.fetch('/native_coroutine')
+ self.assertEqual(response.code, 200)
+ self.assertEqual(response.body, b'ok')
+
class WithTimeoutTest(AsyncTestCase):
@gen_test
except ImportError:
from io import StringIO # py3
+try:
+ from collections.abc import Generator as GeneratorType # py35+
+except ImportError:
+ from types import GeneratorType
+
# Tornado's own test suite requires the updated unittest module
# (either py27+ or unittest2) so tornado.test.util enforces
# this requirement, but for other users of tornado.testing we want
def __call__(self, *args, **kwargs):
result = self.orig_method(*args, **kwargs)
- if isinstance(result, types.GeneratorType):
+ if isinstance(result, GeneratorType):
raise TypeError("Generator test methods should be decorated with "
"tornado.testing.gen_test")
elif result is not None:
@functools.wraps(f)
def pre_coroutine(self, *args, **kwargs):
result = f(self, *args, **kwargs)
- if isinstance(result, types.GeneratorType):
+ if isinstance(result, GeneratorType):
self._test_generator = result
else:
self._test_generator = None
self.check_xsrf_cookie()
result = self.prepare()
- if is_future(result):
- result = yield result
if result is not None:
- raise TypeError("Expected None, got %r" % result)
+ result = yield result
if self._prepared_future is not None:
# Tell the Application we've finished with prepare()
# and are ready for the body to arrive.
method = getattr(self, self.request.method.lower())
result = method(*self.path_args, **self.path_kwargs)
- if is_future(result):
- result = yield result
if result is not None:
- raise TypeError("Expected None, got %r" % result)
+ result = yield result
if self._auto_finish and not self._finished:
self.finish()
except Exception as e: