From 8b5c187f4100f696d65a042e181a52143a5c3dec Mon Sep 17 00:00:00 2001 From: Jason Held Date: Wed, 11 Mar 2020 21:56:07 -0400 Subject: [PATCH] Enhancement: py3 only, and RetryHandler class support (#18) --- .travis.yml | 1 - retry_decorator/__init__.py | 2 +- retry_decorator/retry_decorator.py | 118 +++++++++++++++++++---------- setup.cfg | 2 - setup.py | 6 +- tests/test_callback.py | 11 +-- tests/test_retry.py | 1 + tox.ini | 4 +- 8 files changed, 93 insertions(+), 52 deletions(-) delete mode 100644 setup.cfg diff --git a/.travis.yml b/.travis.yml index cf41a60..aee99f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python sudo: false python: -- '2.7' - '3.5' - '3.6' - '3.7' diff --git a/retry_decorator/__init__.py b/retry_decorator/__init__.py index b95c5f9..2a9d4ca 100644 --- a/retry_decorator/__init__.py +++ b/retry_decorator/__init__.py @@ -8,4 +8,4 @@ from .retry_decorator import * __title__ = 'retry_decorator' -__version__ = "1.1.1" +__version__ = "2.0.a1" diff --git a/retry_decorator/retry_decorator.py b/retry_decorator/retry_decorator.py index 6b499e0..b5cc4b7 100644 --- a/retry_decorator/retry_decorator.py +++ b/retry_decorator/retry_decorator.py @@ -3,54 +3,90 @@ # Copyright: Patrick Ng - 2012 # -from __future__ import print_function - -import traceback import logging import time import random -import sys -def retry(ExceptionToCheck, tries=10, timeout_secs=1.0, logger=None, callback_by_exception=None): +def _deco_retry(f, exc=Exception, tries=10, timeout_secs=1.0, logger=None, callback_by_exception=None): + """ + Common function logic for the internal retry flows. + :param f: + :param exc: + :param tries: + :param timeout_secs: + :param logger: + :param callback_by_exception: + :return: + """ + def f_retry(*args, **kwargs): + mtries, mdelay = tries, timeout_secs + run_one_last_time = True + while mtries > 1: + try: + return f(*args, **kwargs) + except exc as e: + # check if this exception is something the caller wants special handling for + callback_errors = callback_by_exception or {} + for error_type in callback_errors: + if isinstance(e, error_type): + callback_logic = callback_by_exception[error_type] + should_break_out = run_one_last_time = False + if isinstance(callback_logic, (list, tuple)): + callback_logic, should_break_out = callback_logic + if isinstance(should_break_out, (list, tuple)): + should_break_out, run_one_last_time = should_break_out + callback_logic() + if should_break_out: # caller requests we stop handling this exception + break + half_interval = mdelay * 0.10 # interval size + actual_delay = random.uniform(mdelay - half_interval, mdelay + half_interval) + msg = "Retrying in %.2f seconds ..." % actual_delay + logging_object = logger or logging + logging_object.exception(msg) + time.sleep(actual_delay) + mtries -= 1 + mdelay *= 2 + if run_one_last_time: # one exception may be all the caller wanted in certain cases + return f(*args, **kwargs) + + return f_retry # true decorator + + +def retry(exc=Exception, tries=10, timeout_secs=1.0, logger=None, callback_by_exception=None): """ Retry calling the decorated function using an exponential backoff. + + :param exc: catch all exceptions, a specific exception, or an iterable of exceptions + :param tries: how many attempts to retry when catching those exceptions + :param timeout_secs: general delay between retries (we do employ a jitter) + :param logger: an optional logger object :param callback_by_exception: callback/method invocation on certain exceptions :type callback_by_exception: None or dict """ - def deco_retry(f): - def f_retry(*args, **kwargs): - mtries, mdelay = tries, timeout_secs - run_one_last_time = True - while mtries > 1: - try: - return f(*args, **kwargs) - except ExceptionToCheck as e: - # check if this exception is something the caller wants special handling for - callback_errors = callback_by_exception or {} - for error_type in callback_errors: - if isinstance(e, error_type): - callback_logic = callback_by_exception[error_type] - should_break_out = run_one_last_time = False - if isinstance(callback_logic, (list, tuple)): - callback_logic, should_break_out = callback_logic - if isinstance(should_break_out, (list, tuple)): - should_break_out, run_one_last_time = should_break_out - callback_logic() - if should_break_out: # caller requests we stop handling this exception - break - # traceback.print_exc() - half_interval = mdelay * 0.10 # interval size - actual_delay = random.uniform(mdelay - half_interval, mdelay + half_interval) - msg = "Retrying in %.2f seconds ..." % actual_delay - if logger is None: - logging.exception(msg) - else: - logger.exception(msg) - time.sleep(actual_delay) - mtries -= 1 - mdelay *= 2 - if run_one_last_time: # one exception may be all the caller wanted in certain cases - return f(*args, **kwargs) - return f_retry # true decorator - return deco_retry + # We re-use `RetryHandler` so that we can reduce duplication; decorator is still useful! + retry_handler = RetryHandler(exc, tries, timeout_secs, logger, callback_by_exception) + return retry_handler + + +class RetryHandler(object): + """ + Class supporting a more programmatic approach (not requiring a decorator) for retrying logic. + """ + __slots__ = ["exc", "tries", "timeout_secs", "logger", "callback_by_exception"] + + def __init__( + self, exc=Exception, tries=10, timeout_secs=1.0, logger=None, callback_by_exception=None, + ): + self.exc = exc + self.tries = tries + self.timeout_secs = timeout_secs + self.logger = logger + self.callback_by_exception = callback_by_exception + super().__init__() + + def __call__(self, f, *args, **kwargs): + retry_return = _deco_retry( + f, self.exc, self.tries, self.timeout_secs, self.logger, self.callback_by_exception, + ) + return retry_return diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2a9acf1..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py index a80c829..df8eb91 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,11 @@ long_description=open('README.rst').read() if exists("README.rst") else "", install_requires=[], classifiers=[ - 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ] ) diff --git a/tests/test_callback.py b/tests/test_callback.py index 37c6609..6cf919f 100644 --- a/tests/test_callback.py +++ b/tests/test_callback.py @@ -32,20 +32,21 @@ def callback_logic(instance, attr_to_set, value_to_set): setattr(instance, attr_to_set, value_to_set) -class TestError(Exception): +class ExampleTestError(Exception): pass -@retry_decorator.retry(ExceptionToCheck=TestError, tries=2, callback_by_exception={ - TestError: functools.partial(callback_logic, class_for_testing, 'hello', 'world')}) +@retry_decorator.retry(exc=ExampleTestError, tries=2, callback_by_exception={ + ExampleTestError: functools.partial(callback_logic, class_for_testing, 'hello', 'world')}) def my_test_func(): - raise TestError('oh noes.') + raise ExampleTestError('oh noes.') -@retry_decorator.retry(ExceptionToCheck=(TestError, AttributeError), tries=2, callback_by_exception={ +@retry_decorator.retry(exc=(ExampleTestError, AttributeError), tries=2, callback_by_exception={ AttributeError: functools.partial(callback_logic, class_for_testing, 'hello', 'fish')}) def my_test_func_2(): raise AttributeError('attribute oh noes.') + if __name__ == '__main__': unittest.main() diff --git a/tests/test_retry.py b/tests/test_retry.py index e91ecbc..18deb21 100755 --- a/tests/test_retry.py +++ b/tests/test_retry.py @@ -15,6 +15,7 @@ def retry_test(): print('hello', file=sys.stderr) raise Exception('Testing retry') + if __name__ == '__main__': try: retry_test() diff --git a/tox.ini b/tox.ini index 968743b..1d540f2 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distshare={homedir}/.tox/distshare -envlist=py{27,34,35,36} +envlist=py{35,36,37,38} skip_missing_interpreters=true indexserver= pypi = https://pypi.python.org/simple @@ -15,3 +15,5 @@ deps = [pytest] addopts = -vvl pep8maxlinelength=120 +markers = + pep8 \ No newline at end of file