Merge pull request #2 from rholder/1-propagate-tracebacks
propagate complete tracebacks #1
This commit is contained in:
commit
49551b1bc0
68
retrying.py
68
retrying.py
@ -1,4 +1,4 @@
|
||||
## Copyright 2013 Ray Holder
|
||||
## Copyright 2013-2014 Ray Holder
|
||||
##
|
||||
## Licensed under the Apache License, Version 2.0 (the "License");
|
||||
## you may not use this file except in compliance with the License.
|
||||
@ -12,9 +12,59 @@
|
||||
## See the License for the specific language governing permissions and
|
||||
## limitations under the License.
|
||||
|
||||
## --- The following is for portions of the "six" module ----------------------
|
||||
##
|
||||
## Copyright (c) 2010-2014 Benjamin Peterson
|
||||
##
|
||||
## Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
## of this software and associated documentation files (the "Software"), to deal
|
||||
## in the Software without restriction, including without limitation the rights
|
||||
## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
## copies of the Software, and to permit persons to whom the Software is
|
||||
## furnished to do so, subject to the following conditions:
|
||||
##
|
||||
## The above copyright notice and this permission notice shall be included in all
|
||||
## copies or substantial portions of the Software.
|
||||
##
|
||||
## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
## SOFTWARE.
|
||||
## ----------------------------------------------------------------------------
|
||||
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
# Python 3 compatibility hacks, pilfered from https://pypi.python.org/pypi/six/1.6.1
|
||||
PY3 = sys.version_info[0] == 3
|
||||
if PY3:
|
||||
def reraise(tp, value, tb=None):
|
||||
if value.__traceback__ is not tb:
|
||||
raise value.with_traceback(tb)
|
||||
raise value
|
||||
|
||||
else:
|
||||
def exec_(_code_, _globs_=None, _locs_=None):
|
||||
"""Execute code in a namespace."""
|
||||
if _globs_ is None:
|
||||
frame = sys._getframe(1)
|
||||
_globs_ = frame.f_globals
|
||||
if _locs_ is None:
|
||||
_locs_ = frame.f_locals
|
||||
del frame
|
||||
elif _locs_ is None:
|
||||
_locs_ = _globs_
|
||||
exec("""exec _code_ in _globs_, _locs_""")
|
||||
|
||||
|
||||
exec_("""def reraise(tp, value, tb=None):
|
||||
raise tp, value, tb
|
||||
""")
|
||||
|
||||
# sys.maxint / 2, since Python 3.2 doesn't have a sys.maxint...
|
||||
MAX_WAIT = 1073741823
|
||||
@ -139,7 +189,7 @@ class Retrying:
|
||||
def should_reject(self, attempt):
|
||||
reject = False
|
||||
if attempt.has_exception:
|
||||
reject |= self._retry_on_exception(attempt.value)
|
||||
reject |= self._retry_on_exception(attempt.value[1])
|
||||
else:
|
||||
reject |= self._retry_on_result(attempt.value)
|
||||
|
||||
@ -152,8 +202,8 @@ class Retrying:
|
||||
try:
|
||||
attempt = Attempt(fn(*args, **kwargs), attempt_number, False)
|
||||
except:
|
||||
e = sys.exc_info()[1]
|
||||
attempt = Attempt(e, attempt_number, True)
|
||||
tb = sys.exc_info()
|
||||
attempt = Attempt(tb, attempt_number, True)
|
||||
|
||||
if not self.should_reject(attempt):
|
||||
return attempt.get(self._wrap_exception)
|
||||
@ -189,10 +239,16 @@ class Attempt:
|
||||
if wrap_exception:
|
||||
raise RetryError(self)
|
||||
else:
|
||||
raise self.value
|
||||
reraise(self.value[0], self.value[1], self.value[2])
|
||||
else:
|
||||
return self.value
|
||||
|
||||
def __repr__(self):
|
||||
if self.has_exception:
|
||||
return "Attempts: {0}, Error:\n{1}".format(self.attempt_number, "".join(traceback.format_tb(self.value[2])))
|
||||
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.
|
||||
@ -202,4 +258,4 @@ class RetryError(Exception):
|
||||
self.last_attempt = last_attempt
|
||||
|
||||
def __str__(self):
|
||||
return "Last attempt: %s" % str(self.last_attempt)
|
||||
return "RetryError[{0}]".format(self.last_attempt)
|
||||
|
@ -141,7 +141,7 @@ class NoIOErrorAfterCount:
|
||||
"""
|
||||
if self.counter < self.count:
|
||||
self.counter += 1
|
||||
raise IOError()
|
||||
raise IOError("Hi there, I'm an IOError")
|
||||
return True
|
||||
|
||||
class NoNameErrorAfterCount:
|
||||
@ -159,7 +159,7 @@ class NoNameErrorAfterCount:
|
||||
"""
|
||||
if self.counter < self.count:
|
||||
self.counter += 1
|
||||
raise NameError()
|
||||
raise NameError("Hi there, I'm a NameError")
|
||||
return True
|
||||
|
||||
class CustomError(Exception):
|
||||
@ -201,6 +201,7 @@ def retry_if_result_none(result):
|
||||
|
||||
def retry_if_exception_of_type(retryable_types):
|
||||
def retry_if_exception_these_types(exception):
|
||||
print("Detected Exception of type: {0}".format(str(type(exception))))
|
||||
return isinstance(exception, retryable_types)
|
||||
return retry_if_exception_these_types
|
||||
|
||||
@ -282,8 +283,11 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||
try:
|
||||
_retryable_test_with_stop(NoneReturnUntilAfterCount(5))
|
||||
self.fail("Expected RetryError after 3 attempts")
|
||||
except RetryError as e:
|
||||
self.assertEqual(3, e.last_attempt.attempt_number)
|
||||
except RetryError as re:
|
||||
self.assertFalse(re.last_attempt.has_exception)
|
||||
self.assertEqual(3, re.last_attempt.attempt_number)
|
||||
self.assertTrue(re.last_attempt.value is None)
|
||||
print(re)
|
||||
|
||||
def test_retry_if_exception_of_type(self):
|
||||
self.assertTrue(_retryable_test_with_exception_type_io(NoIOErrorAfterCount(5)))
|
||||
@ -293,6 +297,7 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||
self.fail("Expected NameError")
|
||||
except NameError as n:
|
||||
self.assertTrue(isinstance(n, NameError))
|
||||
print(n)
|
||||
|
||||
try:
|
||||
_retryable_test_with_exception_type_io_attempt_limit(NoIOErrorAfterCount(5))
|
||||
@ -300,7 +305,10 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||
except RetryError as re:
|
||||
self.assertEqual(3, re.last_attempt.attempt_number)
|
||||
self.assertTrue(re.last_attempt.has_exception)
|
||||
self.assertTrue(isinstance(re.last_attempt.value, IOError))
|
||||
self.assertTrue(re.last_attempt.value[0] is not None)
|
||||
self.assertTrue(isinstance(re.last_attempt.value[1], IOError))
|
||||
self.assertTrue(re.last_attempt.value[2] is not None)
|
||||
print(re)
|
||||
|
||||
self.assertTrue(_retryable_test_with_exception_type_custom(NoCustomErrorAfterCount(5)))
|
||||
|
||||
@ -309,6 +317,7 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||
self.fail("Expected NameError")
|
||||
except NameError as n:
|
||||
self.assertTrue(isinstance(n, NameError))
|
||||
print(n)
|
||||
|
||||
try:
|
||||
_retryable_test_with_exception_type_custom_attempt_limit(NoCustomErrorAfterCount(5))
|
||||
@ -316,7 +325,10 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||
except RetryError as re:
|
||||
self.assertEqual(3, re.last_attempt.attempt_number)
|
||||
self.assertTrue(re.last_attempt.has_exception)
|
||||
self.assertTrue(isinstance(re.last_attempt.value, CustomError))
|
||||
self.assertTrue(re.last_attempt.value[0] is not None)
|
||||
self.assertTrue(isinstance(re.last_attempt.value[1], CustomError))
|
||||
self.assertTrue(re.last_attempt.value[2] is not None)
|
||||
print(re)
|
||||
|
||||
def test_wrapped_exception(self):
|
||||
|
||||
@ -326,8 +338,9 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||
try:
|
||||
_retryable_test_with_exception_type_io_wrap(NoNameErrorAfterCount(5))
|
||||
self.fail("Expected RetryError")
|
||||
except RetryError as r:
|
||||
self.assertTrue(isinstance(r.last_attempt.value, NameError))
|
||||
except RetryError as re:
|
||||
self.assertTrue(isinstance(re.last_attempt.value[1], NameError))
|
||||
print(re)
|
||||
|
||||
try:
|
||||
_retryable_test_with_exception_type_io_attempt_limit_wrap(NoIOErrorAfterCount(5))
|
||||
@ -335,7 +348,10 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||
except RetryError as re:
|
||||
self.assertEqual(3, re.last_attempt.attempt_number)
|
||||
self.assertTrue(re.last_attempt.has_exception)
|
||||
self.assertTrue(isinstance(re.last_attempt.value, IOError))
|
||||
self.assertTrue(re.last_attempt.value[0] is not None)
|
||||
self.assertTrue(isinstance(re.last_attempt.value[1], IOError))
|
||||
self.assertTrue(re.last_attempt.value[2] is not None)
|
||||
print(re)
|
||||
|
||||
# custom error cases
|
||||
self.assertTrue(_retryable_test_with_exception_type_custom_wrap(NoCustomErrorAfterCount(5)))
|
||||
@ -343,8 +359,11 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||
try:
|
||||
_retryable_test_with_exception_type_custom_wrap(NoNameErrorAfterCount(5))
|
||||
self.fail("Expected RetryError")
|
||||
except RetryError as r:
|
||||
self.assertTrue(isinstance(r.last_attempt.value, NameError))
|
||||
except RetryError as re:
|
||||
self.assertTrue(re.last_attempt.value[0] is not None)
|
||||
self.assertTrue(isinstance(re.last_attempt.value[1], NameError))
|
||||
self.assertTrue(re.last_attempt.value[2] is not None)
|
||||
print(re)
|
||||
|
||||
try:
|
||||
_retryable_test_with_exception_type_custom_attempt_limit_wrap(NoCustomErrorAfterCount(5))
|
||||
@ -352,7 +371,10 @@ class TestDecoratorWrapper(unittest.TestCase):
|
||||
except RetryError as re:
|
||||
self.assertEqual(3, re.last_attempt.attempt_number)
|
||||
self.assertTrue(re.last_attempt.has_exception)
|
||||
self.assertTrue(isinstance(re.last_attempt.value, CustomError))
|
||||
self.assertTrue(re.last_attempt.value[0] is not None)
|
||||
self.assertTrue(isinstance(re.last_attempt.value[1], CustomError))
|
||||
self.assertTrue(re.last_attempt.value[2] is not None)
|
||||
print(re)
|
||||
|
||||
def test_defaults(self):
|
||||
self.assertTrue(_retryable_default(NoNameErrorAfterCount(5)))
|
||||
|
Loading…
Reference in New Issue
Block a user