]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Introduce TracebackFuture and use it where applicable.
authorBen Darnell <ben@bendarnell.com>
Mon, 4 Mar 2013 01:47:15 +0000 (20:47 -0500)
committerBen Darnell <ben@bendarnell.com>
Mon, 4 Mar 2013 01:47:15 +0000 (20:47 -0500)
This allows for better tracebacks when using Futures on Python 2.

tornado/concurrent.py
tornado/gen.py
tornado/ioloop.py
tornado/test/concurrent_test.py

index dd0d015f967dae139388535452161322a825a650..1779fed39312389d3d31eeb6ad3b95484db8907e 100644 (file)
@@ -92,13 +92,38 @@ else:
     Future = futures.Future
 
 
+class TracebackFuture(Future):
+    """Subclass of `Future` which can store a traceback with exceptions.
+
+    The traceback is automatically available in Python 3, but in the
+    Python 2 futures backport this information is discarded.
+    """
+    def __init__(self):
+        super(TracebackFuture, self).__init__()
+        self.__exc_info = None
+
+    def exc_info(self):
+        return self.__exc_info
+
+    def set_exc_info(self, exc_info):
+        """Traceback-aware replacement for `Future.set_exception`."""
+        self.__exc_info = exc_info
+        self.set_exception(exc_info[1])
+
+    def result(self):
+        if self.__exc_info is not None:
+            raise_exc_info(self.__exc_info)
+        else:
+            return super(TracebackFuture, self).result()
+
+
 class DummyExecutor(object):
     def submit(self, fn, *args, **kwargs):
-        future = Future()
+        future = TracebackFuture()
         try:
             future.set_result(fn(*args, **kwargs))
-        except Exception as e:
-            future.set_exception(e)
+        except Exception:
+            future.set_exc_info(sys.exc_info())
         return future
 
 dummy_executor = DummyExecutor()
@@ -154,13 +179,13 @@ def return_future(f):
     replacer = ArgReplacer(f, 'callback')
     @functools.wraps(f)
     def wrapper(*args, **kwargs):
-        future = Future()
+        future = TracebackFuture()
         callback, args, kwargs = replacer.replace(
             lambda value=_NO_RESULT: future.set_result(value),
             args, kwargs)
 
         def handle_error(typ, value, tb):
-            future.set_exception(value)
+            future.set_exc_info((typ, value, tb))
             return True
         exc_info = None
         with ExceptionStackContext(handle_error):
@@ -203,7 +228,10 @@ def chain_future(a, b):
     """
     def copy(future):
         assert future is a
-        if a.exception() is not None:
+        if (isinstance(a, TracebackFuture) and isinstance(b, TracebackFuture)
+            and a.exc_info() is not None):
+            b.set_exc_info(a.exc_info())
+        elif a.exception() is not None:
             b.set_exception(a.exception())
         else:
             b.set_result(a.result())
index 97a4dd1404a804be920c8affc59b045aa5dd4b40..bd58de197c496bf7f3fd8cfbf472a7cc06c498ea 100644 (file)
@@ -70,7 +70,7 @@ import itertools
 import sys
 import types
 
-from tornado.concurrent import Future
+from tornado.concurrent import Future, TracebackFuture
 from tornado.ioloop import IOLoop
 from tornado.stack_context import ExceptionStackContext
 
@@ -166,7 +166,7 @@ def coroutine(func):
     @functools.wraps(func)
     def wrapper(*args, **kwargs):
         runner = None
-        future = Future()
+        future = TracebackFuture()
 
         if 'callback' in kwargs:
             callback = kwargs.pop('callback')
@@ -177,20 +177,18 @@ def coroutine(func):
             try:
                 if runner is not None and runner.handle_exception(typ, value, tb):
                     return True
-            except Exception as e:
-                # can't just say "Exception as value" - exceptions are cleared
-                # from local namespace after except clause finishes.
-                value = e
-            future.set_exception(value)
+            except Exception:
+                typ, value, tb = sys.exc_info()
+            future.set_exc_info((typ, value, tb))
             return True
         with ExceptionStackContext(handle_exception) as deactivate:
             try:
                 result = func(*args, **kwargs)
             except (Return, StopIteration) as e:
                 result = getattr(e, 'value', None)
-            except Exception as e:
+            except Exception:
                 deactivate()
-                future.set_exception(e)
+                future.set_exc_info(sys.exc_info())
                 return future
             else:
                 if isinstance(result, types.GeneratorType):
index a431d5d0c7f220c7e4e3755c0224520a9a52e6f7..09e2c955bbba41bf29fff42022a5f5e828e5ad60 100644 (file)
@@ -36,11 +36,12 @@ import logging
 import numbers
 import os
 import select
+import sys
 import threading
 import time
 import traceback
 
-from tornado.concurrent import Future
+from tornado.concurrent import Future, TracebackFuture
 from tornado.log import app_log, gen_log
 from tornado import stack_context
 from tornado.util import Configurable
@@ -310,9 +311,9 @@ class IOLoop(Configurable):
         def run():
             try:
                 result = func()
-            except Exception as e:
-                future_cell[0] = Future()
-                future_cell[0].set_exception(e)
+            except Exception:
+                future_cell[0] = TracebackFuture()
+                future_cell[0].set_exc_info(sys.exc_info())
             else:
                 if isinstance(result, Future):
                     future_cell[0] = result
index 7291428d621573a099dbf0d0e9b2ee6fafc7e489..3eca29632b91b23abd77b27dd50cb44c482201ff 100644 (file)
@@ -18,6 +18,8 @@ from __future__ import absolute_import, division, print_function, with_statement
 import logging
 import re
 import socket
+import sys
+import traceback
 
 from tornado.concurrent import Future, return_future, ReturnValueIgnoredError
 from tornado.escape import utf8, to_unicode
@@ -143,6 +145,25 @@ class ReturnFutureTest(AsyncTestCase):
         self.assertIs(result, None)
         future.result()
 
+    @gen_test
+    def test_future_traceback(self):
+        @return_future
+        @gen.engine
+        def f(callback):
+            yield gen.Task(self.io_loop.add_callback)
+            try:
+                1 / 0
+            except ZeroDivisionError:
+                self.expected_frame = traceback.extract_tb(
+                    sys.exc_info()[2], limit=1)[0]
+                raise
+        try:
+            yield f()
+            self.fail("didn't get expected exception")
+        except ZeroDivisionError:
+            tb = traceback.extract_tb(sys.exc_info()[2])
+            self.assertIn(self.expected_frame, tb)
+
 # The following series of classes demonstrate and test various styles
 # of use, with and without generators and futures.
 class CapServer(TCPServer):