diff --git a/.travis.yml b/.travis.yml index b8ca6e8..fca311c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ python: - 2.6 - 2.7 - 3.2 + - 3.3 + - 3.4 - pypy -script: python setup.py test \ No newline at end of file +script: python setup.py test diff --git a/AUTHORS.rst b/AUTHORS.rst index 8c1aa55..4803ca4 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -15,3 +15,7 @@ Patches and Suggestions - Justin Turner Arthur - J Derek Wilson - Alex Kuang +- Simon Dollé +- Rees Dooley +- Saul Shanabrook +- Daniel Nephin diff --git a/HISTORY.rst b/HISTORY.rst index 00f054b..ba89481 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -2,6 +2,13 @@ History ------- +1.2.3 (2014-08-25) +++++++++++++++++++ +- Add support for custom wait and stop functions + +1.2.2 (2014-06-20) +++++++++++++++++++ +- Bug fix to not raise a RetryError on failure when exceptions aren't being wrapped 1.2.1 (2014-05-05) ++++++++++++++++++ diff --git a/retrying.py b/retrying.py index b337744..50fc439 100644 --- a/retrying.py +++ b/retrying.py @@ -69,9 +69,12 @@ else: # sys.maxint / 2, since Python 3.2 doesn't have a sys.maxint... MAX_WAIT = 1073741823 + def retry(*dargs, **dkw): """ - TODO comment + Decorator function that instantiates the Retrying object + @param *dargs: positional arguments passed to Retrying object + @param **dkw: keyword arguments passed to the Retrying object """ # support both @retry and @retry() as valid syntax if len(dargs) == 1 and callable(dargs[0]): @@ -105,7 +108,9 @@ class Retrying(object): wait_exponential_multiplier=None, wait_exponential_max=None, retry_on_exception=None, retry_on_result=None, - wrap_exception=False): + wrap_exception=False, + stop_func=None, + wait_func=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 @@ -126,7 +131,10 @@ class Retrying(object): if stop_max_delay is not None: stop_funcs.append(self.stop_after_delay) - if stop is None: + if stop_func is not None: + self.stop = stop_func + + elif stop is None: self.stop = lambda attempts, delay: any(f(attempts, delay) for f in stop_funcs) else: @@ -147,7 +155,10 @@ class Retrying(object): if wait_exponential_multiplier is not None or wait_exponential_max is not None: wait_funcs.append(self.exponential_sleep) - if wait is None: + if wait_func is not None: + self.wait = wait_func + + elif wait is None: self.wait = lambda attempts, delay: max(f(attempts, delay) for f in wait_funcs) else: @@ -237,13 +248,18 @@ class Retrying(object): delay_since_first_attempt_ms = int(round(time.time() * 1000)) - start_time if self.stop(attempt_number, delay_since_first_attempt_ms): - raise RetryError(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() + else: + raise RetryError(attempt) else: sleep = self.wait(attempt_number, delay_since_first_attempt_ms) time.sleep(sleep / 1000.0) attempt_number += 1 + class Attempt(object): """ An Attempt encapsulates a call to a target function that may end as a @@ -276,6 +292,7 @@ class Attempt(object): else: return "Attempts: {0}, Value: {1}".format(self.attempt_number, self.value) + class RetryError(Exception): """ A RetryError encapsulates the last Attempt instance right before giving up. diff --git a/setup.py b/setup.py index f87576a..7793b8f 100644 --- a/setup.py +++ b/setup.py @@ -23,13 +23,15 @@ CLASSIFIERS = [ 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', 'Topic :: Internet', 'Topic :: Utilities', ] settings.update( name='retrying', - version='1.2.1', + version='1.2.3', description='Retrying', long_description=open('README.rst').read() + '\n\n' + open('HISTORY.rst').read(), diff --git a/test_retrying.py b/test_retrying.py index 43c9000..da440b0 100644 --- a/test_retrying.py +++ b/test_retrying.py @@ -40,6 +40,13 @@ class TestStopConditions(unittest.TestCase): def test_legacy_explicit_stop_type(self): r = Retrying(stop="stop_after_attempt") + def test_stop_func(self): + r = Retrying(stop_func=lambda attempt, delay: attempt == delay) + self.assertFalse(r.stop(1, 3)) + self.assertFalse(r.stop(100, 99)) + self.assertTrue(r.stop(101, 101)) + + class TestWaitConditions(unittest.TestCase): def test_no_sleep(self): @@ -114,6 +121,13 @@ class TestWaitConditions(unittest.TestCase): def test_legacy_explicit_wait_type(self): r = Retrying(wait="exponential_sleep") + def test_wait_func(self): + r = Retrying(wait_func=lambda attempt, delay: attempt * delay) + self.assertEqual(r.wait(1, 5), 5) + self.assertEqual(r.wait(2, 11), 22) + self.assertEqual(r.wait(10, 100), 1000) + + class NoneReturnUntilAfterCount: """ This class holds counter state for invoking a method several times in a row. @@ -282,7 +296,7 @@ class TestDecoratorWrapper(unittest.TestCase): self.assertTrue(t >= 250) self.assertTrue(result) - def test_with_stop(self): + def test_with_stop_on_return_value(self): try: _retryable_test_with_stop(NoneReturnUntilAfterCount(5)) self.fail("Expected RetryError after 3 attempts") @@ -292,6 +306,14 @@ class TestDecoratorWrapper(unittest.TestCase): self.assertTrue(re.last_attempt.value is None) print(re) + def test_with_stop_on_exception(self): + try: + _retryable_test_with_stop(NoIOErrorAfterCount(5)) + self.fail("Expected IOError") + except IOError as re: + self.assertTrue(isinstance(re, IOError)) + print(re) + def test_retry_if_exception_of_type(self): self.assertTrue(_retryable_test_with_exception_type_io(NoIOErrorAfterCount(5))) @@ -303,7 +325,7 @@ class TestDecoratorWrapper(unittest.TestCase): print(n) try: - _retryable_test_with_exception_type_io_attempt_limit(NoIOErrorAfterCount(5)) + _retryable_test_with_exception_type_io_attempt_limit_wrap(NoIOErrorAfterCount(5)) self.fail("Expected RetryError") except RetryError as re: self.assertEqual(3, re.last_attempt.attempt_number) @@ -323,7 +345,7 @@ class TestDecoratorWrapper(unittest.TestCase): print(n) try: - _retryable_test_with_exception_type_custom_attempt_limit(NoCustomErrorAfterCount(5)) + _retryable_test_with_exception_type_custom_attempt_limit_wrap(NoCustomErrorAfterCount(5)) self.fail("Expected RetryError") except RetryError as re: self.assertEqual(3, re.last_attempt.attempt_number)