]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
gen: Use the asyncio task runner for native coroutines 2213/head
authorBen Darnell <ben@bendarnell.com>
Sat, 9 Dec 2017 20:15:39 +0000 (15:15 -0500)
committerBen Darnell <ben@bendarnell.com>
Sat, 9 Dec 2017 21:14:54 +0000 (16:14 -0500)
tornado/gen.py
tornado/ioloop.py
tornado/test/ioloop_test.py
tornado/testing.py

index 533ccb749d99df9ecf76c881981ec3ddd801f45b..452fb1bd4cb621268242cb3cebb5f79e2be239f5 100644 (file)
@@ -1193,20 +1193,10 @@ def _argument_adapter(callback):
     return wrapper
 
 
-# Convert Awaitables into Futures. It is unfortunately possible
-# to have infinite recursion here if those Awaitables assume that
-# we're using a different coroutine runner and yield objects
-# we don't understand. If that happens, the solution is to
-# register that runner's yieldable objects with convert_yielded.
-if sys.version_info >= (3, 3):
-    exec(textwrap.dedent("""
-    @coroutine
-    def _wrap_awaitable(x):
-        if hasattr(x, '__await__'):
-            x = x.__await__()
-        return (yield from x)
-    """))
-else:
+# Convert Awaitables into Futures.
+try:
+    import asyncio
+except ImportError:
     # Py2-compatible version for use with Cython.
     # Copied from PEP 380.
     @coroutine
@@ -1253,6 +1243,8 @@ else:
                         _r = _value_from_stopiteration(_e)
                         break
         raise Return(_r)
+else:
+    _wrap_awaitable = asyncio.ensure_future
 
 
 def convert_yielded(yielded):
index c950a89d3143cf3433b9f00e7896aa6a1bd4c0dd..76b5956b4c075167d4f89a2cceb3cbb1b2a60b40 100644 (file)
@@ -504,11 +504,18 @@ class IOLoop(Configurable):
             self.add_future(future_cell[0], lambda future: self.stop())
         self.add_callback(run)
         if timeout is not None:
-            timeout_handle = self.add_timeout(self.time() + timeout, self.stop)
+            def timeout_callback():
+                # If we can cancel the future, do so and wait on it. If not,
+                # Just stop the loop and return with the task still pending.
+                # (If we neither cancel nor wait for the task, a warning
+                # will be logged).
+                if not future_cell[0].cancel():
+                    self.stop()
+            timeout_handle = self.add_timeout(self.time() + timeout, timeout_callback)
         self.start()
         if timeout is not None:
             self.remove_timeout(timeout_handle)
-        if not future_cell[0].done():
+        if future_cell[0].cancelled() or not future_cell[0].done():
             raise TimeoutError('Operation timed out after %s seconds' % timeout)
         return future_cell[0].result()
 
index adabeafe5fc41b07627031021f6d4cee23cb9612..616099452aaa742a47cc814479daa22ab4ab53ea 100644 (file)
@@ -405,7 +405,9 @@ class TestIOLoop(AsyncTestCase):
         """The IOLoop examines exceptions from awaitables and logs them."""
         namespace = exec_test(globals(), locals(), """
         async def callback():
-            self.io_loop.add_callback(self.stop)
+            # Stop the IOLoop two iterations after raising an exception
+            # to give the exception time to be logged.
+            self.io_loop.add_callback(self.io_loop.add_callback, self.stop)
             1 / 0
         """)
         with NullContext():
index 8463b14ecdb0b32bce3e0902a8bb3e7f6efab701..975df86cc9d6dea66b7fa743c1c10123df6249d3 100644 (file)
@@ -527,12 +527,17 @@ def gen_test(func=None, timeout=None):
                     timeout=timeout)
             except TimeoutError as e:
                 # run_sync raises an error with an unhelpful traceback.
-                # Throw it back into the generator or coroutine so the stack
-                # trace is replaced by the point where the test is stopped.
-                self._test_generator.throw(e)
-                # In case the test contains an overly broad except clause,
-                # we may get back here.  In this case re-raise the original
-                # exception, which is better than nothing.
+                # If the underlying generator is still running, we can throw the
+                # exception back into it so the stack trace is replaced by the
+                # point where the test is stopped. The only reason the generator
+                # would not be running would be if it were cancelled, which means
+                # a native coroutine, so we can rely on the cr_running attribute.
+                if getattr(self._test_generator, 'cr_running', True):
+                    self._test_generator.throw(e)
+                    # In case the test contains an overly broad except
+                    # clause, we may get back here.
+                # Coroutine was stopped or didn't raise a useful stack trace,
+                # so re-raise the original exception which is better than nothing.
                 raise
         return post_coroutine