]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Allow prepare to be asynchronous, and detect coroutines by their result.
authorBen Darnell <ben@bendarnell.com>
Sun, 12 May 2013 23:34:12 +0000 (19:34 -0400)
committerBen Darnell <ben@bendarnell.com>
Sun, 12 May 2013 23:46:41 +0000 (19:46 -0400)
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.

docs/web.rst
tornado/test/gen_test.py
tornado/web.py

index a400dcad312f9ff1c840c837b0714c1ad0985ffd..14a1f9dea48aa15766d140c0b65eaa0d1f815d70 100644 (file)
@@ -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
index a1e09a1330b865c62ade9f324a9ff125c9c66a2d..f51077efb85d1c0ebe082f8d747c05a81760fd56 100644 (file)
@@ -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()
index dfa357c002cae33e2c0677067d825d6710ffae9d..686d8de39c9ca5c2571ce7cd7659642f38cd8598 100644 (file)
@@ -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 <verbs>`; its behavior is undefined for any other method.
     This decorator does not *make* a method asynchronous; it tells