diff --git a/AUTHORS b/AUTHORS index 3ae1286..5ed2328 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,5 +1,10 @@ +Andrei Suiu +Mateusz Warowny +Richard O'Dwyer +Rémy Rémy Rémy Greinhofer +invl invlpg -Richard O'Dwyer +invlpg@gmail.com williara diff --git a/ChangeLog b/ChangeLog index f537717..9e82902 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,23 +1,55 @@ CHANGES ======= +0.9.5 +----- + +* removed redundant dependencies + +0.9.4 +----- + +* Updated version to 0.9.4 with tag +* Add on\_exception handler called with captured exception. (#1) +* Updates to construct PyPi retry2 wheel package + +0.9.3 +----- + +* Added log\_traceback parameter to retry + improved logging by adding failed function reference +* Trim setup.py +* Move NullHandler to .compat +* Expose retry.retry\_call +* Relative import +* Move tests out of package +* Bump version +* Fix badges + 0.9.2 ----- +* Update ChangeLog +* Update AUTHORS +* Don't pin pytest +* tox: Add py35 +* Bump version * Updating requirements.txt to allow for any decorators >=3.4.2 0.9.1 ----- -* Fix dependency issues with other packages caused by explicit dep verions in requirements +* Updates setup.cfg version info +* Updates authors and changelog +* Adds version ranges to requirements, widening compatibility with other packages and their deps 0.9.0 ----- +* Fix typo in classifier * Add AUTHORS and ChangeLog files * Packaging the application using PBR * Update documentation -* Add retry_call function +* Add retry\_call function * Update tox.ini * Update requirements * Move the tests to the appropriate package @@ -35,7 +67,7 @@ CHANGES * v0.8.0 * dos2unix * Add argument jitter for retry() -* Add argument max_delay for retry() +* Add argument max\_delay for retry() * Refactor retry() 0.7.0 @@ -46,8 +78,8 @@ CHANGES * retry(): Update docstring * retry(): Change default tries to -1 * Move decorator() to .compat -* Add test_tries_minus1() -* Add test_tries_inf() +* Add test\_tries\_minus1() +* Add test\_tries\_inf() * Mock time.sleep in test case * Refactor retry() @@ -57,7 +89,7 @@ CHANGES * v0.6.0 * Fix inaccurate attempt counter * logger is now optional -* Extract logging_logger +* Extract logging\_logger * Make decorator module optional 0.5.0 @@ -74,14 +106,14 @@ CHANGES * Add tox.ini * Extract retry/api.py * Require pytest -* Add test_retry.py +* Add test\_retry.py * Added tag 0.4.2 for changeset 315f5f1229f6 0.4.2 ----- * Version 0.4.2 -* python2.6 support +* (untested) python2.6 support * README.rst: Add installation * Add classifiers * Add LICENSE @@ -89,7 +121,7 @@ CHANGES * Fix rST h1 h2 for README.rst * Add url * Add README.rst -* Ignore *.egg-info +* Ignore \*.egg-info * Ignore .env * Ignore .ropeproject * Ignore .git @@ -100,7 +132,7 @@ CHANGES * Version 0.4.1 * Add license -* Add long_description +* Add long\_description * Add docstring for retry() * Added tag 0.4.0 for changeset e053cae4b105 diff --git a/LICENSE b/LICENSE index 3a2cc32..4997ed8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2014 invl +Copyright 2021 andrei.suiu@gmail.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.rst b/README.rst index e1d0e9c..72c3b64 100644 --- a/README.rst +++ b/README.rst @@ -1,19 +1,21 @@ -retry -===== +retry2 +====== -.. image:: https://img.shields.io/pypi/dm/retry.svg?maxAge=2592000 - :target: https://pypi.python.org/pypi/retry/ +.. image:: https://img.shields.io/pypi/dm/retry2.svg?maxAge=2592000 + :target: https://pypi.python.org/pypi/retry2/ -.. image:: https://img.shields.io/pypi/v/retry.svg?maxAge=2592000 - :target: https://pypi.python.org/pypi/retry/ +.. image:: https://img.shields.io/pypi/v/retry2.svg?maxAge=2592000 + :target: https://pypi.python.org/pypi/retry2/ -.. image:: https://img.shields.io/pypi/l/retry.svg?maxAge=2592000 - :target: https://pypi.python.org/pypi/retry/ +.. image:: https://img.shields.io/pypi/l/retry2.svg?maxAge=2592000 + :target: https://pypi.python.org/pypi/retry2/ Easy to use retry decorator. +[This is a fork of https://github.com/invl/retry which is not maintained anymore] + Features -------- @@ -27,7 +29,7 @@ Installation .. code-block:: bash - $ pip install retry + $ pip install retry2 API @@ -38,7 +40,8 @@ retry decorator .. code:: python - def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger): + def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger, + on_exception=None): """Return a retry decorator. :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. @@ -50,6 +53,9 @@ retry decorator fixed if a number, random if a range tuple (min, max) :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. default: retry.logging_logger. if None, logging is disabled. + :param on_exception: handler called when exception occurs. will be passed the captured + exception as an argument. further retries are stopped when handler + returns True. default: None """ Various retrying logic can be achieved by combination of arguments. @@ -107,8 +113,7 @@ retry_call .. code:: python def retry_call(f, fargs=None, fkwargs=None, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, - jitter=0, - logger=logging_logger): + jitter=0, logger=logging_logger, on_exception=None): """ Calls a function and re-executes it if it failed. @@ -124,6 +129,9 @@ retry_call fixed if a number, random if a range tuple (min, max) :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. default: retry.logging_logger. if None, logging is disabled. + :param on_exception: handler called when exception occurs. will be passed the captured + exception as an argument. further retries are stopped when handler + returns True. default: None :returns: the result of the f function. """ diff --git a/requirements.txt b/requirements.txt index 5c13ceb..8b66863 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ decorator>=3.4.2 -py>=1.4.26,<2.0.0 diff --git a/retry/api.py b/retry/api.py index 4a404b9..4856442 100644 --- a/retry/api.py +++ b/retry/api.py @@ -1,17 +1,16 @@ import logging import random import time - +import traceback from functools import partial from .compat import decorator - logging_logger = logging.getLogger(__name__) def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, - logger=logging_logger): + logger=logging_logger, log_traceback=False, on_exception=None): """ Executes a function and retries it if it failed. @@ -25,6 +24,9 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, fixed if a number, random if a range tuple (min, max) :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. default: retry.logging_logger. if None, logging is disabled. + :param on_exception: handler called when exception occurs. will be passed the captured + exception as an argument. further retries are stopped when handler + returns True. default: None :returns: the result of the f function. """ _tries, _delay = tries, delay @@ -32,12 +34,23 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, try: return f() except exceptions as e: + if on_exception is not None: + if on_exception(e): + break + _tries -= 1 if not _tries: raise if logger is not None: - logger.warning('%s, retrying in %s seconds...', e, _delay) + try: + func_qualname = f.func.__qualname__ + except AttributeError: + func_qualname = str(f.func) + logger.warning('{}: {} in {}.{}, retrying in {} seconds...'.format(e.__class__.__qualname__, e, + f.func.__module__, func_qualname, _delay)) + if log_traceback: + logger.warning(traceback.format_exc()) time.sleep(_delay) _delay *= backoff @@ -51,7 +64,8 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, _delay = min(_delay, max_delay) -def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger): +def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger, + log_traceback=False, on_exception=None): """Returns a retry decorator. :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. @@ -63,6 +77,9 @@ def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, ji fixed if a number, random if a range tuple (min, max) :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. default: retry.logging_logger. if None, logging is disabled. + :param on_exception: handler called when exception occurs. will be passed the captured + exception as an argument. further retries are stopped when handler + returns True. default: None :returns: a retry decorator. """ @@ -71,14 +88,13 @@ def retry_decorator(f, *fargs, **fkwargs): args = fargs if fargs else list() kwargs = fkwargs if fkwargs else dict() return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter, - logger) + logger, log_traceback, on_exception) return retry_decorator def retry_call(f, fargs=None, fkwargs=None, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, - jitter=0, - logger=logging_logger): + jitter=0, logger=logging_logger, log_traceback=False, on_exception=None): """ Calls a function and re-executes it if it failed. @@ -94,8 +110,12 @@ def retry_call(f, fargs=None, fkwargs=None, exceptions=Exception, tries=-1, dela fixed if a number, random if a range tuple (min, max) :param logger: logger.warning(fmt, error, delay) will be called on failed attempts. default: retry.logging_logger. if None, logging is disabled. + :param on_exception: handler called when exception occurs. will be passed the captured + exception as an argument. further retries are stopped when handler + returns True. default: None :returns: the result of the f function. """ args = fargs if fargs else list() kwargs = fkwargs if fkwargs else dict() - return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter, logger) + return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter, logger, + log_traceback, on_exception) diff --git a/setup.cfg b/setup.cfg index d723671..0dd803e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,12 @@ [metadata] -name = retry -version = 0.9.3 -author = invl -author-email = invlpg@gmail.com +name = retry2 +version = 0.9.5 +author = Andrei Suiu | eSAMTrade +author-email = andrei.suiu@gmail.com summary = Easy to use retry decorator. license = Apache License 2.0 description-file = README.rst -home-page = https://github.com/invl/retry +home-page = https://github.com/eSAMTrade/retry requires-python = >=2.6 classifier = Development Status :: 4 - Beta @@ -18,7 +18,6 @@ classifier = Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development diff --git a/test-requirements.txt b/test-requirements.txt index b84cb24..210371c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ -mock +-r requirements.txt pbr pytest tox diff --git a/tests/test_retry.py b/tests/test_retry.py index 64f45cd..ddc2aa5 100644 --- a/tests/test_retry.py +++ b/tests/test_retry.py @@ -1,3 +1,7 @@ +import logging +from io import StringIO +from uuid import uuid1 + try: from unittest.mock import create_autospec except ImportError: @@ -53,6 +57,7 @@ def f(): return target else: raise ValueError + assert f() == target @@ -67,6 +72,7 @@ def f(): return target else: raise ValueError + assert f() == target @@ -146,7 +152,6 @@ def test_retry_call_2(): def test_retry_call_with_args(): - def f(value=0): if value < 0: return value @@ -166,7 +171,6 @@ def f(value=0): def test_retry_call_with_kwargs(): - def f(value=0): if value < 0: return value @@ -183,3 +187,52 @@ def f(value=0): assert result == kwargs['value'] assert f_mock.call_count == 1 + + +def test_call_on_exception(): + exception = RuntimeError() + f_mock = MagicMock(side_effect=exception) + callback_mock = MagicMock() + try: + retry_call(f_mock, tries=1, on_exception=callback_mock) + except RuntimeError: + pass + callback_mock.assert_called_once_with(exception) + + +def test_logs_function_details(monkeypatch): + mock_sleep_time = [0] + + def mock_sleep(seconds): + mock_sleep_time[0] += seconds + + monkeypatch.setattr(time, 'sleep', mock_sleep) + + hit = [0] + + tries = 3 + fails = 2 + delay = 1 + backoff = 2 + max_delay = delay # Never increase delay + logger_name = str(uuid1()) + logger = logging.getLogger(logger_name) + logger.setLevel(logging.WARNING) + logging_stream = StringIO() + handler = logging.StreamHandler(logging_stream) + logger.addHandler(handler) + + @retry(exceptions=ZeroDivisionError, tries=tries, delay=delay, max_delay=max_delay, backoff=backoff, logger=logger, + log_traceback=True) + def f(): + hit[0] += 1 + if hit[0] <= fails: + 1 / 0 + + f() + log_value = logging_stream.getvalue() + assert log_value.startswith( + "ZeroDivisionError: division by zero in test_retry.test_logs_function_details..f, retrying in 1 seconds...") + assert log_value.endswith("ZeroDivisionError: division by zero\n\n") + assert hit[0] == fails + 1 + assert mock_sleep_time[0] == delay * fails diff --git a/upload.bat b/upload.bat new file mode 100644 index 0000000..415d4f5 --- /dev/null +++ b/upload.bat @@ -0,0 +1,3 @@ +del ./dist/*.whl +python setup.py bdist_wheel %* +twine upload dist/*.whl -u asuiu --verbose \ No newline at end of file diff --git a/upload.sh b/upload.sh new file mode 100644 index 0000000..2c8ad02 --- /dev/null +++ b/upload.sh @@ -0,0 +1,4 @@ +#!/bin/bash +rm ./dist/*.whl +python setup.py sdist bdist_wheel +twine upload dist/*.whl \ No newline at end of file