From 4f7ceedb8a7742e52b0436a4160c7c44665a2597 Mon Sep 17 00:00:00 2001 From: Alex Gr?nholm Date: Mon, 8 Sep 2014 06:46:34 +0300 Subject: Added the set_exception_info() and exception_info() methods to Future which provide the missing traceback information on Python 2.x --- CHANGES | 8 ++++++++ concurrent/futures/_base.py | 47 +++++++++++++++++++++++++++++++++++-------- concurrent/futures/_compat.py | 10 +++++++++ concurrent/futures/thread.py | 4 ++-- setup.py | 2 +- 5 files changed, 60 insertions(+), 11 deletions(-) diff --git a/CHANGES b/CHANGES index 4418f07..c99eff1 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,11 @@ +2.2.0 +===== + +- Added the set_exception_info() and exception_info() methods to Future + to enable extraction of tracebacks on Python 2.x +- Added support for Future.set_exception_info() to ThreadPoolExecutor + + 2.1.6 ===== diff --git a/concurrent/futures/_base.py b/concurrent/futures/_base.py index 8ed69b7..6f0c0f3 100644 --- a/concurrent/futures/_base.py +++ b/concurrent/futures/_base.py @@ -6,6 +6,8 @@ import logging import threading import time +from concurrent.futures._compat import reraise + try: from collections import namedtuple except ImportError: @@ -290,6 +292,7 @@ class Future(object): self._state = PENDING self._result = None self._exception = None + self._traceback = None self._waiters = [] self._done_callbacks = [] @@ -353,7 +356,7 @@ class Future(object): def __get_result(self): if self._exception: - raise self._exception + reraise(self._exception, self._traceback) else: return self._result @@ -405,8 +408,9 @@ class Future(object): else: raise TimeoutError() - def exception(self, timeout=None): - """Return the exception raised by the call that the future represents. + def exception_info(self, timeout=None): + """Return a tuple of (exception, traceback) raised by the call that the + future represents. Args: timeout: The number of seconds to wait for the exception if the @@ -422,22 +426,40 @@ class Future(object): TimeoutError: If the future didn't finish executing before the given timeout. """ - with self._condition: if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: raise CancelledError() elif self._state == FINISHED: - return self._exception + return self._exception, self._traceback self._condition.wait(timeout) if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]: raise CancelledError() elif self._state == FINISHED: - return self._exception + return self._exception, self._traceback else: raise TimeoutError() + def exception(self, timeout=None): + """Return the exception raised by the call that the future represents. + + Args: + timeout: The number of seconds to wait for the exception if the + future isn't done. If None, then there is no limit on the wait + time. + + Returns: + The exception raised by the call that the future represents or None + if the call completed without raising. + + Raises: + CancelledError: If the future was cancelled. + TimeoutError: If the future didn't finish executing before the given + timeout. + """ + return self.exception_info(timeout)[0] + # The following methods should only be used by Executors and in tests. def set_running_or_notify_cancel(self): """Mark the future as running or process any cancel notifications. @@ -492,19 +514,28 @@ class Future(object): self._condition.notify_all() self._invoke_callbacks() - def set_exception(self, exception): - """Sets the result of the future as being the given exception. + def set_exception_info(self, exception, traceback): + """Sets the result of the future as being the given exception + and traceback. Should only be used by Executor implementations and unit tests. """ with self._condition: self._exception = exception + self._traceback = traceback self._state = FINISHED for waiter in self._waiters: waiter.add_exception(self) self._condition.notify_all() self._invoke_callbacks() + def set_exception(self, exception): + """Sets the result of the future as being the given exception. + + Should only be used by Executor implementations and unit tests. + """ + self.set_exception_info(exception, None) + class Executor(object): """This is an abstract base class for concrete asynchronous executors.""" diff --git a/concurrent/futures/_compat.py b/concurrent/futures/_compat.py index 1146232..e77cf0e 100644 --- a/concurrent/futures/_compat.py +++ b/concurrent/futures/_compat.py @@ -99,3 +99,13 @@ def namedtuple(typename, field_names): result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__') return result + + +if _sys.version_info[0] < 3: + def reraise(exc, traceback): + locals_ = {'exc_type': type(exc), 'exc_value': exc, 'traceback': traceback} + exec('raise exc_type, exc_value, traceback', {}, locals_) +else: + def reraise(exc, traceback): + # Tracebacks are embedded in exceptions in Python 3 + raise exc diff --git a/concurrent/futures/thread.py b/concurrent/futures/thread.py index a45959d..930d167 100644 --- a/concurrent/futures/thread.py +++ b/concurrent/futures/thread.py @@ -60,8 +60,8 @@ class _WorkItem(object): try: result = self.fn(*self.args, **self.kwargs) except BaseException: - e = sys.exc_info()[1] - self.future.set_exception(e) + e, tb = sys.exc_info()[1:] + self.future.set_exception_info(e, tb) else: self.future.set_result(result) diff --git a/setup.py b/setup.py index 79be06c..2961f2a 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ except ImportError: from distutils.core import setup setup(name='futures', - version='2.1.6', + version='2.2.0', description='Backport of the concurrent.futures package from Python 3.2', author='Brian Quinlan', author_email='brian@sweetapp.com', -- cgit v1.2.1