commit
ed49cbbb70
|
@ -4,6 +4,8 @@ python:
|
||||||
- 2.6
|
- 2.6
|
||||||
- 2.7
|
- 2.7
|
||||||
- 3.2
|
- 3.2
|
||||||
|
- 3.3
|
||||||
|
- 3.4
|
||||||
- pypy
|
- pypy
|
||||||
|
|
||||||
script: python setup.py test
|
script: python setup.py test
|
||||||
|
|
|
@ -15,3 +15,7 @@ Patches and Suggestions
|
||||||
- Justin Turner Arthur
|
- Justin Turner Arthur
|
||||||
- J Derek Wilson
|
- J Derek Wilson
|
||||||
- Alex Kuang
|
- Alex Kuang
|
||||||
|
- Simon Dollé
|
||||||
|
- Rees Dooley
|
||||||
|
- Saul Shanabrook
|
||||||
|
- Daniel Nephin
|
||||||
|
|
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
History
|
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)
|
1.2.1 (2014-05-05)
|
||||||
++++++++++++++++++
|
++++++++++++++++++
|
||||||
|
|
27
retrying.py
27
retrying.py
|
@ -69,9 +69,12 @@ else:
|
||||||
# sys.maxint / 2, since Python 3.2 doesn't have a sys.maxint...
|
# sys.maxint / 2, since Python 3.2 doesn't have a sys.maxint...
|
||||||
MAX_WAIT = 1073741823
|
MAX_WAIT = 1073741823
|
||||||
|
|
||||||
|
|
||||||
def retry(*dargs, **dkw):
|
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
|
# support both @retry and @retry() as valid syntax
|
||||||
if len(dargs) == 1 and callable(dargs[0]):
|
if len(dargs) == 1 and callable(dargs[0]):
|
||||||
|
@ -105,7 +108,9 @@ class Retrying(object):
|
||||||
wait_exponential_multiplier=None, wait_exponential_max=None,
|
wait_exponential_multiplier=None, wait_exponential_max=None,
|
||||||
retry_on_exception=None,
|
retry_on_exception=None,
|
||||||
retry_on_result=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_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
|
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:
|
if stop_max_delay is not None:
|
||||||
stop_funcs.append(self.stop_after_delay)
|
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)
|
self.stop = lambda attempts, delay: any(f(attempts, delay) for f in stop_funcs)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -147,7 +155,10 @@ class Retrying(object):
|
||||||
if wait_exponential_multiplier is not None or wait_exponential_max is not None:
|
if wait_exponential_multiplier is not None or wait_exponential_max is not None:
|
||||||
wait_funcs.append(self.exponential_sleep)
|
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)
|
self.wait = lambda attempts, delay: max(f(attempts, delay) for f in wait_funcs)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -237,13 +248,18 @@ class Retrying(object):
|
||||||
|
|
||||||
delay_since_first_attempt_ms = int(round(time.time() * 1000)) - start_time
|
delay_since_first_attempt_ms = int(round(time.time() * 1000)) - start_time
|
||||||
if self.stop(attempt_number, delay_since_first_attempt_ms):
|
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:
|
else:
|
||||||
sleep = self.wait(attempt_number, delay_since_first_attempt_ms)
|
sleep = self.wait(attempt_number, delay_since_first_attempt_ms)
|
||||||
time.sleep(sleep / 1000.0)
|
time.sleep(sleep / 1000.0)
|
||||||
|
|
||||||
attempt_number += 1
|
attempt_number += 1
|
||||||
|
|
||||||
|
|
||||||
class Attempt(object):
|
class Attempt(object):
|
||||||
"""
|
"""
|
||||||
An Attempt encapsulates a call to a target function that may end as a
|
An Attempt encapsulates a call to a target function that may end as a
|
||||||
|
@ -276,6 +292,7 @@ class Attempt(object):
|
||||||
else:
|
else:
|
||||||
return "Attempts: {0}, Value: {1}".format(self.attempt_number, self.value)
|
return "Attempts: {0}, Value: {1}".format(self.attempt_number, self.value)
|
||||||
|
|
||||||
|
|
||||||
class RetryError(Exception):
|
class RetryError(Exception):
|
||||||
"""
|
"""
|
||||||
A RetryError encapsulates the last Attempt instance right before giving up.
|
A RetryError encapsulates the last Attempt instance right before giving up.
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -23,13 +23,15 @@ CLASSIFIERS = [
|
||||||
'Programming Language :: Python :: 2.6',
|
'Programming Language :: Python :: 2.6',
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3.2',
|
'Programming Language :: Python :: 3.2',
|
||||||
|
'Programming Language :: Python :: 3.3',
|
||||||
|
'Programming Language :: Python :: 3.4',
|
||||||
'Topic :: Internet',
|
'Topic :: Internet',
|
||||||
'Topic :: Utilities',
|
'Topic :: Utilities',
|
||||||
]
|
]
|
||||||
|
|
||||||
settings.update(
|
settings.update(
|
||||||
name='retrying',
|
name='retrying',
|
||||||
version='1.2.1',
|
version='1.2.3',
|
||||||
description='Retrying',
|
description='Retrying',
|
||||||
long_description=open('README.rst').read() + '\n\n' +
|
long_description=open('README.rst').read() + '\n\n' +
|
||||||
open('HISTORY.rst').read(),
|
open('HISTORY.rst').read(),
|
||||||
|
|
|
@ -40,6 +40,13 @@ class TestStopConditions(unittest.TestCase):
|
||||||
def test_legacy_explicit_stop_type(self):
|
def test_legacy_explicit_stop_type(self):
|
||||||
r = Retrying(stop="stop_after_attempt")
|
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):
|
class TestWaitConditions(unittest.TestCase):
|
||||||
|
|
||||||
def test_no_sleep(self):
|
def test_no_sleep(self):
|
||||||
|
@ -114,6 +121,13 @@ class TestWaitConditions(unittest.TestCase):
|
||||||
def test_legacy_explicit_wait_type(self):
|
def test_legacy_explicit_wait_type(self):
|
||||||
r = Retrying(wait="exponential_sleep")
|
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:
|
class NoneReturnUntilAfterCount:
|
||||||
"""
|
"""
|
||||||
This class holds counter state for invoking a method several times in a row.
|
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(t >= 250)
|
||||||
self.assertTrue(result)
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_with_stop(self):
|
def test_with_stop_on_return_value(self):
|
||||||
try:
|
try:
|
||||||
_retryable_test_with_stop(NoneReturnUntilAfterCount(5))
|
_retryable_test_with_stop(NoneReturnUntilAfterCount(5))
|
||||||
self.fail("Expected RetryError after 3 attempts")
|
self.fail("Expected RetryError after 3 attempts")
|
||||||
|
@ -292,6 +306,14 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||||
self.assertTrue(re.last_attempt.value is None)
|
self.assertTrue(re.last_attempt.value is None)
|
||||||
print(re)
|
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):
|
def test_retry_if_exception_of_type(self):
|
||||||
self.assertTrue(_retryable_test_with_exception_type_io(NoIOErrorAfterCount(5)))
|
self.assertTrue(_retryable_test_with_exception_type_io(NoIOErrorAfterCount(5)))
|
||||||
|
|
||||||
|
@ -303,7 +325,7 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||||
print(n)
|
print(n)
|
||||||
|
|
||||||
try:
|
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")
|
self.fail("Expected RetryError")
|
||||||
except RetryError as re:
|
except RetryError as re:
|
||||||
self.assertEqual(3, re.last_attempt.attempt_number)
|
self.assertEqual(3, re.last_attempt.attempt_number)
|
||||||
|
@ -323,7 +345,7 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||||
print(n)
|
print(n)
|
||||||
|
|
||||||
try:
|
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")
|
self.fail("Expected RetryError")
|
||||||
except RetryError as re:
|
except RetryError as re:
|
||||||
self.assertEqual(3, re.last_attempt.attempt_number)
|
self.assertEqual(3, re.last_attempt.attempt_number)
|
||||||
|
|
Loading…
Reference in New Issue