From: Ben Darnell Date: Mon, 4 Mar 2013 01:47:15 +0000 (-0500) Subject: Introduce TracebackFuture and use it where applicable. X-Git-Tag: v3.0.0~63 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b8063e480c2812eef284895ebfbdc286d20a2a98;p=thirdparty%2Ftornado.git Introduce TracebackFuture and use it where applicable. This allows for better tracebacks when using Futures on Python 2. --- diff --git a/tornado/concurrent.py b/tornado/concurrent.py index dd0d015f9..1779fed39 100644 --- a/tornado/concurrent.py +++ b/tornado/concurrent.py @@ -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()) diff --git a/tornado/gen.py b/tornado/gen.py index 97a4dd140..bd58de197 100644 --- a/tornado/gen.py +++ b/tornado/gen.py @@ -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): diff --git a/tornado/ioloop.py b/tornado/ioloop.py index a431d5d0c..09e2c955b 100644 --- a/tornado/ioloop.py +++ b/tornado/ioloop.py @@ -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 diff --git a/tornado/test/concurrent_test.py b/tornado/test/concurrent_test.py index 7291428d6..3eca29632 100644 --- a/tornado/test/concurrent_test.py +++ b/tornado/test/concurrent_test.py @@ -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):