diff --git a/AUTHORS.rst b/AUTHORS.rst index 42a456c..c654c4a 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -32,3 +32,4 @@ Patches and Suggestions - Jonathan Herriott - Job Evers - Cyrus Durgin +- Daniel Bennett diff --git a/README.rst b/README.rst index ab5d6bd..c417aff 100644 --- a/README.rst +++ b/README.rst @@ -141,6 +141,17 @@ We can also use the result of the function to alter the behavior of retrying. def might_return_none(): print "Retry forever ignoring Exceptions with no wait if return value is None" +Don't like RetryError on failure? Try running a callback instead. + +.. code-block:: python + + def return_last_result(attempt): + """Return the last result of the function instead of raising an exception""" + return attempt.value + + @retry(stop_max_attempt_number=3, retry_on_result=retry_if_result_none, failure_callback=return_last_result) + def eventually_return_none(): + print("Return None after trying not to") Any combination of stop, wait, etc. is also supported to give you the freedom to mix and match. diff --git a/retrying.py b/retrying.py index bcb7a9d..3a08339 100644 --- a/retrying.py +++ b/retrying.py @@ -77,7 +77,8 @@ def __init__(self, wait_func=None, wait_jitter_max=None, before_attempts=None, - after_attempts=None): + after_attempts=None, + failure_callback=None): self._stop_max_attempt_number = 5 if stop_max_attempt_number is None else stop_max_attempt_number self._stop_max_delay = 100 if stop_max_delay is None else stop_max_delay @@ -92,6 +93,7 @@ def __init__(self, self._wait_jitter_max = 0 if wait_jitter_max is None else wait_jitter_max self._before_attempts = before_attempts self._after_attempts = after_attempts + self._failure_callback = failure_callback # TODO add chaining of stop behaviors # stop behavior @@ -235,6 +237,8 @@ def call(self, fn, *args, **kwargs): delay_since_first_attempt_ms = int(round(time.time() * 1000)) - start_time if self.stop(attempt_number, delay_since_first_attempt_ms): + if self._failure_callback: + return self._failure_callback(attempt) if not self._wrap_exception and attempt.has_exception: # get() on an attempt with an exception should cause it to be raised, but raise just in case raise attempt.get() diff --git a/test_retrying.py b/test_retrying.py index 8ce4ac3..9aecec4 100644 --- a/test_retrying.py +++ b/test_retrying.py @@ -15,6 +15,7 @@ import time import unittest +from retrying import Attempt from retrying import RetryError from retrying import Retrying from retrying import retry @@ -468,5 +469,61 @@ def _test_after(): self.assertTrue(TestBeforeAfterAttempts._attempt_number is 2) + +class TestOnFailure(unittest.TestCase): + + _attempts = 3 + _attempt_number = 0 + _callback_called = False + _exception_message = "Callback should be called instead of this exception being raised." + + def tearDown(self): + self._attempt_number = 0 + self._callback_called = False + + def _callback(self, attempt): + self._callback_called = True + return attempt + + def test_failure_callback(self): + @retry(stop_max_attempt_number=self._attempts, failure_callback=self._callback) + def _run(): + self._attempt_number += 1 + raise Exception(self._exception_message) + + # should *not* raise an exception + _run() + + self.assertTrue(self._callback_called) + self.assertEqual(self._attempts, self._attempt_number) + + def test_failure_callback_callback_receives_attempt(self): + @retry(stop_max_attempt_number=self._attempts, failure_callback=self._callback) + def _run(): + self._attempt_number += 1 + raise Exception(self._exception_message) + + result = _run() + + self.assertTrue(isinstance(result, Attempt)) + + self.assertTrue(self._callback_called) + self.assertEqual(self._attempts, self._attempt_number) + + def test_failure_callback_callback_last_attempt_value(self): + @retry(stop_max_attempt_number=self._attempts, + retry_on_result=retry_if_result_none, + failure_callback=self._callback) + def _run(): + self._attempt_number += 1 + + result = _run() + + self.assertTrue(result.value is None) + + self.assertTrue(self._callback_called) + self.assertEqual(self._attempts, self._attempt_number) + + if __name__ == '__main__': unittest.main()