Repository: litl/backoff

Issue:
[Feature Suggestion] - Exception in _call_handlers
Would be great to know which exception was thrown in handlers from decorator exception tuple  in `backoff.on_exception`. Maybe receive exception as extra kwargs in _call_handlers. What you guys think?


Discussion:

Patch:
diff --git a/README.rst b/README.rst
index d80f98f..720c3a8 100644
--- a/README.rst
+++ b/README.rst
@@ -288,7 +288,9 @@ In the case of the ``on_exception`` decorator, all ``on_backoff`` and
 ``on_giveup`` handlers are called from within the except block for the
 exception being handled. Therefore exception info is available to the
 handler functions via the python standard library, specifically
-``sys.exc_info()`` or the ``traceback`` module.
+``sys.exc_info()`` or the ``traceback`` module. The exception is also
+available at the *exception* key in the `details` dict passed to the
+handlers.
 
 Asynchronous code
 -----------------
diff --git a/backoff/_async.py b/backoff/_async.py
index bf01642..0ca7084 100644
--- a/backoff/_async.py
+++ b/backoff/_async.py
@@ -156,7 +156,7 @@ def retry_exception(target, wait_gen, exception,
                                      elapsed >= max_time_value)
 
                 if giveup_result or max_tries_exceeded or max_time_exceeded:
-                    await _call_handlers(on_giveup, **details)
+                    await _call_handlers(on_giveup, **details, exception=e)
                     if raise_on_giveup:
                         raise
                     return None
@@ -165,10 +165,10 @@ def retry_exception(target, wait_gen, exception,
                     seconds = _next_wait(wait, e, jitter, elapsed,
                                          max_time_value)
                 except StopIteration:
-                    await _call_handlers(on_giveup, **details)
+                    await _call_handlers(on_giveup, **details, exception=e)
                     raise e
 
-                await _call_handlers(on_backoff, **details, wait=seconds)
+                await _call_handlers(on_backoff, **details, wait=seconds, exception=e)
 
                 # Note: there is no convenient way to pass explicit event
                 # loop to decorator, so here we assume that either default
diff --git a/backoff/_sync.py b/backoff/_sync.py
index 61b93dd..f0bd4a1 100644
--- a/backoff/_sync.py
+++ b/backoff/_sync.py
@@ -109,7 +109,7 @@ def retry_exception(target, wait_gen, exception,
                                      elapsed >= max_time_value)
 
                 if giveup(e) or max_tries_exceeded or max_time_exceeded:
-                    _call_handlers(on_giveup, **details)
+                    _call_handlers(on_giveup, **details, exception=e)
                     if raise_on_giveup:
                         raise
                     return None
@@ -118,10 +118,10 @@ def retry_exception(target, wait_gen, exception,
                     seconds = _next_wait(wait, e, jitter, elapsed,
                                          max_time_value)
                 except StopIteration:
-                    _call_handlers(on_giveup, **details)
+                    _call_handlers(on_giveup, **details, exception=e)
                     raise e
 
-                _call_handlers(on_backoff, **details, wait=seconds)
+                _call_handlers(on_backoff, **details, wait=seconds, exception=e)
 
                 time.sleep(seconds)
             else:


Test patch:
diff --git a/tests/test_backoff.py b/tests/test_backoff.py
index 25b6eb4..cd33b63 100644
--- a/tests/test_backoff.py
+++ b/tests/test_backoff.py
@@ -299,7 +299,9 @@ def test_on_exception_success():
     for i in range(2):
         details = backoffs[i]
         elapsed = details.pop('elapsed')
+        exception = details.pop('exception')
         assert isinstance(elapsed, float)
+        assert isinstance(exception, ValueError)
         assert details == {'args': (1, 2, 3),
                            'kwargs': {'foo': 1, 'bar': 2},
                            'target': succeeder._target,
@@ -345,7 +347,9 @@ def test_on_exception_giveup(raise_on_giveup):
 
     details = giveups[0]
     elapsed = details.pop('elapsed')
+    exception = details.pop('exception')
     assert isinstance(elapsed, float)
+    assert isinstance(exception, ValueError)
     assert details == {'args': (1, 2, 3),
                        'kwargs': {'foo': 1, 'bar': 2},
                        'target': exceptor._target,
@@ -517,7 +521,9 @@ def test_on_exception_success_0_arg_jitter(monkeypatch):
     for i in range(2):
         details = backoffs[i]
         elapsed = details.pop('elapsed')
+        exception = details.pop('exception')
         assert isinstance(elapsed, float)
+        assert isinstance(exception, ValueError)
         assert details == {'args': (1, 2, 3),
                            'kwargs': {'foo': 1, 'bar': 2},
                            'target': succeeder._target,
diff --git a/tests/test_backoff_async.py b/tests/test_backoff_async.py
index d2947ad..226ef08 100644
--- a/tests/test_backoff_async.py
+++ b/tests/test_backoff_async.py
@@ -255,7 +255,9 @@ async def test_on_exception_success():
     for i in range(2):
         details = log['backoff'][i]
         elapsed = details.pop('elapsed')
+        exception = details.pop('exception')
         assert isinstance(elapsed, float)
+        assert isinstance(exception, ValueError)
         assert details == {'args': (1, 2, 3),
                            'kwargs': {'foo': 1, 'bar': 2},
                            'target': succeeder._target,
@@ -302,7 +304,9 @@ async def test_on_exception_giveup(raise_on_giveup):
 
     details = log['giveup'][0]
     elapsed = details.pop('elapsed')
+    exception = details.pop('exception')
     assert isinstance(elapsed, float)
+    assert isinstance(exception, ValueError)
     assert details == {'args': (1, 2, 3),
                        'kwargs': {'foo': 1, 'bar': 2},
                        'target': exceptor._target,
@@ -521,7 +525,9 @@ async def test_on_exception_success_0_arg_jitter(monkeypatch):
     for i in range(2):
         details = log['backoff'][i]
         elapsed = details.pop('elapsed')
+        exception = details.pop('exception')
         assert isinstance(elapsed, float)
+        assert isinstance(exception, ValueError)
         assert details == {'args': (1, 2, 3),
                            'kwargs': {'foo': 1, 'bar': 2},
                            'target': succeeder._target,