From 03a2f0591141f8017927b8fc2bbcc556ef3e453d Mon Sep 17 00:00:00 2001 From: ASU Date: Mon, 19 Apr 2021 09:38:15 +0300 Subject: [PATCH 1/9] Added log_traceback parameter to retry + improved logging by adding failed function reference --- retry/api.py | 25 +++++++++++++++--------- tests/test_retry.py | 46 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/retry/api.py b/retry/api.py index 4a404b9..a2d1729 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): """ Executes a function and retries it if it failed. @@ -37,7 +36,14 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, 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('%s: %s in %s.%s, retrying in %s seconds...', 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 +57,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): """Returns a retry decorator. :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. @@ -71,14 +78,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=log_traceback) 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): """ Calls a function and re-executes it if it failed. @@ -98,4 +104,5 @@ def retry_call(f, fargs=None, fkwargs=None, exceptions=Exception, tries=-1, dela """ 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=log_traceback) diff --git a/tests/test_retry.py b/tests/test_retry.py index 64f45cd..556ec0a 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,41 @@ def f(value=0): assert result == kwargs['value'] assert f_mock.call_count == 1 + + +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 From efcb3265316be3f0aaa1891f707237440c8e5941 Mon Sep 17 00:00:00 2001 From: ASU Date: Mon, 26 Jul 2021 17:24:57 +0300 Subject: [PATCH 2/9] Updates to construct PyPi retry2 wheel package --- AUTHORS | 1 + ChangeLog | 40 ++++++++++++++++++++++++++++++---------- LICENSE | 2 +- README.rst | 16 +++++++++------- setup.cfg | 7 +++---- test-requirements.txt | 1 + upload.bat | 3 +++ upload.sh | 4 ++++ 8 files changed, 52 insertions(+), 22 deletions(-) create mode 100644 upload.bat create mode 100644 upload.sh diff --git a/AUTHORS b/AUTHORS index 3ae1286..77fa0ee 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,4 @@ +ASU Rémy Rémy Greinhofer invlpg diff --git a/ChangeLog b/ChangeLog index f537717..7a1b666 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,23 +1,43 @@ CHANGES ======= +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 +55,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 +66,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 +77,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 +94,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 +109,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 +120,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..8de7941 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/ + :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/ + :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 diff --git a/setup.cfg b/setup.cfg index d723671..81a4a20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,12 @@ [metadata] -name = retry +name = retry2 version = 0.9.3 author = invl -author-email = invlpg@gmail.com +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..6533772 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ +-r requirements.txt mock pbr pytest 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 From 093ac26ccc1cd06e525795a9ab4500dd69b0d2d6 Mon Sep 17 00:00:00 2001 From: Mateusz Warowny Date: Sun, 5 Dec 2021 00:25:36 +0100 Subject: [PATCH 3/9] Add on_exception handler called with captured exception. (#1) Add on_exception handler called with captured exception by @warownia1 --- ChangeLog | 6 ++++++ README.rst | 12 +++++++++--- retry/api.py | 23 ++++++++++++++++++----- tests/test_retry.py | 11 +++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/ChangeLog b/ChangeLog index 7a1b666..34f68f4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,12 @@ CHANGES ======= +0.9.4 +----- + +* Add on_exception parameter accepting a handler called when exception is captured. + The callback can also be used to handle the exception and interrupt further retries. + 0.9.3 ----- diff --git a/README.rst b/README.rst index 8de7941..17a2591 100644 --- a/README.rst +++ b/README.rst @@ -40,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. @@ -52,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. @@ -109,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. @@ -126,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/retry/api.py b/retry/api.py index a2d1729..eaccfef 100644 --- a/retry/api.py +++ b/retry/api.py @@ -10,7 +10,7 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, - logger=logging_logger, log_traceback=False): + logger=logging_logger, log_traceback=False, on_exception=None): """ Executes a function and retries it if it failed. @@ -24,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 @@ -31,6 +34,10 @@ 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 @@ -58,7 +65,7 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger, - log_traceback=False): + log_traceback=False, on_exception=None): """Returns a retry decorator. :param exceptions: an exception or a tuple of exceptions to catch. default: Exception. @@ -70,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. """ @@ -78,13 +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, log_traceback=log_traceback) + 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, log_traceback=False): + jitter=0, logger=logging_logger, log_traceback=False, on_exception=None): """ Calls a function and re-executes it if it failed. @@ -100,9 +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, - log_traceback=log_traceback) + log_traceback, on_exception) diff --git a/tests/test_retry.py b/tests/test_retry.py index 556ec0a..ddc2aa5 100644 --- a/tests/test_retry.py +++ b/tests/test_retry.py @@ -189,6 +189,17 @@ def f(value=0): 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] From 3ab94bfb8d0887267d7ca4a33f39e22358400713 Mon Sep 17 00:00:00 2001 From: ASU Date: Sun, 7 Aug 2022 20:31:24 +0300 Subject: [PATCH 4/9] Updated version to 0.9.4 with tag --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 81a4a20..0aa9872 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = retry2 -version = 0.9.3 +version = 0.9.4 author = invl author-email = andrei.suiu@gmail.com summary = Easy to use retry decorator. From 5444a68944b3c7c4183384aade25b2988f4741a6 Mon Sep 17 00:00:00 2001 From: PyRSA <83496233+PyRSA@users.noreply.github.com> Date: Thu, 22 Sep 2022 07:22:53 +0800 Subject: [PATCH 5/9] add loguru logger object support (#3) --- retry/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/retry/api.py b/retry/api.py index eaccfef..4856442 100644 --- a/retry/api.py +++ b/retry/api.py @@ -47,8 +47,8 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, func_qualname = f.func.__qualname__ except AttributeError: func_qualname = str(f.func) - logger.warning('%s: %s in %s.%s, retrying in %s seconds...', e.__class__.__qualname__, e, - f.func.__module__, func_qualname, _delay) + 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()) From 0bbf84adadb1883353f7b3c7dfb38f31a45e4a45 Mon Sep 17 00:00:00 2001 From: Jonathan Wright <8390543+jonathanspw@users.noreply.github.com> Date: Wed, 11 Jan 2023 15:34:06 -0600 Subject: [PATCH 6/9] Use built in unittest.mock (#8) the separate package is deprecated --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 6533772..210371c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,4 @@ -r requirements.txt -mock pbr pytest tox From 1fef48c149d11b511aab6dff2d0fcdfefe9e03ee Mon Sep 17 00:00:00 2001 From: Pavlo Shchelokovskyy Date: Wed, 11 Jan 2023 23:40:11 +0200 Subject: [PATCH 7/9] Remove dependency on Py package (#7) this package is not actually used in the code of this project Closes #6 --- requirements.txt | 1 - 1 file changed, 1 deletion(-) 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 From ff6e88999818070a3012191c3e949fbdc8131cae Mon Sep 17 00:00:00 2001 From: ASU Date: Wed, 11 Jan 2023 23:46:51 +0200 Subject: [PATCH 8/9] v0.9.5 release --- AUTHORS | 8 ++++++-- ChangeLog | 10 ++++++++-- setup.cfg | 4 ++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/AUTHORS b/AUTHORS index 77fa0ee..5ed2328 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,10 @@ -ASU +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 34f68f4..9e82902 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,11 +1,17 @@ CHANGES ======= +0.9.5 +----- + +* removed redundant dependencies + 0.9.4 ----- -* Add on_exception parameter accepting a handler called when exception is captured. - The callback can also be used to handle the exception and interrupt further retries. +* 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 ----- diff --git a/setup.cfg b/setup.cfg index 0aa9872..0dd803e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = retry2 -version = 0.9.4 -author = invl +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 From a02dfcd94783a971ab180dafba022fe5d469e586 Mon Sep 17 00:00:00 2001 From: silversocket <59583600+silversocket@users.noreply.github.com> Date: Fri, 24 Feb 2023 12:05:09 -0500 Subject: [PATCH 9/9] Update README images to point to retry2 (#10) --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 17a2591..72c3b64 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,10 @@ retry2 ====== -.. image:: https://img.shields.io/pypi/dm/retry.svg?maxAge=2592000 +.. 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 +.. 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/retry2.svg?maxAge=2592000