From: Ben Darnell Date: Sun, 12 May 2013 23:34:12 +0000 (-0400) Subject: Allow prepare to be asynchronous, and detect coroutines by their result. X-Git-Tag: v3.1.0~76^2~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ca8495d934f659dffb7350f2e7df77b59b39e0b5;p=thirdparty%2Ftornado.git Allow prepare to be asynchronous, and detect coroutines by their result. The prepare method does not use the @asynchronous decorator, only @gen.coroutine (or @return_future; it detects the Future return type). The same logic is now available for the regular http verb methods as well. Closes #605. --- diff --git a/docs/web.rst b/docs/web.rst index a400dcad3..14a1f9dea 100644 --- a/docs/web.rst +++ b/docs/web.rst @@ -18,6 +18,8 @@ Implement any of the following methods (collectively known as the HTTP verb methods) to handle the corresponding HTTP method. + These methods can be made asynchronous with one of the following + decorators: `.gen.coroutine`, `.return_future`, or `asynchronous`. .. automethod:: RequestHandler.get .. automethod:: RequestHandler.post diff --git a/tornado/test/gen_test.py b/tornado/test/gen_test.py index a1e09a133..f51077efb 100644 --- a/tornado/test/gen_test.py +++ b/tornado/test/gen_test.py @@ -11,11 +11,12 @@ import weakref from tornado.concurrent import return_future from tornado.escape import url_escape from tornado.httpclient import AsyncHTTPClient +from tornado.ioloop import IOLoop from tornado.log import app_log from tornado import stack_context from tornado.testing import AsyncHTTPTestCase, AsyncTestCase, ExpectLog, gen_test from tornado.test.util import unittest, skipOnTravis -from tornado.web import Application, RequestHandler, asynchronous +from tornado.web import Application, RequestHandler, asynchronous, HTTPError from tornado import gen @@ -735,7 +736,6 @@ class GenSequenceHandler(RequestHandler): class GenCoroutineSequenceHandler(RequestHandler): - @asynchronous @gen.coroutine def get(self): self.io_loop = self.request.connection.stream.io_loop @@ -816,6 +816,32 @@ class GenYieldExceptionHandler(RequestHandler): self.finish('ok') +class UndecoratedCoroutinesHandler(RequestHandler): + @gen.coroutine + def prepare(self): + self.chunks = [] + yield gen.Task(IOLoop.current().add_callback) + self.chunks.append('1') + + @gen.coroutine + def get(self): + self.chunks.append('2') + yield gen.Task(IOLoop.current().add_callback) + self.chunks.append('3') + yield gen.Task(IOLoop.current().add_callback) + self.write(''.join(self.chunks)) + + +class AsyncPrepareErrorHandler(RequestHandler): + @gen.coroutine + def prepare(self): + yield gen.Task(IOLoop.current().add_callback) + raise HTTPError(403) + + def get(self): + self.finish('ok') + + class GenWebTest(AsyncHTTPTestCase): def get_app(self): return Application([ @@ -827,6 +853,8 @@ class GenWebTest(AsyncHTTPTestCase): ('/exception', GenExceptionHandler), ('/coroutine_exception', GenCoroutineExceptionHandler), ('/yield_exception', GenYieldExceptionHandler), + ('/undecorated_coroutine', UndecoratedCoroutinesHandler), + ('/async_prepare_error', AsyncPrepareErrorHandler), ]) def test_sequence_handler(self): @@ -861,5 +889,13 @@ class GenWebTest(AsyncHTTPTestCase): response = self.fetch('/yield_exception') self.assertEqual(response.body, b'ok') + def test_undecorated_coroutines(self): + response = self.fetch('/undecorated_coroutine') + self.assertEqual(response.body, b'123') + + def test_async_prepare_error_handler(self): + response = self.fetch('/async_prepare_error') + self.assertEqual(response.code, 403) + if __name__ == '__main__': unittest.main() diff --git a/tornado/web.py b/tornado/web.py index dfa357c00..686d8de39 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -198,6 +198,12 @@ class RequestHandler(object): Override this method to perform common initialization regardless of the request method. + + Asynchronous support: Decorate this method with `.gen.coroutine` + or `.return_future` to make it asynchronous (the + `asynchronous` decorator cannot be used on `prepare`). + If this method returns a `.Future` execution will not proceed + until the `.Future` is done. """ pass @@ -1077,15 +1083,40 @@ class RequestHandler(object): if self.request.method not in ("GET", "HEAD", "OPTIONS") and \ self.application.settings.get("xsrf_cookies"): self.check_xsrf_cookie() - self.prepare() - if not self._finished: - getattr(self, self.request.method.lower())( - *self.path_args, **self.path_kwargs) - if self._auto_finish and not self._finished: - self.finish() + self._when_complete(self.prepare(), self._execute_method) + except Exception as e: + self._handle_request_exception(e) + + def _when_complete(self, result, callback): + try: + if result is None: + callback() + elif isinstance(result, Future): + if result.done(): + if result.result() is not None: + raise ValueError('Expected None, got %r' % result) + callback() + else: + # Delayed import of IOLoop because it's not available + # on app engine + from tornado.ioloop import IOLoop + IOLoop.current().add_future( + result, functools.partial(self._when_complete, + callback=callback)) + else: + raise ValueError("Expected Future or None, got %r" % result) except Exception as e: self._handle_request_exception(e) + def _execute_method(self): + method = getattr(self, self.request.method.lower()) + self._when_complete(method(*self.path_args, **self.path_kwargs), + self._execute_finish) + + def _execute_finish(self): + if self._auto_finish and not self._finished: + self.finish() + def _generate_headers(self): reason = self._reason lines = [utf8(self.request.version + " " + @@ -1173,6 +1204,11 @@ class RequestHandler(object): def asynchronous(method): """Wrap request handler methods with this if they are asynchronous. + This decorator is unnecessary if the method is also decorated with + ``@gen.coroutine`` (it is legal but unnecessary to use the two + decorators together, in which case ``@asynchronous`` must be + first). + This decorator should only be applied to the :ref:`HTTP verb methods `; its behavior is undefined for any other method. This decorator does not *make* a method asynchronous; it tells