././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1608146453.0250974 tenacity-6.3.1/0000755000076500000240000000000000000000000011622 5ustar00jdstaff././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1608146453.00944 tenacity-6.3.1/.circleci/0000755000076500000240000000000000000000000013455 5ustar00jdstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1573042094.0 tenacity-6.3.1/.circleci/config.yml0000644000076500000240000000365000000000000015451 0ustar00jdstaffversion: 2 jobs: pep8: docker: - image: circleci/python:3.8 steps: - checkout - run: command: | sudo pip install tox tox -e pep8 py27: docker: - image: circleci/python:2.7 steps: - checkout - run: command: | sudo pip install tox tox -e py27 py35: docker: - image: circleci/python:3.5 steps: - checkout - run: command: | sudo pip install tox tox -e py35 py36: docker: - image: circleci/python:3.6 steps: - checkout - run: command: | sudo pip install tox tox -e py36 py37: docker: - image: circleci/python:3.7 steps: - checkout - run: command: | sudo pip install tox tox -e py37 py38: docker: - image: circleci/python:3.8 steps: - checkout - run: command: | sudo pip install tox tox -e py38 deploy: docker: - image: circleci/python:3.8 steps: - checkout - run: | python -m venv venv - run: | venv/bin/pip install twine wheel - run: name: init .pypirc command: | echo -e "[pypi]" >> ~/.pypirc echo -e "username = __token__" >> ~/.pypirc echo -e "password = $PYPI_TOKEN" >> ~/.pypirc - run: name: create packages command: | venv/bin/python setup.py sdist bdist_wheel - run: name: upload to PyPI command: | venv/bin/twine upload dist/* workflows: version: 2 test: jobs: - pep8 - py27 - py35 - py36 - py37 - py38 - deploy: filters: tags: only: /[0-9]+(\.[0-9]+)*/ branches: ignore: /.*/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1534510058.0 tenacity-6.3.1/.gitignore0000644000076500000240000000013400000000000013610 0ustar00jdstaff.idea dist *.pyc *.egg-info build .tox/ AUTHORS ChangeLog .eggs/ doc/_build /.pytest_cache ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1587115572.0 tenacity-6.3.1/.mergify.yml0000644000076500000240000000171500000000000014071 0ustar00jdstaffpull_request_rules: - name: automatic merge conditions: - "status-success=ci/circleci: pep8" - "status-success=ci/circleci: py27" - "status-success=ci/circleci: py35" - "status-success=ci/circleci: py36" - "status-success=ci/circleci: py37" - "status-success=ci/circleci: py38" - "#approved-reviews-by>=1" - label!=work-in-progress actions: merge: strict: "smart" method: squash - name: automatic merge for jd conditions: - author=jd - "status-success=ci/circleci: pep8" - "status-success=ci/circleci: py27" - "status-success=ci/circleci: py35" - "status-success=ci/circleci: py36" - "status-success=ci/circleci: py37" - "status-success=ci/circleci: py38" - label!=work-in-progress actions: merge: strict: "smart" method: squash - name: dismiss reviews conditions: [] actions: dismiss_reviews: {} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1565944978.0 tenacity-6.3.1/.readthedocs.yml0000644000076500000240000000014600000000000014711 0ustar00jdstaffversion: 2 python: install: - method: pip path: . extra_requirements: - doc ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1433173043.0 tenacity-6.3.1/LICENSE0000644000076500000240000002613500000000000012636 0ustar00jdstaff Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1608146453.0252588 tenacity-6.3.1/PKG-INFO0000644000076500000240000000160600000000000012722 0ustar00jdstaffMetadata-Version: 2.1 Name: tenacity Version: 6.3.1 Summary: Retry code until it succeeds Home-page: https://github.com/jd/tenacity Author: Julien Danjou Author-email: julien@danjou.info License: Apache 2.0 Description: Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Utilities Provides-Extra: doc ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1604332774.0 tenacity-6.3.1/README.rst0000644000076500000240000004476100000000000013325 0ustar00jdstaffTenacity ======== .. image:: https://img.shields.io/pypi/v/tenacity.svg :target: https://pypi.python.org/pypi/tenacity .. image:: https://circleci.com/gh/jd/tenacity.svg?style=svg :target: https://circleci.com/gh/jd/tenacity .. image:: https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/jd .. image:: https://img.shields.io/endpoint.svg?url=https://dashboard.mergify.io/badges/jd/tenacity&style=flat :target: https://mergify.io :alt: Mergify Status Tenacity is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything. It originates from `a fork of retrying `_ which is sadly no longer `maintained `_. Tenacity isn't api compatible with retrying but adds significant new functionality and fixes a number of longstanding bugs. The simplest use case is retrying a flaky function whenever an `Exception` occurs until a value is returned. .. testcode:: import random from tenacity import retry @retry def do_something_unreliable(): if random.randint(0, 10) > 1: raise IOError("Broken sauce, everything is hosed!!!111one") else: return "Awesome sauce!" print(do_something_unreliable()) .. testoutput:: :hide: Awesome sauce! .. toctree:: :hidden: :maxdepth: 2 changelog api Features -------- - Generic Decorator API - Specify stop condition (i.e. limit by number of attempts) - Specify wait condition (i.e. exponential backoff sleeping between attempts) - Customize retrying on Exceptions - Customize retrying on expected returned result - Retry on coroutines - Retry code block with context manager Installation ------------ To install *tenacity*, simply: .. code-block:: bash $ pip install tenacity Examples ---------- Basic Retry ~~~~~~~~~~~ .. testsetup:: * import logging # # Note the following import is used for demonstration convenience only. # Production code should always explicitly import the names it needs. # from tenacity import * class MyException(Exception): pass As you saw above, the default behavior is to retry forever without waiting when an exception is raised. .. testcode:: @retry def never_give_up_never_surrender(): print("Retry forever ignoring Exceptions, don't wait between retries") raise Exception Stopping ~~~~~~~~ Let's be a little less persistent and set some boundaries, such as the number of attempts before giving up. .. testcode:: @retry(stop=stop_after_attempt(7)) def stop_after_7_attempts(): print("Stopping after 7 attempts") raise Exception We don't have all day, so let's set a boundary for how long we should be retrying stuff. .. testcode:: @retry(stop=stop_after_delay(10)) def stop_after_10_s(): print("Stopping after 10 seconds") raise Exception You can combine several stop conditions by using the `|` operator: .. testcode:: @retry(stop=(stop_after_delay(10) | stop_after_attempt(5))) def stop_after_10_s_or_5_retries(): print("Stopping after 10 seconds or 5 retries") raise Exception Waiting before retrying ~~~~~~~~~~~~~~~~~~~~~~~ Most things don't like to be polled as fast as possible, so let's just wait 2 seconds between retries. .. testcode:: @retry(wait=wait_fixed(2)) def wait_2_s(): print("Wait 2 second between retries") raise Exception Some things perform best with a bit of randomness injected. .. testcode:: @retry(wait=wait_random(min=1, max=2)) def wait_random_1_to_2_s(): print("Randomly wait 1 to 2 seconds between retries") raise Exception Then again, it's hard to beat exponential backoff when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_exponential(multiplier=1, min=4, max=10)) def wait_exponential_1(): print("Wait 2^x * 1 second between each retry starting with 4 seconds, then up to 10 seconds, then 10 seconds afterwards") raise Exception Then again, it's also hard to beat combining fixed waits and jitter (to help avoid thundering herds) when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_fixed(3) + wait_random(0, 2)) def wait_fixed_jitter(): print("Wait at least 3 seconds, and add up to 2 seconds of random delay") raise Exception When multiple processes are in contention for a shared resource, exponentially increasing jitter helps minimise collisions. .. testcode:: @retry(wait=wait_random_exponential(multiplier=1, max=60)) def wait_exponential_jitter(): print("Randomly wait up to 2^x * 1 seconds between each retry until the range reaches 60 seconds, then randomly up to 60 seconds afterwards") raise Exception Sometimes it's necessary to build a chain of backoffs. .. testcode:: @retry(wait=wait_chain(*[wait_fixed(3) for i in range(3)] + [wait_fixed(7) for i in range(2)] + [wait_fixed(9)])) def wait_fixed_chained(): print("Wait 3s for 3 attempts, 7s for the next 2 attempts and 9s for all attempts thereafter") raise Exception Whether to retry ~~~~~~~~~~~~~~~~ We have a few options for dealing with retries that raise specific or general exceptions, as in the cases here. .. testcode:: @retry(retry=retry_if_exception_type(IOError)) def might_io_error(): print("Retry forever with no wait if an IOError occurs, raise any other errors") raise Exception We can also use the result of the function to alter the behavior of retrying. .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=retry_if_result(is_none_p)) def might_return_none(): print("Retry with no wait if return value is None") We can also combine several conditions: .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=(retry_if_result(is_none_p) | retry_if_exception_type())) def might_return_none(): print("Retry forever ignoring Exceptions with no wait if return value is None") Any combination of stop, wait, etc. is also supported to give you the freedom to mix and match. It's also possible to retry explicitly at any time by raising the `TryAgain` exception: .. testcode:: @retry def do_something(): result = something_else() if result == 23: raise TryAgain Error Handling ~~~~~~~~~~~~~~ While callables that "timeout" retrying raise a `RetryError` by default, we can reraise the last attempt's exception if needed: .. testcode:: @retry(reraise=True, stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except MyException: # timed out retrying pass Before and After Retry, and Logging ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It's possible to execute an action before any attempt of calling the function by using the before callback function: .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), before=before_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") In the same spirit, It's possible to execute after a call that failed: .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), after=after_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") It's also possible to only log failures that are going to be retried. Normally retries happen after a wait interval, so the keyword argument is called ``before_sleep``: .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), before_sleep=before_sleep_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") Statistics ~~~~~~~~~~ You can access the statistics about the retry made over a function by using the `retry` attribute attached to the function and its `statistics` attribute: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... Custom Callbacks ~~~~~~~~~~~~~~~~ You can also define your own callbacks. The callback should accept one parameter called ``retry_state`` that contains all information about current retry invocation. For example, you can call a custom callback function after all retries failed, without raising an exception (or you can re-raise or do anything really) .. testcode:: def return_last_value(retry_state): """return the result of the last call attempt""" return retry_state.outcome.result() def is_false(value): """Return True if value is False""" return value is False # will return False after trying 3 times to get a different result @retry(stop=stop_after_attempt(3), retry_error_callback=return_last_value, retry=retry_if_result(is_false)) def eventually_return_false(): return False .. note:: Calling the parameter ``retry_state`` is important, because this is how *tenacity* internally distinguishes callbacks from their :ref:`deprecated counterparts `. RetryCallState ~~~~~~~~~~~~~~ ``retry_state`` argument is an object of `RetryCallState` class: .. autoclass:: tenacity.RetryCallState Constant attributes: .. autoinstanceattribute:: start_time(float) :annotation: .. autoinstanceattribute:: retry_object(BaseRetrying) :annotation: .. autoinstanceattribute:: fn(callable) :annotation: .. autoinstanceattribute:: args(tuple) :annotation: .. autoinstanceattribute:: kwargs(dict) :annotation: Variable attributes: .. autoinstanceattribute:: attempt_number(int) :annotation: .. autoinstanceattribute:: outcome(tenacity.Future or None) :annotation: .. autoinstanceattribute:: outcome_timestamp(float or None) :annotation: .. autoinstanceattribute:: idle_for(float) :annotation: .. autoinstanceattribute:: next_action(tenacity.RetryAction or None) :annotation: Other Custom Callbacks ~~~~~~~~~~~~~~~~~~~~~~ It's also possible to define custom callbacks for other keyword arguments. .. function:: my_stop(retry_state) :param RetryState retry_state: info about current retry invocation :return: whether or not retrying should stop :rtype: bool .. function:: my_wait(retry_state) :param RetryState retry_state: info about current retry invocation :return: number of seconds to wait before next retry :rtype: float .. function:: my_retry(retry_state) :param RetryState retry_state: info about current retry invocation :return: whether or not retrying should continue :rtype: bool .. function:: my_before(retry_state) :param RetryState retry_state: info about current retry invocation .. function:: my_after(retry_state) :param RetryState retry_state: info about current retry invocation .. function:: my_before_sleep(retry_state) :param RetryState retry_state: info about current retry invocation Here's an example with a custom ``before_sleep`` function: .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) def my_before_sleep(retry_state): if retry_state.attempt_number < 1: loglevel = logging.INFO else: loglevel = logging.WARNING logger.log( loglevel, 'Retrying %s: attempt %s ended with: %s', retry_state.fn, retry_state.attempt_number, retry_state.outcome) @retry(stop=stop_after_attempt(3), before_sleep=my_before_sleep) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except RetryError: pass .. _deprecated-callbacks: .. note:: It was also possible to define custom callbacks before, but they accepted varying parameter sets and none of those provided full state. The old way is deprecated, but kept for backward compatibility. .. function:: my_deprecated_stop(previous_attempt_number, delay_since_first_attempt) *deprecated* :param previous_attempt_number: the number of current attempt :type previous_attempt_number: int :param delay_since_first_attempt: interval in seconds between the beginning of first attempt and current time :type delay_since_first_attempt: float :rtype: bool .. function:: my_deprecated_wait(previous_attempt_number, delay_since_first_attempt [, last_result]) *deprecated* :param previous_attempt_number: the number of current attempt :type previous_attempt_number: int :param delay_since_first_attempt: interval in seconds between the beginning of first attempt and current time :type delay_since_first_attempt: float :param tenacity.Future last_result: current outcome :return: number of seconds to wait before next retry :rtype: float .. function:: my_deprecated_retry(attempt) *deprecated* :param tenacity.Future attempt: current outcome :return: whether or not retrying should continue :rtype: bool .. function:: my_deprecated_before(func, trial_number) *deprecated* :param callable func: function whose outcome is to be retried :param int trial_number: the number of current attempt .. function:: my_deprecated_after(func, trial_number, trial_time_taken) *deprecated* :param callable func: function whose outcome is to be retried :param int trial_number: the number of current attempt :param float trial_time_taken: interval in seconds between the beginning of first attempt and current time .. function:: my_deprecated_before_sleep(func, sleep, last_result) *deprecated* :param callable func: function whose outcome is to be retried :param float sleep: number of seconds to wait until next retry :param tenacity.Future last_result: current outcome .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) def my_before_sleep(retry_object, sleep, last_result): logger.warning( 'Retrying %s: last_result=%s, retrying in %s seconds...', retry_object.fn, last_result, sleep) @retry(stop=stop_after_attempt(3), before_sleep=my_before_sleep) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except RetryError: pass Changing Arguments at Run Time ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can change the arguments of a retry decorator as needed when calling it by using the `retry_with` function attached to the wrapped function: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception.retry_with(stop=stop_after_attempt(4))() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... If you want to use variables to set up the retry parameters, you don't have to use the `retry` decorator - you can instead use `Retrying` directly: .. testcode:: def never_good_enough(arg1): raise Exception('Invalid argument: {}'.format(arg1)) def try_never_good_enough(max_attempts=3): retryer = Retrying(stop=stop_after_attempt(max_attempts), reraise=True) retryer(never_good_enough, 'I really do try') Retrying code block ~~~~~~~~~~~~~~~~~~~ Tenacity allows you to retry a code block without the need to wraps it in an isolated function. This makes it easy to isolate failing block while sharing context. The trick is to combine a for loop and a context manager. .. testcode:: from tenacity import Retrying, RetryError, stop_after_attempt try: for attempt in Retrying(stop=stop_after_attempt(3)): with attempt: raise Exception('My code is failing!') except RetryError: pass You can configure every details of retry policy by configuring the Retrying object. With async code you can use AsyncRetrying. .. testcode:: from tenacity import AsyncRetrying, RetryError, stop_after_attempt async def function(): try: async for attempt in AsyncRetrying(stop=stop_after_attempt(3)): with attempt: raise Exception('My code is failing!') except RetryError: pass Async and retry ~~~~~~~~~~~~~~~ Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_async_function(loop): await loop.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry @tornado.gen.coroutine def my_async_function(http_client, url): yield http_client.fetch(url) You can even use alternative event loops such as `curio` or `Trio` by passing the correct sleep function: .. code-block:: python @retry(sleep=trio.sleep) async def my_async_function(loop): await asks.get('https://example.org') Contribute ---------- #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. #. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it). #. Write a test which shows that the bug was fixed or that the feature works as expected. #. Add a `changelog <#Changelogs>`_ #. Make the docs better (or more detailed, or more easier to read, or ...) .. _`the repository`: https://github.com/jd/tenacity Changelogs ~~~~~~~~~~ `reno`_ is used for managing changelogs. Take a look at their usage docs. The doc generation will automatically compile the changelogs. You just need to add them. .. code-block:: sh # Opens a template file in an editor tox -e reno -- new some-slug-for-my-change --edit .. _`reno`: https://docs.openstack.org/reno/latest/user/usage.html ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1608146453.0007794 tenacity-6.3.1/doc/0000755000076500000240000000000000000000000012367 5ustar00jdstaff././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1608146453.0120254 tenacity-6.3.1/doc/source/0000755000076500000240000000000000000000000013667 5ustar00jdstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576230037.0 tenacity-6.3.1/doc/source/api.rst0000644000076500000240000000272200000000000015175 0ustar00jdstaff=============== API Reference =============== Retry Main API -------------- .. autofunction:: tenacity.retry :noindex: .. autoclass:: tenacity.Retrying :members: .. autoclass:: tenacity.AsyncRetrying :members: .. autoclass:: tenacity.tornadoweb.TornadoRetrying :members: After Functions --------------- Those functions can be used as the `after` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.after :members: Before Functions ---------------- Those functions can be used as the `before` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.before :members: Before Sleep Functions ---------------------- Those functions can be used as the `before_sleep` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.before_sleep :members: Nap Functions ------------- Those functions can be used as the `sleep` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.nap :members: Retry Functions --------------- Those functions can be used as the `retry` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.retry :members: Stop Functions -------------- Those functions can be used as the `stop` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.stop :members: Wait Functions -------------- Those functions can be used as the `wait` keyword argument of :py:func:`tenacity.retry`. .. automodule:: tenacity.wait :members: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1565944916.0 tenacity-6.3.1/doc/source/changelog.rst0000644000076500000240000000005000000000000016343 0ustar00jdstaffChangelog ========= .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1576230037.0 tenacity-6.3.1/doc/source/conf.py0000644000076500000240000000242200000000000015166 0ustar00jdstaff# -*- coding: utf-8 -*- # Copyright 2016 Étienne Bersac # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys master_doc = 'index' project = "Tenacity" # Add tenacity to the path, so sphinx can find the functions for autodoc. sys.path.insert(0, os.path.abspath('../..')) extensions = [ 'sphinx.ext.doctest', 'sphinx.ext.autodoc', 'reno.sphinxext', ] # -- Options for sphinx.ext.doctest ----------------------------------------- # doctest_default_flags = cwd = os.path.abspath(os.path.dirname(__file__)) tenacity_path = os.path.join(cwd, os.pardir, os.pardir) doctest_path = [tenacity_path] # doctest_global_setup = # doctest_global_cleanup = # doctest_test_doctest_blocks = ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1604332774.0 tenacity-6.3.1/doc/source/index.rst0000644000076500000240000004476100000000000015544 0ustar00jdstaffTenacity ======== .. image:: https://img.shields.io/pypi/v/tenacity.svg :target: https://pypi.python.org/pypi/tenacity .. image:: https://circleci.com/gh/jd/tenacity.svg?style=svg :target: https://circleci.com/gh/jd/tenacity .. image:: https://img.shields.io/badge/SayThanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/jd .. image:: https://img.shields.io/endpoint.svg?url=https://dashboard.mergify.io/badges/jd/tenacity&style=flat :target: https://mergify.io :alt: Mergify Status Tenacity is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything. It originates from `a fork of retrying `_ which is sadly no longer `maintained `_. Tenacity isn't api compatible with retrying but adds significant new functionality and fixes a number of longstanding bugs. The simplest use case is retrying a flaky function whenever an `Exception` occurs until a value is returned. .. testcode:: import random from tenacity import retry @retry def do_something_unreliable(): if random.randint(0, 10) > 1: raise IOError("Broken sauce, everything is hosed!!!111one") else: return "Awesome sauce!" print(do_something_unreliable()) .. testoutput:: :hide: Awesome sauce! .. toctree:: :hidden: :maxdepth: 2 changelog api Features -------- - Generic Decorator API - Specify stop condition (i.e. limit by number of attempts) - Specify wait condition (i.e. exponential backoff sleeping between attempts) - Customize retrying on Exceptions - Customize retrying on expected returned result - Retry on coroutines - Retry code block with context manager Installation ------------ To install *tenacity*, simply: .. code-block:: bash $ pip install tenacity Examples ---------- Basic Retry ~~~~~~~~~~~ .. testsetup:: * import logging # # Note the following import is used for demonstration convenience only. # Production code should always explicitly import the names it needs. # from tenacity import * class MyException(Exception): pass As you saw above, the default behavior is to retry forever without waiting when an exception is raised. .. testcode:: @retry def never_give_up_never_surrender(): print("Retry forever ignoring Exceptions, don't wait between retries") raise Exception Stopping ~~~~~~~~ Let's be a little less persistent and set some boundaries, such as the number of attempts before giving up. .. testcode:: @retry(stop=stop_after_attempt(7)) def stop_after_7_attempts(): print("Stopping after 7 attempts") raise Exception We don't have all day, so let's set a boundary for how long we should be retrying stuff. .. testcode:: @retry(stop=stop_after_delay(10)) def stop_after_10_s(): print("Stopping after 10 seconds") raise Exception You can combine several stop conditions by using the `|` operator: .. testcode:: @retry(stop=(stop_after_delay(10) | stop_after_attempt(5))) def stop_after_10_s_or_5_retries(): print("Stopping after 10 seconds or 5 retries") raise Exception Waiting before retrying ~~~~~~~~~~~~~~~~~~~~~~~ Most things don't like to be polled as fast as possible, so let's just wait 2 seconds between retries. .. testcode:: @retry(wait=wait_fixed(2)) def wait_2_s(): print("Wait 2 second between retries") raise Exception Some things perform best with a bit of randomness injected. .. testcode:: @retry(wait=wait_random(min=1, max=2)) def wait_random_1_to_2_s(): print("Randomly wait 1 to 2 seconds between retries") raise Exception Then again, it's hard to beat exponential backoff when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_exponential(multiplier=1, min=4, max=10)) def wait_exponential_1(): print("Wait 2^x * 1 second between each retry starting with 4 seconds, then up to 10 seconds, then 10 seconds afterwards") raise Exception Then again, it's also hard to beat combining fixed waits and jitter (to help avoid thundering herds) when retrying distributed services and other remote endpoints. .. testcode:: @retry(wait=wait_fixed(3) + wait_random(0, 2)) def wait_fixed_jitter(): print("Wait at least 3 seconds, and add up to 2 seconds of random delay") raise Exception When multiple processes are in contention for a shared resource, exponentially increasing jitter helps minimise collisions. .. testcode:: @retry(wait=wait_random_exponential(multiplier=1, max=60)) def wait_exponential_jitter(): print("Randomly wait up to 2^x * 1 seconds between each retry until the range reaches 60 seconds, then randomly up to 60 seconds afterwards") raise Exception Sometimes it's necessary to build a chain of backoffs. .. testcode:: @retry(wait=wait_chain(*[wait_fixed(3) for i in range(3)] + [wait_fixed(7) for i in range(2)] + [wait_fixed(9)])) def wait_fixed_chained(): print("Wait 3s for 3 attempts, 7s for the next 2 attempts and 9s for all attempts thereafter") raise Exception Whether to retry ~~~~~~~~~~~~~~~~ We have a few options for dealing with retries that raise specific or general exceptions, as in the cases here. .. testcode:: @retry(retry=retry_if_exception_type(IOError)) def might_io_error(): print("Retry forever with no wait if an IOError occurs, raise any other errors") raise Exception We can also use the result of the function to alter the behavior of retrying. .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=retry_if_result(is_none_p)) def might_return_none(): print("Retry with no wait if return value is None") We can also combine several conditions: .. testcode:: def is_none_p(value): """Return True if value is None""" return value is None @retry(retry=(retry_if_result(is_none_p) | retry_if_exception_type())) def might_return_none(): print("Retry forever ignoring Exceptions with no wait if return value is None") Any combination of stop, wait, etc. is also supported to give you the freedom to mix and match. It's also possible to retry explicitly at any time by raising the `TryAgain` exception: .. testcode:: @retry def do_something(): result = something_else() if result == 23: raise TryAgain Error Handling ~~~~~~~~~~~~~~ While callables that "timeout" retrying raise a `RetryError` by default, we can reraise the last attempt's exception if needed: .. testcode:: @retry(reraise=True, stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except MyException: # timed out retrying pass Before and After Retry, and Logging ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It's possible to execute an action before any attempt of calling the function by using the before callback function: .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), before=before_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") In the same spirit, It's possible to execute after a call that failed: .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), after=after_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") It's also possible to only log failures that are going to be retried. Normally retries happen after a wait interval, so the keyword argument is called ``before_sleep``: .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) @retry(stop=stop_after_attempt(3), before_sleep=before_sleep_log(logger, logging.DEBUG)) def raise_my_exception(): raise MyException("Fail") Statistics ~~~~~~~~~~ You can access the statistics about the retry made over a function by using the `retry` attribute attached to the function and its `statistics` attribute: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... Custom Callbacks ~~~~~~~~~~~~~~~~ You can also define your own callbacks. The callback should accept one parameter called ``retry_state`` that contains all information about current retry invocation. For example, you can call a custom callback function after all retries failed, without raising an exception (or you can re-raise or do anything really) .. testcode:: def return_last_value(retry_state): """return the result of the last call attempt""" return retry_state.outcome.result() def is_false(value): """Return True if value is False""" return value is False # will return False after trying 3 times to get a different result @retry(stop=stop_after_attempt(3), retry_error_callback=return_last_value, retry=retry_if_result(is_false)) def eventually_return_false(): return False .. note:: Calling the parameter ``retry_state`` is important, because this is how *tenacity* internally distinguishes callbacks from their :ref:`deprecated counterparts `. RetryCallState ~~~~~~~~~~~~~~ ``retry_state`` argument is an object of `RetryCallState` class: .. autoclass:: tenacity.RetryCallState Constant attributes: .. autoinstanceattribute:: start_time(float) :annotation: .. autoinstanceattribute:: retry_object(BaseRetrying) :annotation: .. autoinstanceattribute:: fn(callable) :annotation: .. autoinstanceattribute:: args(tuple) :annotation: .. autoinstanceattribute:: kwargs(dict) :annotation: Variable attributes: .. autoinstanceattribute:: attempt_number(int) :annotation: .. autoinstanceattribute:: outcome(tenacity.Future or None) :annotation: .. autoinstanceattribute:: outcome_timestamp(float or None) :annotation: .. autoinstanceattribute:: idle_for(float) :annotation: .. autoinstanceattribute:: next_action(tenacity.RetryAction or None) :annotation: Other Custom Callbacks ~~~~~~~~~~~~~~~~~~~~~~ It's also possible to define custom callbacks for other keyword arguments. .. function:: my_stop(retry_state) :param RetryState retry_state: info about current retry invocation :return: whether or not retrying should stop :rtype: bool .. function:: my_wait(retry_state) :param RetryState retry_state: info about current retry invocation :return: number of seconds to wait before next retry :rtype: float .. function:: my_retry(retry_state) :param RetryState retry_state: info about current retry invocation :return: whether or not retrying should continue :rtype: bool .. function:: my_before(retry_state) :param RetryState retry_state: info about current retry invocation .. function:: my_after(retry_state) :param RetryState retry_state: info about current retry invocation .. function:: my_before_sleep(retry_state) :param RetryState retry_state: info about current retry invocation Here's an example with a custom ``before_sleep`` function: .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) def my_before_sleep(retry_state): if retry_state.attempt_number < 1: loglevel = logging.INFO else: loglevel = logging.WARNING logger.log( loglevel, 'Retrying %s: attempt %s ended with: %s', retry_state.fn, retry_state.attempt_number, retry_state.outcome) @retry(stop=stop_after_attempt(3), before_sleep=my_before_sleep) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except RetryError: pass .. _deprecated-callbacks: .. note:: It was also possible to define custom callbacks before, but they accepted varying parameter sets and none of those provided full state. The old way is deprecated, but kept for backward compatibility. .. function:: my_deprecated_stop(previous_attempt_number, delay_since_first_attempt) *deprecated* :param previous_attempt_number: the number of current attempt :type previous_attempt_number: int :param delay_since_first_attempt: interval in seconds between the beginning of first attempt and current time :type delay_since_first_attempt: float :rtype: bool .. function:: my_deprecated_wait(previous_attempt_number, delay_since_first_attempt [, last_result]) *deprecated* :param previous_attempt_number: the number of current attempt :type previous_attempt_number: int :param delay_since_first_attempt: interval in seconds between the beginning of first attempt and current time :type delay_since_first_attempt: float :param tenacity.Future last_result: current outcome :return: number of seconds to wait before next retry :rtype: float .. function:: my_deprecated_retry(attempt) *deprecated* :param tenacity.Future attempt: current outcome :return: whether or not retrying should continue :rtype: bool .. function:: my_deprecated_before(func, trial_number) *deprecated* :param callable func: function whose outcome is to be retried :param int trial_number: the number of current attempt .. function:: my_deprecated_after(func, trial_number, trial_time_taken) *deprecated* :param callable func: function whose outcome is to be retried :param int trial_number: the number of current attempt :param float trial_time_taken: interval in seconds between the beginning of first attempt and current time .. function:: my_deprecated_before_sleep(func, sleep, last_result) *deprecated* :param callable func: function whose outcome is to be retried :param float sleep: number of seconds to wait until next retry :param tenacity.Future last_result: current outcome .. testcode:: import logging logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) logger = logging.getLogger(__name__) def my_before_sleep(retry_object, sleep, last_result): logger.warning( 'Retrying %s: last_result=%s, retrying in %s seconds...', retry_object.fn, last_result, sleep) @retry(stop=stop_after_attempt(3), before_sleep=my_before_sleep) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception() except RetryError: pass Changing Arguments at Run Time ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can change the arguments of a retry decorator as needed when calling it by using the `retry_with` function attached to the wrapped function: .. testcode:: @retry(stop=stop_after_attempt(3)) def raise_my_exception(): raise MyException("Fail") try: raise_my_exception.retry_with(stop=stop_after_attempt(4))() except Exception: pass print(raise_my_exception.retry.statistics) .. testoutput:: :hide: ... If you want to use variables to set up the retry parameters, you don't have to use the `retry` decorator - you can instead use `Retrying` directly: .. testcode:: def never_good_enough(arg1): raise Exception('Invalid argument: {}'.format(arg1)) def try_never_good_enough(max_attempts=3): retryer = Retrying(stop=stop_after_attempt(max_attempts), reraise=True) retryer(never_good_enough, 'I really do try') Retrying code block ~~~~~~~~~~~~~~~~~~~ Tenacity allows you to retry a code block without the need to wraps it in an isolated function. This makes it easy to isolate failing block while sharing context. The trick is to combine a for loop and a context manager. .. testcode:: from tenacity import Retrying, RetryError, stop_after_attempt try: for attempt in Retrying(stop=stop_after_attempt(3)): with attempt: raise Exception('My code is failing!') except RetryError: pass You can configure every details of retry policy by configuring the Retrying object. With async code you can use AsyncRetrying. .. testcode:: from tenacity import AsyncRetrying, RetryError, stop_after_attempt async def function(): try: async for attempt in AsyncRetrying(stop=stop_after_attempt(3)): with attempt: raise Exception('My code is failing!') except RetryError: pass Async and retry ~~~~~~~~~~~~~~~ Finally, ``retry`` works also on asyncio and Tornado (>= 4.5) coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_async_function(loop): await loop.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry @tornado.gen.coroutine def my_async_function(http_client, url): yield http_client.fetch(url) You can even use alternative event loops such as `curio` or `Trio` by passing the correct sleep function: .. code-block:: python @retry(sleep=trio.sleep) async def my_async_function(loop): await asks.get('https://example.org') Contribute ---------- #. Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. #. Fork `the repository`_ on GitHub to start making your changes to the **master** branch (or branch off of it). #. Write a test which shows that the bug was fixed or that the feature works as expected. #. Add a `changelog <#Changelogs>`_ #. Make the docs better (or more detailed, or more easier to read, or ...) .. _`the repository`: https://github.com/jd/tenacity Changelogs ~~~~~~~~~~ `reno`_ is used for managing changelogs. Take a look at their usage docs. The doc generation will automatically compile the changelogs. You just need to add them. .. code-block:: sh # Opens a template file in an editor tox -e reno -- new some-slug-for-my-change --edit .. _`reno`: https://docs.openstack.org/reno/latest/user/usage.html ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1608146453.001274 tenacity-6.3.1/releasenotes/0000755000076500000240000000000000000000000014313 5ustar00jdstaff././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1608146453.013199 tenacity-6.3.1/releasenotes/notes/0000755000076500000240000000000000000000000015443 5ustar00jdstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1565944916.0 tenacity-6.3.1/releasenotes/notes/add-reno-d1ab5710f272650a.yaml0000644000076500000240000000005600000000000022140 0ustar00jdstaff--- features: - Add reno (changelog system) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1604332774.0 tenacity-6.3.1/releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml0000644000076500000240000000013700000000000025363 0ustar00jdstaff--- other: - Unit tests can now mock ``nap.sleep()`` for testing in all tenacity usage styles././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588240811.0 tenacity-6.3.1/releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml0000644000076500000240000000012400000000000026575 0ustar00jdstaff--- features: - Add an ``exc_info`` option to the ``before_sleep_log()`` strategy.././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1565944916.0 tenacity-6.3.1/reno.yaml0000644000076500000240000000005100000000000013445 0ustar00jdstaff--- unreleased_version_title: Unreleased ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1608146453.0258808 tenacity-6.3.1/setup.cfg0000644000076500000240000000223600000000000013446 0ustar00jdstaff[metadata] name = tenacity license = Apache 2.0 url = https://github.com/jd/tenacity summary = Retry code until it succeeds long_description = Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. author = Julien Danjou author-email = julien@danjou.info home-page = https://github.com/jd/tenacity classifier = Intended Audience :: Developers License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Topic :: Utilities [options] install_requires = six>=1.9.0 futures>=3.0;python_version=='2.7' monotonic>=0.6;python_version=='2.7' typing>=3.7.4.1;python_version=='2.7' packages = tenacity [options.package_data] tenacity = py.typed [options.extras_require] doc = reno sphinx tornado>=4.5 [wheel] universal = 1 [tool:pytest] filterwarnings = once::DeprecationWarning [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1565944978.0 tenacity-6.3.1/setup.py0000644000076500000240000000124200000000000013333 0ustar00jdstaff#!/usr/bin/env python # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import setuptools setuptools.setup( setup_requires=['setuptools_scm'], use_scm_version=True, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1608146453.0202954 tenacity-6.3.1/tenacity/0000755000076500000240000000000000000000000013442 5ustar00jdstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608146428.0 tenacity-6.3.1/tenacity/__init__.py0000644000076500000240000004050500000000000015557 0ustar00jdstaff# -*- coding: utf-8 -*- # Copyright 2016-2018 Julien Danjou # Copyright 2017 Elisey Zanko # Copyright 2016 Étienne Bersac # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. try: from inspect import iscoroutinefunction except ImportError: iscoroutinefunction = None try: import tornado except ImportError: tornado = None import sys import threading import typing as t import warnings from abc import ABCMeta, abstractmethod from concurrent import futures import six from tenacity import _utils from tenacity import compat as _compat # Import all built-in retry strategies for easier usage. from .retry import retry_all # noqa from .retry import retry_always # noqa from .retry import retry_any # noqa from .retry import retry_if_exception # noqa from .retry import retry_if_exception_type # noqa from .retry import retry_if_not_result # noqa from .retry import retry_if_result # noqa from .retry import retry_never # noqa from .retry import retry_unless_exception_type # noqa from .retry import retry_if_exception_message # noqa from .retry import retry_if_not_exception_message # noqa # Import all nap strategies for easier usage. from .nap import sleep # noqa from .nap import sleep_using_event # noqa # Import all built-in stop strategies for easier usage. from .stop import stop_after_attempt # noqa from .stop import stop_after_delay # noqa from .stop import stop_all # noqa from .stop import stop_any # noqa from .stop import stop_never # noqa from .stop import stop_when_event_set # noqa # Import all built-in wait strategies for easier usage. from .wait import wait_chain # noqa from .wait import wait_combine # noqa from .wait import wait_exponential # noqa from .wait import wait_fixed # noqa from .wait import wait_incrementing # noqa from .wait import wait_none # noqa from .wait import wait_random # noqa from .wait import wait_random_exponential # noqa from .wait import wait_random_exponential as wait_full_jitter # noqa # Import all built-in before strategies for easier usage. from .before import before_log # noqa from .before import before_nothing # noqa # Import all built-in after strategies for easier usage. from .after import after_log # noqa from .after import after_nothing # noqa # Import all built-in after strategies for easier usage. from .before_sleep import before_sleep_log # noqa from .before_sleep import before_sleep_nothing # noqa WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable) @t.overload def retry(fn): # type: (WrappedFn) -> WrappedFn """Type signature for @retry as a raw decorator.""" pass @t.overload def retry(*dargs, **dkw): # noqa # type: (...) -> t.Callable[[WrappedFn], WrappedFn] """Type signature for the @retry() decorator constructor.""" pass def retry(*dargs, **dkw): # noqa """Wrap a function with a new `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 if len(dargs) == 1 and callable(dargs[0]): return retry()(dargs[0]) else: def wrap(f): if iscoroutinefunction is not None and iscoroutinefunction(f): r = AsyncRetrying(*dargs, **dkw) elif tornado and hasattr(tornado.gen, 'is_coroutine_function') \ and tornado.gen.is_coroutine_function(f): r = TornadoRetrying(*dargs, **dkw) else: r = Retrying(*dargs, **dkw) return r.wraps(f) return wrap class TryAgain(Exception): """Always retry the executed function when raised.""" NO_RESULT = object() class DoAttempt(object): pass class DoSleep(float): pass class BaseAction(object): """Base class for representing actions to take by retry object. Concrete implementations must define: - __init__: to initialize all necessary fields - REPR_ATTRS: class variable specifying attributes to include in repr(self) - NAME: for identification in retry object methods and callbacks """ REPR_FIELDS = () NAME = None def __repr__(self): state_str = ', '.join('%s=%r' % (field, getattr(self, field)) for field in self.REPR_FIELDS) return '%s(%s)' % (type(self).__name__, state_str) def __str__(self): return repr(self) class RetryAction(BaseAction): REPR_FIELDS = ('sleep',) NAME = 'retry' def __init__(self, sleep): self.sleep = float(sleep) _unset = object() class RetryError(Exception): """Encapsulates the last attempt instance right before giving up.""" def __init__(self, last_attempt): self.last_attempt = last_attempt super(RetryError, self).__init__(last_attempt) def reraise(self): if self.last_attempt.failed: raise self.last_attempt.result() raise self def __str__(self): return "{0}[{1}]".format(self.__class__.__name__, self.last_attempt) class AttemptManager(object): """Manage attempt context.""" def __init__(self, retry_state): self.retry_state = retry_state def __enter__(self): pass def __exit__(self, exc_type, exc_value, traceback): if isinstance(exc_value, BaseException): self.retry_state.set_exception((exc_type, exc_value, traceback)) return True # Swallow exception. else: # We don't have the result, actually. self.retry_state.set_result(None) class BaseRetrying(object): __metaclass__ = ABCMeta def __init__(self, sleep=sleep, stop=stop_never, wait=wait_none(), retry=retry_if_exception_type(), before=before_nothing, after=after_nothing, before_sleep=None, reraise=False, retry_error_cls=RetryError, retry_error_callback=None): self.sleep = sleep self._stop = stop self._wait = wait self._retry = retry self._before = before self._after = after self._before_sleep = before_sleep self.reraise = reraise self._local = threading.local() self.retry_error_cls = retry_error_cls self._retry_error_callback = retry_error_callback # This attribute was moved to RetryCallState and is deprecated on # Retrying objects but kept for backward compatibility. self.fn = None @_utils.cached_property def stop(self): return _compat.stop_func_accept_retry_state(self._stop) @_utils.cached_property def wait(self): return _compat.wait_func_accept_retry_state(self._wait) @_utils.cached_property def retry(self): return _compat.retry_func_accept_retry_state(self._retry) @_utils.cached_property def before(self): return _compat.before_func_accept_retry_state(self._before) @_utils.cached_property def after(self): return _compat.after_func_accept_retry_state(self._after) @_utils.cached_property def before_sleep(self): return _compat.before_sleep_func_accept_retry_state(self._before_sleep) @_utils.cached_property def retry_error_callback(self): return _compat.retry_error_callback_accept_retry_state( self._retry_error_callback) def copy(self, sleep=_unset, stop=_unset, wait=_unset, retry=_unset, before=_unset, after=_unset, before_sleep=_unset, reraise=_unset): """Copy this object with some parameters changed if needed.""" if before_sleep is _unset: before_sleep = self.before_sleep return self.__class__( sleep=self.sleep if sleep is _unset else sleep, stop=self.stop if stop is _unset else stop, wait=self.wait if wait is _unset else wait, retry=self.retry if retry is _unset else retry, before=self.before if before is _unset else before, after=self.after if after is _unset else after, before_sleep=before_sleep, reraise=self.reraise if after is _unset else reraise, ) def __repr__(self): attrs = dict( _utils.visible_attrs(self, attrs={'me': id(self)}), __class__=self.__class__.__name__, ) return ("<%(__class__)s object at 0x%(me)x (stop=%(stop)s, " "wait=%(wait)s, sleep=%(sleep)s, retry=%(retry)s, " "before=%(before)s, after=%(after)s)>") % (attrs) @property def statistics(self): """Return a dictionary of runtime statistics. This dictionary will be empty when the controller has never been ran. When it is running or has ran previously it should have (but may not) have useful and/or informational keys and values when running is underway and/or completed. .. warning:: The keys in this dictionary **should** be some what stable (not changing), but there existence **may** change between major releases as new statistics are gathered or removed so before accessing keys ensure that they actually exist and handle when they do not. .. note:: The values in this dictionary are local to the thread running call (so if multiple threads share the same retrying object - either directly or indirectly) they will each have there own view of statistics they have collected (in the future we may provide a way to aggregate the various statistics from each thread). """ try: return self._local.statistics except AttributeError: self._local.statistics = {} return self._local.statistics def wraps(self, f): """Wrap a function for retrying. :param f: A function to wraps for retrying. """ @_utils.wraps(f) def wrapped_f(*args, **kw): return self(f, *args, **kw) def retry_with(*args, **kwargs): return self.copy(*args, **kwargs).wraps(f) wrapped_f.retry = self wrapped_f.retry_with = retry_with return wrapped_f def begin(self, fn): self.statistics.clear() self.statistics['start_time'] = _utils.now() self.statistics['attempt_number'] = 1 self.statistics['idle_for'] = 0 self.fn = fn def iter(self, retry_state): # noqa fut = retry_state.outcome if fut is None: if self.before is not None: self.before(retry_state) return DoAttempt() is_explicit_retry = retry_state.outcome.failed \ and isinstance(retry_state.outcome.exception(), TryAgain) if not (is_explicit_retry or self.retry(retry_state=retry_state)): return fut.result() if self.after is not None: self.after(retry_state=retry_state) self.statistics['delay_since_first_attempt'] = \ retry_state.seconds_since_start if self.stop(retry_state=retry_state): if self.retry_error_callback: return self.retry_error_callback(retry_state=retry_state) retry_exc = self.retry_error_cls(fut) if self.reraise: raise retry_exc.reraise() six.raise_from(retry_exc, fut.exception()) if self.wait: sleep = self.wait(retry_state=retry_state) else: sleep = 0.0 retry_state.next_action = RetryAction(sleep) retry_state.idle_for += sleep self.statistics['idle_for'] += sleep self.statistics['attempt_number'] += 1 if self.before_sleep is not None: self.before_sleep(retry_state=retry_state) return DoSleep(sleep) def __iter__(self): self.begin(None) retry_state = RetryCallState(self, fn=None, args=(), kwargs={}) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): yield AttemptManager(retry_state=retry_state) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() self.sleep(do) else: break @abstractmethod def __call__(self, *args, **kwargs): pass def call(self, *args, **kwargs): """Use ``__call__`` instead because this method is deprecated.""" warnings.warn("'call()' method is deprecated. " + "Use '__call__()' instead", DeprecationWarning) return self.__call__(*args, **kwargs) class Retrying(BaseRetrying): """Retrying controller.""" def __call__(self, fn, *args, **kwargs): self.begin(fn) retry_state = RetryCallState( retry_object=self, fn=fn, args=args, kwargs=kwargs) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): try: result = fn(*args, **kwargs) except BaseException: retry_state.set_exception(sys.exc_info()) else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() self.sleep(do) else: return do class Future(futures.Future): """Encapsulates a (future or past) attempted call to a target function.""" def __init__(self, attempt_number): super(Future, self).__init__() self.attempt_number = attempt_number @property def failed(self): """Return whether a exception is being held in this future.""" return self.exception() is not None @classmethod def construct(cls, attempt_number, value, has_exception): """Construct a new Future object.""" fut = cls(attempt_number) if has_exception: fut.set_exception(value) else: fut.set_result(value) return fut class RetryCallState(object): """State related to a single call wrapped with Retrying.""" def __init__(self, retry_object, fn, args, kwargs): #: Retry call start timestamp self.start_time = _utils.now() #: Retry manager object self.retry_object = retry_object #: Function wrapped by this retry call self.fn = fn #: Arguments of the function wrapped by this retry call self.args = args #: Keyword arguments of the function wrapped by this retry call self.kwargs = kwargs #: The number of the current attempt self.attempt_number = 1 #: Last outcome (result or exception) produced by the function self.outcome = None #: Timestamp of the last outcome self.outcome_timestamp = None #: Time spent sleeping in retries self.idle_for = 0 #: Next action as decided by the retry manager self.next_action = None @property def seconds_since_start(self): if self.outcome_timestamp is None: return None return self.outcome_timestamp - self.start_time def prepare_for_next_attempt(self): self.outcome = None self.outcome_timestamp = None self.attempt_number += 1 self.next_action = None def set_result(self, val): ts = _utils.now() fut = Future(self.attempt_number) fut.set_result(val) self.outcome, self.outcome_timestamp = fut, ts def set_exception(self, exc_info): ts = _utils.now() fut = Future(self.attempt_number) _utils.capture(fut, exc_info) self.outcome, self.outcome_timestamp = fut, ts if iscoroutinefunction: from tenacity._asyncio import AsyncRetrying if tornado: from tenacity.tornadoweb import TornadoRetrying ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608024633.0 tenacity-6.3.1/tenacity/_asyncio.py0000644000076500000240000000536700000000000015633 0ustar00jdstaff# -*- coding: utf-8 -*- # Copyright 2016 Étienne Bersac # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from asyncio import sleep from tenacity import AttemptManager from tenacity import BaseRetrying from tenacity import DoAttempt from tenacity import DoSleep from tenacity import RetryCallState class AsyncRetrying(BaseRetrying): def __init__(self, sleep=sleep, **kwargs): super(AsyncRetrying, self).__init__(**kwargs) self.sleep = sleep async def __call__(self, fn, *args, **kwargs): self.begin(fn) retry_state = RetryCallState( retry_object=self, fn=fn, args=args, kwargs=kwargs) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): try: result = await fn(*args, **kwargs) except BaseException: retry_state.set_exception(sys.exc_info()) else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() await self.sleep(do) else: return do def __aiter__(self): self.begin(None) self._retry_state = RetryCallState(self, fn=None, args=(), kwargs={}) return self async def __anext__(self): while True: do = self.iter(retry_state=self._retry_state) if do is None: raise StopAsyncIteration elif isinstance(do, DoAttempt): return AttemptManager(retry_state=self._retry_state) elif isinstance(do, DoSleep): self._retry_state.prepare_for_next_attempt() await self.sleep(do) else: return do def wraps(self, fn): fn = super().wraps(fn) # Ensure wrapper is recognized as a coroutine function. async def async_wrapped(*args, **kwargs): return await fn(*args, **kwargs) # Preserve attributes async_wrapped.retry = fn.retry async_wrapped.retry_with = fn.retry_with return async_wrapped ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1534510058.0 tenacity-6.3.1/tenacity/_utils.py0000644000076500000240000001065400000000000015321 0ustar00jdstaff# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import inspect import sys import time from functools import update_wrapper import six # sys.maxint / 2, since Python 3.2 doesn't have a sys.maxint... try: MAX_WAIT = sys.maxint / 2 except AttributeError: MAX_WAIT = 1073741823 if six.PY2: from functools import WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES def wraps(fn): """Do the same as six.wraps but only copy attributes that exist. For example, object instances don't have __name__ attribute, so six.wraps fails. This is fixed in Python 3 (https://bugs.python.org/issue3445), but didn't get backported to six. Also, see https://github.com/benjaminp/six/issues/250. """ def filter_hasattr(obj, attrs): return tuple(a for a in attrs if hasattr(obj, a)) return six.wraps( fn, assigned=filter_hasattr(fn, WRAPPER_ASSIGNMENTS), updated=filter_hasattr(fn, WRAPPER_UPDATES)) def capture(fut, tb): # TODO(harlowja): delete this in future, since its # has to repeatedly calculate this crap. fut.set_exception_info(tb[1], tb[2]) def getargspec(func): # This was deprecated in Python 3. return inspect.getargspec(func) else: from functools import wraps # noqa def capture(fut, tb): fut.set_exception(tb[1]) def getargspec(func): return inspect.getfullargspec(func) def visible_attrs(obj, attrs=None): if attrs is None: attrs = {} for attr_name, attr in inspect.getmembers(obj): if attr_name.startswith("_"): continue attrs[attr_name] = attr return attrs def find_ordinal(pos_num): # See: https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers if pos_num == 0: return "th" elif pos_num == 1: return 'st' elif pos_num == 2: return 'nd' elif pos_num == 3: return 'rd' elif pos_num >= 4 and pos_num <= 20: return 'th' else: return find_ordinal(pos_num % 10) def to_ordinal(pos_num): return "%i%s" % (pos_num, find_ordinal(pos_num)) def get_callback_name(cb): """Get a callback fully-qualified name. If no name can be produced ``repr(cb)`` is called and returned. """ segments = [] try: segments.append(cb.__qualname__) except AttributeError: try: segments.append(cb.__name__) if inspect.ismethod(cb): try: # This attribute doesn't exist on py3.x or newer, so # we optionally ignore it... (on those versions of # python `__qualname__` should have been found anyway). segments.insert(0, cb.im_class.__name__) except AttributeError: pass except AttributeError: pass if not segments: return repr(cb) else: try: # When running under sphinx it appears this can be none? if cb.__module__: segments.insert(0, cb.__module__) except AttributeError: pass return ".".join(segments) try: now = time.monotonic # noqa except AttributeError: from monotonic import monotonic as now # noqa class cached_property(object): """A property that is computed once per instance. Upon being computed it replaces itself with an ordinary attribute. Deleting the attribute resets the property. Source: https://github.com/bottlepy/bottle/blob/1de24157e74a6971d136550afe1b63eec5b0df2b/bottle.py#L234-L246 """ # noqa: E501 def __init__(self, func): update_wrapper(self, func) self.func = func def __get__(self, obj, cls): if obj is None: return self value = obj.__dict__[self.func.__name__] = self.func(obj) return value ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1534510058.0 tenacity-6.3.1/tenacity/after.py0000644000076500000240000000237200000000000015121 0ustar00jdstaff# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tenacity import _utils def after_nothing(retry_state): """After call strategy that does nothing.""" def after_log(logger, log_level, sec_format="%0.3f"): """After call strategy that logs to some logger the finished attempt.""" log_tpl = ("Finished call to '%s' after " + str(sec_format) + "(s), " "this was the %s time calling it.") def log_it(retry_state): logger.log(log_level, log_tpl, _utils.get_callback_name(retry_state.fn), retry_state.seconds_since_start, _utils.to_ordinal(retry_state.attempt_number)) return log_it ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1534510058.0 tenacity-6.3.1/tenacity/before.py0000644000076500000240000000216300000000000015260 0ustar00jdstaff# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tenacity import _utils def before_nothing(retry_state): """Before call strategy that does nothing.""" def before_log(logger, log_level): """Before call strategy that logs to some logger the attempt.""" def log_it(retry_state): logger.log(log_level, "Starting call to '%s', this is the %s time calling it.", _utils.get_callback_name(retry_state.fn), _utils.to_ordinal(retry_state.attempt_number)) return log_it ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588240811.0 tenacity-6.3.1/tenacity/before_sleep.py0000644000076500000240000000333600000000000016453 0ustar00jdstaff# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from tenacity import _utils from tenacity.compat import get_exc_info_from_future def before_sleep_nothing(retry_state): """Before call strategy that does nothing.""" def before_sleep_log(logger, log_level, exc_info=False): """Before call strategy that logs to some logger the attempt.""" def log_it(retry_state): if retry_state.outcome.failed: ex = retry_state.outcome.exception() verb, value = 'raised', '%s: %s' % (type(ex).__name__, ex) if exc_info: local_exc_info = get_exc_info_from_future(retry_state.outcome) else: local_exc_info = False else: verb, value = 'returned', retry_state.outcome.result() local_exc_info = False # exc_info does not apply when no exception logger.log(log_level, "Retrying %s in %s seconds as it %s %s.", _utils.get_callback_name(retry_state.fn), getattr(retry_state.next_action, 'sleep'), verb, value, exc_info=local_exc_info) return log_it ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1588240811.0 tenacity-6.3.1/tenacity/compat.py0000644000076500000240000002545600000000000015313 0ustar00jdstaff"""Utilities for providing backward compatibility.""" import inspect from fractions import Fraction from warnings import warn import six from tenacity import _utils def warn_about_non_retry_state_deprecation(cbname, func, stacklevel): msg = ( '"%s" function must accept single "retry_state" parameter,' ' please update %s' % (cbname, _utils.get_callback_name(func))) warn(msg, DeprecationWarning, stacklevel=stacklevel + 1) def warn_about_dunder_non_retry_state_deprecation(fn, stacklevel): msg = ( '"%s" method must be called with' ' single "retry_state" parameter' % (_utils.get_callback_name(fn))) warn(msg, DeprecationWarning, stacklevel=stacklevel + 1) def func_takes_retry_state(func): if not six.callable(func): raise Exception(func) return False if not inspect.isfunction(func) and not inspect.ismethod(func): # func is a callable object rather than a function/method func = func.__call__ func_spec = _utils.getargspec(func) return 'retry_state' in func_spec.args _unset = object() def _make_unset_exception(func_name, **kwargs): missing = [] for k, v in six.iteritems(kwargs): if v is _unset: missing.append(k) missing_str = ', '.join(repr(s) for s in missing) return TypeError(func_name + ' func missing parameters: ' + missing_str) def _set_delay_since_start(retry_state, delay): # Ensure outcome_timestamp - start_time is *exactly* equal to the delay to # avoid complexity in test code. retry_state.start_time = Fraction(retry_state.start_time) retry_state.outcome_timestamp = (retry_state.start_time + Fraction(delay)) assert retry_state.seconds_since_start == delay def make_retry_state(previous_attempt_number, delay_since_first_attempt, last_result=None): """Construct RetryCallState for given attempt number & delay. Only used in testing and thus is extra careful about timestamp arithmetics. """ required_parameter_unset = (previous_attempt_number is _unset or delay_since_first_attempt is _unset) if required_parameter_unset: raise _make_unset_exception( 'wait/stop', previous_attempt_number=previous_attempt_number, delay_since_first_attempt=delay_since_first_attempt) from tenacity import RetryCallState retry_state = RetryCallState(None, None, (), {}) retry_state.attempt_number = previous_attempt_number if last_result is not None: retry_state.outcome = last_result else: retry_state.set_result(None) _set_delay_since_start(retry_state, delay_since_first_attempt) return retry_state def func_takes_last_result(waiter): """Check if function has a "last_result" parameter. Needed to provide backward compatibility for wait functions that didn't take "last_result" in the beginning. """ if not six.callable(waiter): return False if not inspect.isfunction(waiter) and not inspect.ismethod(waiter): # waiter is a class, check dunder-call rather than dunder-init. waiter = waiter.__call__ waiter_spec = _utils.getargspec(waiter) return 'last_result' in waiter_spec.args def stop_dunder_call_accept_old_params(fn): """Decorate cls.__call__ method to accept old "stop" signature.""" @_utils.wraps(fn) def new_fn(self, previous_attempt_number=_unset, delay_since_first_attempt=_unset, retry_state=None): if retry_state is None: from tenacity import RetryCallState retry_state_passed_as_non_kwarg = ( previous_attempt_number is not _unset and isinstance(previous_attempt_number, RetryCallState)) if retry_state_passed_as_non_kwarg: retry_state = previous_attempt_number else: warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) retry_state = make_retry_state( previous_attempt_number=previous_attempt_number, delay_since_first_attempt=delay_since_first_attempt) return fn(self, retry_state=retry_state) return new_fn def stop_func_accept_retry_state(stop_func): """Wrap "stop" function to accept "retry_state" parameter.""" if not six.callable(stop_func): return stop_func if func_takes_retry_state(stop_func): return stop_func @_utils.wraps(stop_func) def wrapped_stop_func(retry_state): warn_about_non_retry_state_deprecation( 'stop', stop_func, stacklevel=4) return stop_func( retry_state.attempt_number, retry_state.seconds_since_start, ) return wrapped_stop_func def wait_dunder_call_accept_old_params(fn): """Decorate cls.__call__ method to accept old "wait" signature.""" @_utils.wraps(fn) def new_fn(self, previous_attempt_number=_unset, delay_since_first_attempt=_unset, last_result=None, retry_state=None): if retry_state is None: from tenacity import RetryCallState retry_state_passed_as_non_kwarg = ( previous_attempt_number is not _unset and isinstance(previous_attempt_number, RetryCallState)) if retry_state_passed_as_non_kwarg: retry_state = previous_attempt_number else: warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) retry_state = make_retry_state( previous_attempt_number=previous_attempt_number, delay_since_first_attempt=delay_since_first_attempt, last_result=last_result) return fn(self, retry_state=retry_state) return new_fn def wait_func_accept_retry_state(wait_func): """Wrap wait function to accept "retry_state" parameter.""" if not six.callable(wait_func): return wait_func if func_takes_retry_state(wait_func): return wait_func if func_takes_last_result(wait_func): @_utils.wraps(wait_func) def wrapped_wait_func(retry_state): warn_about_non_retry_state_deprecation( 'wait', wait_func, stacklevel=4) return wait_func( retry_state.attempt_number, retry_state.seconds_since_start, last_result=retry_state.outcome, ) else: @_utils.wraps(wait_func) def wrapped_wait_func(retry_state): warn_about_non_retry_state_deprecation( 'wait', wait_func, stacklevel=4) return wait_func( retry_state.attempt_number, retry_state.seconds_since_start, ) return wrapped_wait_func def retry_dunder_call_accept_old_params(fn): """Decorate cls.__call__ method to accept old "retry" signature.""" @_utils.wraps(fn) def new_fn(self, attempt=_unset, retry_state=None): if retry_state is None: from tenacity import RetryCallState if attempt is _unset: raise _make_unset_exception('retry', attempt=attempt) retry_state_passed_as_non_kwarg = ( attempt is not _unset and isinstance(attempt, RetryCallState)) if retry_state_passed_as_non_kwarg: retry_state = attempt else: warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) retry_state = RetryCallState(None, None, (), {}) retry_state.outcome = attempt return fn(self, retry_state=retry_state) return new_fn def retry_func_accept_retry_state(retry_func): """Wrap "retry" function to accept "retry_state" parameter.""" if not six.callable(retry_func): return retry_func if func_takes_retry_state(retry_func): return retry_func @_utils.wraps(retry_func) def wrapped_retry_func(retry_state): warn_about_non_retry_state_deprecation( 'retry', retry_func, stacklevel=4) return retry_func(retry_state.outcome) return wrapped_retry_func def before_func_accept_retry_state(fn): """Wrap "before" function to accept "retry_state".""" if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_before_func(retry_state): # func, trial_number, trial_time_taken warn_about_non_retry_state_deprecation('before', fn, stacklevel=4) return fn( retry_state.fn, retry_state.attempt_number, ) return wrapped_before_func def after_func_accept_retry_state(fn): """Wrap "after" function to accept "retry_state".""" if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_after_sleep_func(retry_state): # func, trial_number, trial_time_taken warn_about_non_retry_state_deprecation('after', fn, stacklevel=4) return fn( retry_state.fn, retry_state.attempt_number, retry_state.seconds_since_start) return wrapped_after_sleep_func def before_sleep_func_accept_retry_state(fn): """Wrap "before_sleep" function to accept "retry_state".""" if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_before_sleep_func(retry_state): # retry_object, sleep, last_result warn_about_non_retry_state_deprecation( 'before_sleep', fn, stacklevel=4) return fn( retry_state.retry_object, sleep=getattr(retry_state.next_action, 'sleep'), last_result=retry_state.outcome) return wrapped_before_sleep_func def retry_error_callback_accept_retry_state(fn): if not six.callable(fn): return fn if func_takes_retry_state(fn): return fn @_utils.wraps(fn) def wrapped_retry_error_callback(retry_state): warn_about_non_retry_state_deprecation( 'retry_error_callback', fn, stacklevel=4) return fn(retry_state.outcome) return wrapped_retry_error_callback def get_exc_info_from_future(future): """ Get an exc_info value from a Future. Given a a Future instance, retrieve an exc_info value suitable for passing in as the exc_info parameter to logging.Logger.log() and related methods. On Python 2, this will be a (type, value, traceback) triple. On Python 3, this will be an exception instance (with embedded traceback). If there was no exception, None is returned on both versions of Python. """ if six.PY3: return future.exception() else: ex, tb = future.exception_info() if ex is None: return None return type(ex), ex, tb ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1604332774.0 tenacity-6.3.1/tenacity/nap.py0000644000076500000240000000240000000000000014566 0ustar00jdstaff# -*- coding: utf-8 -*- # Copyright 2016 Étienne Bersac # Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import time def sleep(seconds): """ Sleep strategy that delays execution for a given number of seconds. This is the default strategy, and may be mocked out for unit testing. """ time.sleep(seconds) class sleep_using_event(object): """Sleep strategy that waits on an event to be set.""" def __init__(self, event): self.event = event def __call__(self, timeout): # NOTE(harlowja): this may *not* actually wait for timeout # seconds if the event is set (ie this may eject out early). self.event.wait(timeout=timeout) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1587115511.0 tenacity-6.3.1/tenacity/py.typed0000644000076500000240000000000000000000000015127 0ustar00jdstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1604332774.0 tenacity-6.3.1/tenacity/retry.py0000644000076500000240000001340600000000000015165 0ustar00jdstaff# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import re import six from tenacity import compat as _compat @six.add_metaclass(abc.ABCMeta) class retry_base(object): """Abstract base class for retry strategies.""" @abc.abstractmethod def __call__(self, retry_state): pass def __and__(self, other): return retry_all(self, other) def __or__(self, other): return retry_any(self, other) class _retry_never(retry_base): """Retry strategy that never rejects any result.""" def __call__(self, retry_state): return False retry_never = _retry_never() class _retry_always(retry_base): """Retry strategy that always rejects any result.""" def __call__(self, retry_state): return True retry_always = _retry_always() class retry_if_exception(retry_base): """Retry strategy that retries if an exception verifies a predicate.""" def __init__(self, predicate): self.predicate = predicate @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if retry_state.outcome.failed: return self.predicate(retry_state.outcome.exception()) else: return False class retry_if_exception_type(retry_if_exception): """Retries if an exception has been raised of one or more types.""" def __init__(self, exception_types=Exception): self.exception_types = exception_types super(retry_if_exception_type, self).__init__( lambda e: isinstance(e, exception_types)) class retry_unless_exception_type(retry_if_exception): """Retries until an exception is raised of one or more types.""" def __init__(self, exception_types=Exception): self.exception_types = exception_types super(retry_unless_exception_type, self).__init__( lambda e: not isinstance(e, exception_types)) @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): # always retry if no exception was raised if not retry_state.outcome.failed: return True return self.predicate(retry_state.outcome.exception()) class retry_if_result(retry_base): """Retries if the result verifies a predicate.""" def __init__(self, predicate): self.predicate = predicate @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if not retry_state.outcome.failed: return self.predicate(retry_state.outcome.result()) else: return False class retry_if_not_result(retry_base): """Retries if the result refutes a predicate.""" def __init__(self, predicate): self.predicate = predicate @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if not retry_state.outcome.failed: return not self.predicate(retry_state.outcome.result()) else: return False class retry_if_exception_message(retry_if_exception): """Retries if an exception message equals or matches.""" def __init__(self, message=None, match=None): if message and match: raise TypeError( "{}() takes either 'message' or 'match', not both".format( self.__class__.__name__)) # set predicate if message: def message_fnc(exception): return message == str(exception) predicate = message_fnc elif match: prog = re.compile(match) def match_fnc(exception): return prog.match(str(exception)) predicate = match_fnc else: raise TypeError( "{}() missing 1 required argument 'message' or 'match'". format(self.__class__.__name__)) super(retry_if_exception_message, self).__init__(predicate) class retry_if_not_exception_message(retry_if_exception_message): """Retries until an exception message equals or matches.""" def __init__(self, *args, **kwargs): super(retry_if_not_exception_message, self).__init__(*args, **kwargs) # invert predicate if_predicate = self.predicate self.predicate = lambda *args_, **kwargs_: not if_predicate( *args_, **kwargs_) @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if not retry_state.outcome.failed: return True return self.predicate(retry_state.outcome.exception()) class retry_any(retry_base): """Retries if any of the retries condition is valid.""" def __init__(self, *retries): self.retries = tuple(_compat.retry_func_accept_retry_state(r) for r in retries) @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): return any(r(retry_state) for r in self.retries) class retry_all(retry_base): """Retries if all the retries condition are valid.""" def __init__(self, *retries): self.retries = tuple(_compat.retry_func_accept_retry_state(r) for r in retries) @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): return all(r(retry_state) for r in self.retries) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1565944916.0 tenacity-6.3.1/tenacity/stop.py0000644000076500000240000000553500000000000015011 0ustar00jdstaff# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import six from tenacity import compat as _compat @six.add_metaclass(abc.ABCMeta) class stop_base(object): """Abstract base class for stop strategies.""" @abc.abstractmethod def __call__(self, retry_state): pass def __and__(self, other): return stop_all(self, other) def __or__(self, other): return stop_any(self, other) class stop_any(stop_base): """Stop if any of the stop condition is valid.""" def __init__(self, *stops): self.stops = tuple(_compat.stop_func_accept_retry_state(stop_func) for stop_func in stops) @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return any(x(retry_state) for x in self.stops) class stop_all(stop_base): """Stop if all the stop conditions are valid.""" def __init__(self, *stops): self.stops = tuple(_compat.stop_func_accept_retry_state(stop_func) for stop_func in stops) @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return all(x(retry_state) for x in self.stops) class _stop_never(stop_base): """Never stop.""" @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return False stop_never = _stop_never() class stop_when_event_set(stop_base): """Stop when the given event is set.""" def __init__(self, event): self.event = event @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return self.event.is_set() class stop_after_attempt(stop_base): """Stop when the previous attempt >= max_attempt.""" def __init__(self, max_attempt_number): self.max_attempt_number = max_attempt_number @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return retry_state.attempt_number >= self.max_attempt_number class stop_after_delay(stop_base): """Stop when the time from the first attempt >= limit.""" def __init__(self, max_delay): self.max_delay = max_delay @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return retry_state.seconds_since_start >= self.max_delay ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1608146453.024431 tenacity-6.3.1/tenacity/tests/0000755000076500000240000000000000000000000014604 5ustar00jdstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1471507480.0 tenacity-6.3.1/tenacity/tests/__init__.py0000644000076500000240000000000000000000000016703 0ustar00jdstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608146428.0 tenacity-6.3.1/tenacity/tests/test_asyncio.py0000644000076500000240000001175700000000000017675 0ustar00jdstaff# coding: utf-8 # Copyright 2016 Étienne Bersac # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import asyncio import inspect import unittest import pytest import six from tenacity import AsyncRetrying, RetryError from tenacity import _asyncio as tasyncio from tenacity import retry, stop_after_attempt from tenacity.tests.test_tenacity import NoIOErrorAfterCount, current_time_ms from tenacity.wait import wait_fixed def asynctest(callable_): @six.wraps(callable_) def wrapper(*a, **kw): loop = asyncio.get_event_loop() return loop.run_until_complete(callable_(*a, **kw)) return wrapper async def _async_function(thing): await asyncio.sleep(0.00001) return thing.go() @retry async def _retryable_coroutine(thing): await asyncio.sleep(0.00001) return thing.go() @retry(stop=stop_after_attempt(2)) async def _retryable_coroutine_with_2_attempts(thing): await asyncio.sleep(0.00001) thing.go() class TestAsync(unittest.TestCase): @asynctest async def test_retry(self): thing = NoIOErrorAfterCount(5) await _retryable_coroutine(thing) assert thing.counter == thing.count @asynctest async def test_iscoroutinefunction(self): assert asyncio.iscoroutinefunction(_retryable_coroutine) assert inspect.iscoroutinefunction(_retryable_coroutine) @asynctest async def test_retry_using_async_retying(self): thing = NoIOErrorAfterCount(5) retrying = AsyncRetrying() await retrying(_async_function, thing) assert thing.counter == thing.count @asynctest async def test_retry_using_async_retying_legacy_method(self): thing = NoIOErrorAfterCount(5) retrying = AsyncRetrying() with pytest.warns(DeprecationWarning): await retrying.call(_async_function, thing) assert thing.counter == thing.count @asynctest async def test_stop_after_attempt(self): thing = NoIOErrorAfterCount(2) try: await _retryable_coroutine_with_2_attempts(thing) except RetryError: assert thing.counter == 2 def test_repr(self): repr(tasyncio.AsyncRetrying()) def test_retry_attributes(self): assert hasattr(_retryable_coroutine, 'retry') assert hasattr(_retryable_coroutine, 'retry_with') @asynctest async def test_attempt_number_is_correct_for_interleaved_coroutines(self): attempts = [] def after(retry_state): attempts.append((retry_state.args[0], retry_state.attempt_number)) thing1 = NoIOErrorAfterCount(3) thing2 = NoIOErrorAfterCount(3) await asyncio.gather( _retryable_coroutine.retry_with(after=after)(thing1), _retryable_coroutine.retry_with(after=after)(thing2)) # There's no waiting on retry, only a wait in the coroutine, so the # executions should be interleaved. even_thing_attempts = attempts[::2] things, attempt_nos1 = zip(*even_thing_attempts) assert len(set(things)) == 1 assert list(attempt_nos1) == [1, 2, 3] odd_thing_attempts = attempts[1::2] things, attempt_nos2 = zip(*odd_thing_attempts) assert len(set(things)) == 1 assert list(attempt_nos2) == [1, 2, 3] class TestContextManager(unittest.TestCase): @asynctest async def test_do_max_attempts(self): attempts = 0 retrying = tasyncio.AsyncRetrying(stop=stop_after_attempt(3)) try: async for attempt in retrying: with attempt: attempts += 1 raise Exception except RetryError: pass assert attempts == 3 @asynctest async def test_reraise(self): class CustomError(Exception): pass try: async for attempt in tasyncio.AsyncRetrying( stop=stop_after_attempt(1), reraise=True ): with attempt: raise CustomError() except CustomError: pass else: raise Exception @asynctest async def test_sleeps(self): start = current_time_ms() try: async for attempt in tasyncio.AsyncRetrying( stop=stop_after_attempt(1), wait=wait_fixed(1) ): with attempt: raise Exception() except RetryError: pass t = current_time_ms() - start self.assertLess(t, 1.1) if __name__ == "__main__": unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1604332774.0 tenacity-6.3.1/tenacity/tests/test_tenacity.py0000644000076500000240000015444500000000000020052 0ustar00jdstaff# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013 Ray Holder # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import re import sys import time import typing import unittest import warnings from contextlib import contextmanager from copy import copy import pytest import six.moves import tenacity from tenacity import RetryError, Retrying, retry from tenacity.compat import make_retry_state class TestBase(unittest.TestCase): def test_repr(self): class ConcreteRetrying(tenacity.BaseRetrying): def __call__(self): pass repr(ConcreteRetrying()) class TestStopConditions(unittest.TestCase): def test_never_stop(self): r = Retrying() self.assertFalse(r.stop(make_retry_state(3, 6546))) def test_stop_any(self): stop = tenacity.stop_any( tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4)) def s(*args): return stop(make_retry_state(*args)) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertTrue(s(4, 0.8)) self.assertTrue(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_all(self): stop = tenacity.stop_all( tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4)) def s(*args): return stop(make_retry_state(*args)) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertFalse(s(4, 0.8)) self.assertFalse(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_or(self): stop = tenacity.stop_after_delay(1) | tenacity.stop_after_attempt(4) def s(*args): return stop(make_retry_state(*args)) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertTrue(s(4, 0.8)) self.assertTrue(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_and(self): stop = tenacity.stop_after_delay(1) & tenacity.stop_after_attempt(4) def s(*args): return stop(make_retry_state(*args)) self.assertFalse(s(1, 0.1)) self.assertFalse(s(2, 0.2)) self.assertFalse(s(2, 0.8)) self.assertFalse(s(4, 0.8)) self.assertFalse(s(3, 1.8)) self.assertTrue(s(4, 1.8)) def test_stop_after_attempt(self): r = Retrying(stop=tenacity.stop_after_attempt(3)) self.assertFalse(r.stop(make_retry_state(2, 6546))) self.assertTrue(r.stop(make_retry_state(3, 6546))) self.assertTrue(r.stop(make_retry_state(4, 6546))) def test_stop_after_delay(self): r = Retrying(stop=tenacity.stop_after_delay(1)) self.assertFalse(r.stop(make_retry_state(2, 0.999))) self.assertTrue(r.stop(make_retry_state(2, 1))) self.assertTrue(r.stop(make_retry_state(2, 1.001))) def test_legacy_explicit_stop_type(self): Retrying(stop="stop_after_attempt") def test_stop_backward_compat(self): r = Retrying(stop=lambda attempt, delay: attempt == delay) with reports_deprecation_warning(): self.assertFalse(r.stop(make_retry_state(1, 3))) with reports_deprecation_warning(): self.assertFalse(r.stop(make_retry_state(100, 99))) with reports_deprecation_warning(): self.assertTrue(r.stop(make_retry_state(101, 101))) def test_retry_child_class_with_override_backward_compat(self): class MyStop(tenacity.stop_after_attempt): def __init__(self): super(MyStop, self).__init__(1) def __call__(self, attempt_number, seconds_since_start): return super(MyStop, self).__call__( attempt_number, seconds_since_start) retrying = Retrying(wait=tenacity.wait_fixed(0.01), stop=MyStop()) def failing(): raise NotImplementedError() with pytest.raises(RetryError): retrying(failing) def test_stop_func_with_retry_state(self): def stop_func(retry_state): rs = retry_state return rs.attempt_number == rs.seconds_since_start r = Retrying(stop=stop_func) self.assertFalse(r.stop(make_retry_state(1, 3))) self.assertFalse(r.stop(make_retry_state(100, 99))) self.assertTrue(r.stop(make_retry_state(101, 101))) class TestWaitConditions(unittest.TestCase): def test_no_sleep(self): r = Retrying() self.assertEqual(0, r.wait(18, 9879)) def test_fixed_sleep(self): r = Retrying(wait=tenacity.wait_fixed(1)) self.assertEqual(1, r.wait(12, 6546)) def test_incrementing_sleep(self): r = Retrying(wait=tenacity.wait_incrementing( start=500, increment=100)) self.assertEqual(500, r.wait(1, 6546)) self.assertEqual(600, r.wait(2, 6546)) self.assertEqual(700, r.wait(3, 6546)) def test_random_sleep(self): r = Retrying(wait=tenacity.wait_random(min=1, max=20)) times = set() for x in six.moves.range(1000): times.add(r.wait(1, 6546)) # this is kind of non-deterministic... self.assertTrue(len(times) > 1) for t in times: self.assertTrue(t >= 1) self.assertTrue(t < 20) def test_random_sleep_without_min(self): r = Retrying(wait=tenacity.wait_random(max=2)) times = set() times.add(r.wait(1, 6546)) times.add(r.wait(1, 6546)) times.add(r.wait(1, 6546)) times.add(r.wait(1, 6546)) # this is kind of non-deterministic... self.assertTrue(len(times) > 1) for t in times: self.assertTrue(t >= 0) self.assertTrue(t <= 2) def test_exponential(self): r = Retrying(wait=tenacity.wait_exponential()) self.assertEqual(r.wait(1, 0), 1) self.assertEqual(r.wait(2, 0), 2) self.assertEqual(r.wait(3, 0), 4) self.assertEqual(r.wait(4, 0), 8) self.assertEqual(r.wait(5, 0), 16) self.assertEqual(r.wait(6, 0), 32) self.assertEqual(r.wait(7, 0), 64) self.assertEqual(r.wait(8, 0), 128) def test_exponential_with_max_wait(self): r = Retrying(wait=tenacity.wait_exponential(max=40)) self.assertEqual(r.wait(1, 0), 1) self.assertEqual(r.wait(2, 0), 2) self.assertEqual(r.wait(3, 0), 4) self.assertEqual(r.wait(4, 0), 8) self.assertEqual(r.wait(5, 0), 16) self.assertEqual(r.wait(6, 0), 32) self.assertEqual(r.wait(7, 0), 40) self.assertEqual(r.wait(8, 0), 40) self.assertEqual(r.wait(50, 0), 40) def test_exponential_with_min_wait(self): r = Retrying(wait=tenacity.wait_exponential(min=20)) self.assertEqual(r.wait(1, 0), 20) self.assertEqual(r.wait(2, 0), 20) self.assertEqual(r.wait(3, 0), 20) self.assertEqual(r.wait(4, 0), 20) self.assertEqual(r.wait(5, 0), 20) self.assertEqual(r.wait(6, 0), 32) self.assertEqual(r.wait(7, 0), 64) self.assertEqual(r.wait(8, 0), 128) self.assertEqual(r.wait(20, 0), 524288) def test_exponential_with_max_wait_and_multiplier(self): r = Retrying(wait=tenacity.wait_exponential( max=50, multiplier=1)) self.assertEqual(r.wait(1, 0), 1) self.assertEqual(r.wait(2, 0), 2) self.assertEqual(r.wait(3, 0), 4) self.assertEqual(r.wait(4, 0), 8) self.assertEqual(r.wait(5, 0), 16) self.assertEqual(r.wait(6, 0), 32) self.assertEqual(r.wait(7, 0), 50) self.assertEqual(r.wait(8, 0), 50) self.assertEqual(r.wait(50, 0), 50) def test_exponential_with_min_wait_and_multiplier(self): r = Retrying(wait=tenacity.wait_exponential( min=20, multiplier=2)) self.assertEqual(r.wait(1, 0), 20) self.assertEqual(r.wait(2, 0), 20) self.assertEqual(r.wait(3, 0), 20) self.assertEqual(r.wait(4, 0), 20) self.assertEqual(r.wait(5, 0), 32) self.assertEqual(r.wait(6, 0), 64) self.assertEqual(r.wait(7, 0), 128) self.assertEqual(r.wait(8, 0), 256) self.assertEqual(r.wait(20, 0), 1048576) def test_exponential_with_min_wait_and_max_wait(self): r = Retrying(wait=tenacity.wait_exponential(min=10, max=100)) self.assertEqual(r.wait(1, 0), 10) self.assertEqual(r.wait(2, 0), 10) self.assertEqual(r.wait(3, 0), 10) self.assertEqual(r.wait(4, 0), 10) self.assertEqual(r.wait(5, 0), 16) self.assertEqual(r.wait(6, 0), 32) self.assertEqual(r.wait(7, 0), 64) self.assertEqual(r.wait(8, 0), 100) self.assertEqual(r.wait(9, 0), 100) self.assertEqual(r.wait(20, 0), 100) def test_legacy_explicit_wait_type(self): Retrying(wait="exponential_sleep") def test_wait_backward_compat_with_result(self): captures = [] def wait_capture(attempt, delay, last_result=None): captures.append(last_result) return 1 def dying(): raise Exception("Broken") r_attempts = 10 r = Retrying(wait=wait_capture, sleep=lambda secs: None, stop=tenacity.stop_after_attempt(r_attempts), reraise=True) with reports_deprecation_warning(): self.assertRaises(Exception, r, dying) self.assertEqual(r_attempts - 1, len(captures)) self.assertTrue(all([r.failed for r in captures])) def test_wait_func(self): def wait_func(retry_state): return retry_state.attempt_number * retry_state.seconds_since_start r = Retrying(wait=wait_func) self.assertEqual(r.wait(make_retry_state(1, 5)), 5) self.assertEqual(r.wait(make_retry_state(2, 11)), 22) self.assertEqual(r.wait(make_retry_state(10, 100)), 1000) def test_wait_combine(self): r = Retrying(wait=tenacity.wait_combine(tenacity.wait_random(0, 3), tenacity.wait_fixed(5))) # Test it a few time since it's random for i in six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 8) self.assertGreaterEqual(w, 5) def test_wait_double_sum(self): r = Retrying(wait=tenacity.wait_random(0, 3) + tenacity.wait_fixed(5)) # Test it a few time since it's random for i in six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 8) self.assertGreaterEqual(w, 5) def test_wait_triple_sum(self): r = Retrying(wait=tenacity.wait_fixed(1) + tenacity.wait_random(0, 3) + tenacity.wait_fixed(5)) # Test it a few time since it's random for i in six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 9) self.assertGreaterEqual(w, 6) def test_wait_arbitrary_sum(self): r = Retrying(wait=sum([tenacity.wait_fixed(1), tenacity.wait_random(0, 3), tenacity.wait_fixed(5), tenacity.wait_none()])) # Test it a few time since it's random for i in six.moves.range(1000): w = r.wait(1, 5) self.assertLess(w, 9) self.assertGreaterEqual(w, 6) def _assert_range(self, wait, min_, max_): self.assertLess(wait, max_) self.assertGreaterEqual(wait, min_) def _assert_inclusive_range(self, wait, low, high): self.assertLessEqual(wait, high) self.assertGreaterEqual(wait, low) def _assert_inclusive_epsilon(self, wait, target, epsilon): self.assertLessEqual(wait, target + epsilon) self.assertGreaterEqual(wait, target - epsilon) def test_wait_chain(self): r = Retrying(wait=tenacity.wait_chain( *[tenacity.wait_fixed(1) for i in six.moves.range(2)] + [tenacity.wait_fixed(4) for i in six.moves.range(2)] + [tenacity.wait_fixed(8) for i in six.moves.range(1)])) for i in six.moves.range(10): w = r.wait(i + 1, 1) if i < 2: self._assert_range(w, 1, 2) elif i < 4: self._assert_range(w, 4, 5) else: self._assert_range(w, 8, 9) def test_wait_chain_multiple_invocations(self): sleep_intervals = [] r = Retrying( sleep=sleep_intervals.append, wait=tenacity.wait_chain(*[ tenacity.wait_fixed(i + 1) for i in six.moves.range(3) ]), stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_if_result(lambda x: x == 1), ) @r.wraps def always_return_1(): return 1 self.assertRaises(tenacity.RetryError, always_return_1) self.assertEqual(sleep_intervals, [1.0, 2.0, 3.0, 3.0]) sleep_intervals[:] = [] # Clear and restart retrying. self.assertRaises(tenacity.RetryError, always_return_1) self.assertEqual(sleep_intervals, [1.0, 2.0, 3.0, 3.0]) sleep_intervals[:] = [] def test_wait_random_exponential(self): fn = tenacity.wait_random_exponential(0.5, 60.0) for _ in six.moves.range(1000): self._assert_inclusive_range(fn(make_retry_state(1, 0)), 0, 0.5) self._assert_inclusive_range(fn(make_retry_state(2, 0)), 0, 1.0) self._assert_inclusive_range(fn(make_retry_state(3, 0)), 0, 2.0) self._assert_inclusive_range(fn(make_retry_state(4, 0)), 0, 4.0) self._assert_inclusive_range(fn(make_retry_state(5, 0)), 0, 8.0) self._assert_inclusive_range(fn(make_retry_state(6, 0)), 0, 16.0) self._assert_inclusive_range(fn(make_retry_state(7, 0)), 0, 32.0) self._assert_inclusive_range(fn(make_retry_state(8, 0)), 0, 60.0) self._assert_inclusive_range(fn(make_retry_state(9, 0)), 0, 60.0) fn = tenacity.wait_random_exponential(10, 5) for _ in six.moves.range(1000): self._assert_inclusive_range( fn(make_retry_state(1, 0)), 0.00, 5.00 ) # Default arguments exist fn = tenacity.wait_random_exponential() fn(0, 0) def test_wait_random_exponential_statistically(self): fn = tenacity.wait_random_exponential(0.5, 60.0) attempt = [] for i in six.moves.range(10): attempt.append( [fn(make_retry_state(i, 0)) for _ in six.moves.range(4000)] ) def mean(lst): return float(sum(lst)) / float(len(lst)) # skipping attempt 0 self._assert_inclusive_epsilon(mean(attempt[1]), 0.25, 0.02) self._assert_inclusive_epsilon(mean(attempt[2]), 0.50, 0.04) self._assert_inclusive_epsilon(mean(attempt[3]), 1, 0.08) self._assert_inclusive_epsilon(mean(attempt[4]), 2, 0.16) self._assert_inclusive_epsilon(mean(attempt[5]), 4, 0.32) self._assert_inclusive_epsilon(mean(attempt[6]), 8, 0.64) self._assert_inclusive_epsilon(mean(attempt[7]), 16, 1.28) self._assert_inclusive_epsilon(mean(attempt[8]), 30, 2.56) self._assert_inclusive_epsilon(mean(attempt[9]), 30, 2.56) def test_wait_backward_compat(self): """Ensure Retrying object accepts both old and newstyle wait funcs.""" def wait1(previous_attempt_number, delay_since_first_attempt): wait1.calls.append(( previous_attempt_number, delay_since_first_attempt)) return 0 wait1.calls = [] def wait2(previous_attempt_number, delay_since_first_attempt, last_result): wait2.calls.append(( previous_attempt_number, delay_since_first_attempt, last_result)) return 0 wait2.calls = [] def dying(): raise Exception("Broken") retrying1 = Retrying(wait=wait1, stop=tenacity.stop_after_attempt(4)) with reports_deprecation_warning(): self.assertRaises(Exception, lambda: retrying1(dying)) self.assertEqual([t[0] for t in wait1.calls], [1, 2, 3]) # This assumes that 3 iterations complete within 1 second. self.assertTrue(all(t[1] < 1 for t in wait1.calls)) retrying2 = Retrying(wait=wait2, stop=tenacity.stop_after_attempt(4)) with reports_deprecation_warning(): self.assertRaises(Exception, lambda: retrying2(dying)) self.assertEqual([t[0] for t in wait2.calls], [1, 2, 3]) # This assumes that 3 iterations complete within 1 second. self.assertTrue(all(t[1] < 1 for t in wait2.calls)) self.assertEqual([str(t[2].exception()) for t in wait2.calls], ['Broken'] * 3) def test_wait_class_backward_compatibility(self): """Ensure builtin objects accept both old and new parameters.""" waitobj = tenacity.wait_fixed(5) self.assertEqual(waitobj(1, 0.1), 5) self.assertEqual( waitobj(1, 0.1, tenacity.Future.construct(1, 1, False)), 5) retry_state = make_retry_state(123, 456) self.assertEqual(retry_state.attempt_number, 123) self.assertEqual(retry_state.seconds_since_start, 456) self.assertEqual(waitobj(retry_state=retry_state), 5) def test_wait_retry_state_attributes(self): class ExtractCallState(Exception): pass # retry_state is mutable, so return it as an exception to extract the # exact values it has when wait is called and bypass any other logic. def waitfunc(retry_state): raise ExtractCallState(retry_state) retrying = Retrying( wait=waitfunc, retry=(tenacity.retry_if_exception_type() | tenacity.retry_if_result(lambda result: result == 123))) def returnval(): return 123 try: retrying(returnval) except ExtractCallState as err: retry_state = err.args[0] self.assertIs(retry_state.fn, returnval) self.assertEqual(retry_state.args, ()) self.assertEqual(retry_state.kwargs, {}) self.assertEqual(retry_state.outcome.result(), 123) self.assertEqual(retry_state.attempt_number, 1) self.assertGreaterEqual(retry_state.outcome_timestamp, retry_state.start_time) def dying(): raise Exception("Broken") try: retrying(dying) except ExtractCallState as err: retry_state = err.args[0] self.assertIs(retry_state.fn, dying) self.assertEqual(retry_state.args, ()) self.assertEqual(retry_state.kwargs, {}) self.assertEqual(str(retry_state.outcome.exception()), 'Broken') self.assertEqual(retry_state.attempt_number, 1) self.assertGreaterEqual(retry_state.outcome_timestamp, retry_state.start_time) class TestRetryConditions(unittest.TestCase): def test_retry_if_result(self): retry = (tenacity.retry_if_result(lambda x: x == 1)) def r(fut): retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertFalse(r(tenacity.Future.construct(1, 2, False))) def test_retry_if_not_result(self): retry = (tenacity.retry_if_not_result(lambda x: x == 1)) def r(fut): retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, False))) def test_retry_any(self): retry = tenacity.retry_any( tenacity.retry_if_result(lambda x: x == 1), tenacity.retry_if_result(lambda x: x == 2)) def r(fut): retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertTrue(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 3, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, True))) def test_retry_all(self): retry = tenacity.retry_all( tenacity.retry_if_result(lambda x: x == 1), tenacity.retry_if_result(lambda x: isinstance(x, int))) def r(fut): retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertFalse(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 3, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, True))) def test_retry_and(self): retry = (tenacity.retry_if_result(lambda x: x == 1) & tenacity.retry_if_result(lambda x: isinstance(x, int))) def r(fut): retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, 1, False))) self.assertFalse(r(tenacity.Future.construct(1, 2, False))) self.assertFalse(r(tenacity.Future.construct(1, 3, False))) self.assertFalse(r(tenacity.Future.construct(1, 1, True))) def test_retry_or(self): retry = (tenacity.retry_if_result(lambda x: x == "foo") | tenacity.retry_if_result(lambda x: isinstance(x, int))) def r(fut): retry_state = make_retry_state(1, 1.0, last_result=fut) return retry(retry_state) self.assertTrue(r(tenacity.Future.construct(1, "foo", False))) self.assertFalse(r(tenacity.Future.construct(1, "foobar", False))) self.assertFalse(r(tenacity.Future.construct(1, 2.2, False))) self.assertFalse(r(tenacity.Future.construct(1, 42, True))) def _raise_try_again(self): self._attempts += 1 if self._attempts < 3: raise tenacity.TryAgain def test_retry_try_again(self): self._attempts = 0 Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never)(self._raise_try_again) self.assertEqual(3, self._attempts) def test_retry_try_again_forever(self): def _r(): raise tenacity.TryAgain r = Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never) self.assertRaises(tenacity.RetryError, r, _r) self.assertEqual(5, r.statistics['attempt_number']) def test_retry_try_again_forever_reraise(self): def _r(): raise tenacity.TryAgain r = Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never, reraise=True) self.assertRaises(tenacity.TryAgain, r, _r) self.assertEqual(5, r.statistics['attempt_number']) def test_retry_if_exception_message_negative_no_inputs(self): with self.assertRaises(TypeError): tenacity.retry_if_exception_message() def test_retry_if_exception_message_negative_too_many_inputs(self): with self.assertRaises(TypeError): tenacity.retry_if_exception_message( message="negative", match="negative") class NoneReturnUntilAfterCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Return None until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 return None return True class NoIOErrorAfterCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Raise an IOError until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 raise IOError("Hi there, I'm an IOError") return True class NoNameErrorAfterCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Raise a NameError until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 raise NameError("Hi there, I'm a NameError") return True class NameErrorUntilCount(object): """Holds counter state for invoking a method several times in a row.""" derived_message = "Hi there, I'm a NameError" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Return True until after count threshold has been crossed. Then raise a NameError. """ if self.counter < self.count: self.counter += 1 return True raise NameError(self.derived_message) class IOErrorUntilCount(object): """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Return True until after count threshold has been crossed. Then raise an IOError. """ if self.counter < self.count: self.counter += 1 return True raise IOError("Hi there, I'm an IOError") class CustomError(Exception): """This is a custom exception class. Note that For Python 2.x, we don't strictly need to extend BaseException, however, Python 3.x will complain. While this test suite won't run correctly under Python 3.x without extending from the Python exception hierarchy, the actual module code is backwards compatible Python 2.x and will allow for cases where exception classes don't extend from the hierarchy. """ def __init__(self, value): self.value = value def __str__(self): return self.value class NoCustomErrorAfterCount(object): """Holds counter state for invoking a method several times in a row.""" derived_message = "This is a Custom exception class" def __init__(self, count): self.counter = 0 self.count = count def go(self): """Raise a CustomError until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 raise CustomError(self.derived_message) return True class CapturingHandler(logging.Handler): """Captures log records for inspection.""" def __init__(self, *args, **kwargs): super(CapturingHandler, self).__init__(*args, **kwargs) self.records = [] def emit(self, record): self.records.append(record) def current_time_ms(): return int(round(time.time() * 1000)) @retry(wait=tenacity.wait_fixed(0.05), retry=tenacity.retry_if_result(lambda result: result is None)) def _retryable_test_with_wait(thing): return thing.go() @retry(stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_result(lambda result: result is None)) def _retryable_test_with_stop(thing): return thing.go() @retry(retry=tenacity.retry_if_exception_type(IOError)) def _retryable_test_with_exception_type_io(thing): return thing.go() @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(IOError)) def _retryable_test_with_exception_type_io_attempt_limit(thing): return thing.go() @retry(retry=tenacity.retry_unless_exception_type(NameError)) def _retryable_test_with_unless_exception_type_name(thing): return thing.go() @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_unless_exception_type(NameError)) def _retryable_test_with_unless_exception_type_name_attempt_limit(thing): return thing.go() @retry(retry=tenacity.retry_unless_exception_type()) def _retryable_test_with_unless_exception_type_no_input(thing): return thing.go() @retry( stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_if_exception_message( message=NoCustomErrorAfterCount.derived_message)) def _retryable_test_if_exception_message_message(thing): return thing.go() @retry(retry=tenacity.retry_if_not_exception_message( message=NoCustomErrorAfterCount.derived_message)) def _retryable_test_if_not_exception_message_message(thing): return thing.go() @retry(retry=tenacity.retry_if_exception_message( match=NoCustomErrorAfterCount.derived_message[:3] + ".*")) def _retryable_test_if_exception_message_match(thing): return thing.go() @retry(retry=tenacity.retry_if_not_exception_message( match=NoCustomErrorAfterCount.derived_message[:3] + ".*")) def _retryable_test_if_not_exception_message_match(thing): return thing.go() @retry(retry=tenacity.retry_if_not_exception_message( message=NameErrorUntilCount.derived_message)) def _retryable_test_not_exception_message_delay(thing): return thing.go() @retry def _retryable_default(thing): return thing.go() @retry() def _retryable_default_f(thing): return thing.go() @retry(retry=tenacity.retry_if_exception_type(CustomError)) def _retryable_test_with_exception_type_custom(thing): return thing.go() @retry( stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(CustomError)) def _retryable_test_with_exception_type_custom_attempt_limit(thing): return thing.go() class TestDecoratorWrapper(unittest.TestCase): def test_with_wait(self): start = current_time_ms() result = _retryable_test_with_wait(NoneReturnUntilAfterCount(5)) t = current_time_ms() - start self.assertGreaterEqual(t, 250) self.assertTrue(result) def test_retry_with(self): start = current_time_ms() result = _retryable_test_with_wait.retry_with( wait=tenacity.wait_fixed(0.1))(NoneReturnUntilAfterCount(5)) t = current_time_ms() - start self.assertGreaterEqual(t, 500) self.assertTrue(result) def test_with_stop_on_return_value(self): try: _retryable_test_with_stop(NoneReturnUntilAfterCount(5)) self.fail("Expected RetryError after 3 attempts") except RetryError as re: self.assertFalse(re.last_attempt.failed) self.assertEqual(3, re.last_attempt.attempt_number) self.assertTrue(re.last_attempt.result() is None) 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): self.assertTrue(_retryable_test_with_exception_type_io( NoIOErrorAfterCount(5))) try: _retryable_test_with_exception_type_io(NoNameErrorAfterCount(5)) self.fail("Expected NameError") except NameError as n: self.assertTrue(isinstance(n, NameError)) print(n) self.assertTrue(_retryable_test_with_exception_type_custom( NoCustomErrorAfterCount(5))) try: _retryable_test_with_exception_type_custom( NoNameErrorAfterCount(5)) self.fail("Expected NameError") except NameError as n: self.assertTrue(isinstance(n, NameError)) print(n) def test_retry_until_exception_of_type_attempt_number(self): try: self.assertTrue(_retryable_test_with_unless_exception_type_name( NameErrorUntilCount(5))) except NameError as e: s = _retryable_test_with_unless_exception_type_name.\ retry.statistics self.assertTrue(s['attempt_number'] == 6) print(e) else: self.fail("Expected NameError") def test_retry_until_exception_of_type_no_type(self): try: # no input should catch all subclasses of Exception self.assertTrue( _retryable_test_with_unless_exception_type_no_input( NameErrorUntilCount(5)) ) except NameError as e: s = _retryable_test_with_unless_exception_type_no_input.\ retry.statistics self.assertTrue(s['attempt_number'] == 6) print(e) else: self.fail("Expected NameError") def test_retry_until_exception_of_type_wrong_exception(self): try: # two iterations with IOError, one that returns True _retryable_test_with_unless_exception_type_name_attempt_limit( IOErrorUntilCount(2)) self.fail("Expected RetryError") except RetryError as e: self.assertTrue(isinstance(e, RetryError)) print(e) def test_retry_if_exception_message(self): try: self.assertTrue(_retryable_test_if_exception_message_message( NoCustomErrorAfterCount(3))) except CustomError: print(_retryable_test_if_exception_message_message.retry. statistics) self.fail("CustomError should've been retried from errormessage") def test_retry_if_not_exception_message(self): try: self.assertTrue(_retryable_test_if_not_exception_message_message( NoCustomErrorAfterCount(2))) except CustomError: s = _retryable_test_if_not_exception_message_message.retry.\ statistics self.assertTrue(s['attempt_number'] == 1) def test_retry_if_not_exception_message_delay(self): try: self.assertTrue(_retryable_test_not_exception_message_delay( NameErrorUntilCount(3))) except NameError: s = _retryable_test_not_exception_message_delay.retry.statistics print(s['attempt_number']) self.assertTrue(s['attempt_number'] == 4) def test_retry_if_exception_message_match(self): try: self.assertTrue(_retryable_test_if_exception_message_match( NoCustomErrorAfterCount(3))) except CustomError: self.fail("CustomError should've been retried from errormessage") def test_retry_if_not_exception_message_match(self): try: self.assertTrue(_retryable_test_if_not_exception_message_message( NoCustomErrorAfterCount(2))) except CustomError: s = _retryable_test_if_not_exception_message_message.retry.\ statistics self.assertTrue(s['attempt_number'] == 1) def test_defaults(self): self.assertTrue(_retryable_default(NoNameErrorAfterCount(5))) self.assertTrue(_retryable_default_f(NoNameErrorAfterCount(5))) self.assertTrue(_retryable_default(NoCustomErrorAfterCount(5))) self.assertTrue(_retryable_default_f(NoCustomErrorAfterCount(5))) def test_retry_function_object(self): """Test that six.wraps doesn't cause problems with callable objects. It raises an error upon trying to wrap it in Py2, because __name__ attribute is missing. It's fixed in Py3 but was never backported. """ class Hello(object): def __call__(self): return "Hello" retrying = Retrying(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3)) h = retrying.wraps(Hello()) self.assertEqual(h(), "Hello") def test_retry_child_class_with_override_backward_compat(self): def always_true(_): return True class MyRetry(tenacity.retry_if_exception): def __init__(self): super(MyRetry, self).__init__(always_true) def __call__(self, attempt): return super(MyRetry, self).__call__(attempt) retrying = Retrying(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(1), retry=MyRetry()) def failing(): raise NotImplementedError() with pytest.raises(RetryError): retrying(failing) class TestBeforeAfterAttempts(unittest.TestCase): _attempt_number = 0 def test_before_attempts(self): TestBeforeAfterAttempts._attempt_number = 0 def _before(retry_state): TestBeforeAfterAttempts._attempt_number = \ retry_state.attempt_number @retry(wait=tenacity.wait_fixed(1), stop=tenacity.stop_after_attempt(1), before=_before) def _test_before(): pass _test_before() self.assertTrue(TestBeforeAfterAttempts._attempt_number == 1) def test_after_attempts(self): TestBeforeAfterAttempts._attempt_number = 0 def _after(retry_state): TestBeforeAfterAttempts._attempt_number = \ retry_state.attempt_number @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(3), after=_after) def _test_after(): if TestBeforeAfterAttempts._attempt_number < 2: raise Exception("testing after_attempts handler") else: pass _test_after() self.assertTrue(TestBeforeAfterAttempts._attempt_number == 2) def test_before_sleep(self): def _before_sleep(retry_state): self.assertGreater(retry_state.next_action.sleep, 0) _before_sleep.attempt_number = retry_state.attempt_number @retry(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=_before_sleep) def _test_before_sleep(): if _before_sleep.attempt_number < 2: raise Exception("testing before_sleep_attempts handler") _test_before_sleep() self.assertEqual(_before_sleep.attempt_number, 2) def test_before_sleep_backward_compat(self): def _before_sleep(retry_obj, sleep, last_result): self.assertGreater(sleep, 0) _before_sleep.attempt_number = \ retry_obj.statistics['attempt_number'] _before_sleep.attempt_number = 0 @retry(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=_before_sleep) def _test_before_sleep(): if _before_sleep.attempt_number < 2: raise Exception("testing before_sleep_attempts handler") with reports_deprecation_warning(): _test_before_sleep() self.assertEqual(_before_sleep.attempt_number, 2) def _before_sleep(self, retry_state): self.slept += 1 def test_before_sleep_backward_compat_method(self): self.slept = 0 @retry(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=self._before_sleep) def _test_before_sleep(): raise Exception("testing before_sleep_attempts handler") try: _test_before_sleep() except tenacity.RetryError: pass self.assertEqual(self.slept, 2) def _before_sleep_log_raises(self, get_call_fn): thing = NoIOErrorAfterCount(2) logger = logging.getLogger(self.id()) logger.propagate = False logger.setLevel(logging.INFO) handler = CapturingHandler() logger.addHandler(handler) try: _before_sleep = tenacity.before_sleep_log(logger, logging.INFO) retrying = Retrying(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=_before_sleep) get_call_fn(retrying)(thing.go) finally: logger.removeHandler(handler) etalon_re = (r"^Retrying .* in 0\.01 seconds as it raised " r"(IO|OS)Error: Hi there, I'm an IOError\.$") self.assertEqual(len(handler.records), 2) fmt = logging.Formatter().format self.assertRegexpMatches(fmt(handler.records[0]), etalon_re) self.assertRegexpMatches(fmt(handler.records[1]), etalon_re) def test_before_sleep_log_raises(self): self._before_sleep_log_raises(lambda x: x) def test_before_sleep_log_raises_deprecated_call(self): self._before_sleep_log_raises(lambda x: x.call) def test_before_sleep_log_raises_with_exc_info(self): thing = NoIOErrorAfterCount(2) logger = logging.getLogger(self.id()) logger.propagate = False logger.setLevel(logging.INFO) handler = CapturingHandler() logger.addHandler(handler) try: _before_sleep = tenacity.before_sleep_log(logger, logging.INFO, exc_info=True) retrying = Retrying(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), before_sleep=_before_sleep) retrying(thing.go) finally: logger.removeHandler(handler) etalon_re = re.compile(r"^Retrying .* in 0\.01 seconds as it raised " r"(IO|OS)Error: Hi there, I'm an IOError\.{0}" r"Traceback \(most recent call last\):{0}" r".*$".format('\n'), flags=re.MULTILINE) self.assertEqual(len(handler.records), 2) fmt = logging.Formatter().format self.assertRegexpMatches(fmt(handler.records[0]), etalon_re) self.assertRegexpMatches(fmt(handler.records[1]), etalon_re) def test_before_sleep_log_returns(self, exc_info=False): thing = NoneReturnUntilAfterCount(2) logger = logging.getLogger(self.id()) logger.propagate = False logger.setLevel(logging.INFO) handler = CapturingHandler() logger.addHandler(handler) try: _before_sleep = tenacity.before_sleep_log(logger, logging.INFO, exc_info=exc_info) _retry = tenacity.retry_if_result(lambda result: result is None) retrying = Retrying(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3), retry=_retry, before_sleep=_before_sleep) retrying(thing.go) finally: logger.removeHandler(handler) etalon_re = r'^Retrying .* in 0\.01 seconds as it returned None\.$' self.assertEqual(len(handler.records), 2) fmt = logging.Formatter().format self.assertRegexpMatches(fmt(handler.records[0]), etalon_re) self.assertRegexpMatches(fmt(handler.records[1]), etalon_re) def test_before_sleep_log_returns_with_exc_info(self): self.test_before_sleep_log_returns(exc_info=True) class TestReraiseExceptions(unittest.TestCase): def test_reraise_by_default(self): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), reraise=True) def _reraised_by_default(): calls.append('x') raise KeyError("Bad key") self.assertRaises(KeyError, _reraised_by_default) self.assertEqual(2, len(calls)) def test_reraise_from_retry_error(self): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2)) def _raise_key_error(): calls.append('x') raise KeyError("Bad key") def _reraised_key_error(): try: _raise_key_error() except tenacity.RetryError as retry_err: retry_err.reraise() self.assertRaises(KeyError, _reraised_key_error) self.assertEqual(2, len(calls)) def test_reraise_timeout_from_retry_error(self): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), retry=lambda retry_state: True) def _mock_fn(): calls.append('x') def _reraised_mock_fn(): try: _mock_fn() except tenacity.RetryError as retry_err: retry_err.reraise() self.assertRaises(tenacity.RetryError, _reraised_mock_fn) self.assertEqual(2, len(calls)) def test_reraise_no_exception(self): calls = [] @retry(wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(2), retry=lambda retry_state: True, reraise=True) def _mock_fn(): calls.append('x') self.assertRaises(tenacity.RetryError, _mock_fn) self.assertEqual(2, len(calls)) class TestStatistics(unittest.TestCase): def test_stats(self): @retry() def _foobar(): return 42 self.assertEqual({}, _foobar.retry.statistics) _foobar() self.assertEqual(1, _foobar.retry.statistics['attempt_number']) def test_stats_failing(self): @retry(stop=tenacity.stop_after_attempt(2)) def _foobar(): raise ValueError(42) self.assertEqual({}, _foobar.retry.statistics) try: _foobar() except Exception: pass self.assertEqual(2, _foobar.retry.statistics['attempt_number']) class TestRetryErrorCallback(unittest.TestCase): def setUp(self): self._attempt_number = 0 self._callback_called = False def _callback(self, fut): self._callback_called = True return fut def test_retry_error_callback_backward_compat(self): num_attempts = 3 def retry_error_callback(fut): retry_error_callback.called_times += 1 return fut retry_error_callback.called_times = 0 @retry(stop=tenacity.stop_after_attempt(num_attempts), retry_error_callback=retry_error_callback) def _foobar(): self._attempt_number += 1 raise Exception("This exception should not be raised") with reports_deprecation_warning(): result = _foobar() self.assertEqual(retry_error_callback.called_times, 1) self.assertEqual(num_attempts, self._attempt_number) self.assertIsInstance(result, tenacity.Future) def test_retry_error_callback(self): num_attempts = 3 def retry_error_callback(retry_state): retry_error_callback.called_times += 1 return retry_state.outcome retry_error_callback.called_times = 0 @retry(stop=tenacity.stop_after_attempt(num_attempts), retry_error_callback=retry_error_callback) def _foobar(): self._attempt_number += 1 raise Exception("This exception should not be raised") result = _foobar() self.assertEqual(retry_error_callback.called_times, 1) self.assertEqual(num_attempts, self._attempt_number) self.assertIsInstance(result, tenacity.Future) class TestContextManager(unittest.TestCase): def test_context_manager_retry_one(self): from tenacity import Retrying raise_ = True for attempt in Retrying(): with attempt: if raise_: raise_ = False raise Exception("Retry it!") def test_context_manager_on_error(self): from tenacity import Retrying class CustomError(Exception): pass retry = Retrying(retry=tenacity.retry_if_exception_type(IOError)) def test(): for attempt in retry: with attempt: raise CustomError("Don't retry!") self.assertRaises(CustomError, test) def test_context_manager_retry_error(self): from tenacity import Retrying retry = Retrying(stop=tenacity.stop_after_attempt(2)) def test(): for attempt in retry: with attempt: raise Exception("Retry it!") self.assertRaises(RetryError, test) def test_context_manager_reraise(self): from tenacity import Retrying class CustomError(Exception): pass retry = Retrying(reraise=True, stop=tenacity.stop_after_attempt(2)) def test(): for attempt in retry: with attempt: raise CustomError("Don't retry!") self.assertRaises(CustomError, test) class TestInvokeAsCallable: """Test direct invocation of Retrying as a callable.""" @staticmethod def invoke(retry, f): """ Invoke Retrying logic. Wrapper allows testing different call mechanisms in test sub-classes. """ return retry(f) def test_retry_one(self): def f(): f.calls.append(len(f.calls) + 1) if len(f.calls) <= 1: raise Exception("Retry it!") return 42 f.calls = [] retry = Retrying() assert self.invoke(retry, f) == 42 assert f.calls == [1, 2] def test_on_error(self): class CustomError(Exception): pass def f(): f.calls.append(len(f.calls) + 1) if len(f.calls) <= 1: raise CustomError("Don't retry!") return 42 f.calls = [] retry = Retrying(retry=tenacity.retry_if_exception_type(IOError)) with pytest.raises(CustomError): self.invoke(retry, f) assert f.calls == [1] def test_retry_error(self): def f(): f.calls.append(len(f.calls) + 1) raise Exception("Retry it!") f.calls = [] retry = Retrying(stop=tenacity.stop_after_attempt(2)) with pytest.raises(RetryError): self.invoke(retry, f) assert f.calls == [1, 2] def test_reraise(self): class CustomError(Exception): pass def f(): f.calls.append(len(f.calls) + 1) raise CustomError("Retry it!") f.calls = [] retry = Retrying(reraise=True, stop=tenacity.stop_after_attempt(2)) with pytest.raises(CustomError): self.invoke(retry, f) assert f.calls == [1, 2] class TestInvokeViaLegacyCallMethod(TestInvokeAsCallable): """Retrying.call() method should work the same as Retrying.__call__().""" @staticmethod def invoke(retry, f): with reports_deprecation_warning(): return retry.call(f) class TestRetryException(unittest.TestCase): def test_retry_error_is_pickleable(self): import pickle expected = RetryError(last_attempt=123) pickled = pickle.dumps(expected) actual = pickle.loads(pickled) self.assertEqual(expected.last_attempt, actual.last_attempt) class TestRetryTyping(unittest.TestCase): @pytest.mark.skipif( sys.version_info < (3, 0), reason="typeguard not supported for python 2" ) def test_retry_type_annotations(self): """The decorator should maintain types of decorated functions.""" # Just in case this is run with unit-test, return early for py2 if sys.version_info < (3, 0): return # Function-level import because we can't install this for python 2. from typeguard import check_type def num_to_str(number): # type: (int) -> str return str(number) # equivalent to a raw @retry decoration with_raw = retry(num_to_str) with_raw_result = with_raw(1) # equivalent to a @retry(...) decoration with_constructor = retry()(num_to_str) with_constructor_result = with_raw(1) # These raise TypeError exceptions if they fail check_type("with_raw", with_raw, typing.Callable[[int], str]) check_type("with_raw_result", with_raw_result, str) check_type( "with_constructor", with_constructor, typing.Callable[[int], str] ) check_type("with_constructor_result", with_constructor_result, str) @contextmanager def reports_deprecation_warning(): __tracebackhide__ = True oldfilters = copy(warnings.filters) warnings.simplefilter('always') try: with pytest.warns(DeprecationWarning): yield finally: warnings.filters = oldfilters class TestMockingSleep(): RETRY_ARGS = dict( wait=tenacity.wait_fixed(0.1), stop=tenacity.stop_after_attempt(5), ) def _fail(self): raise NotImplementedError() @retry(**RETRY_ARGS) def _decorated_fail(self): self._fail() @pytest.fixture() def mock_sleep(self, monkeypatch): class MockSleep(object): call_count = 0 def __call__(self, seconds): self.call_count += 1 sleep = MockSleep() monkeypatch.setattr(tenacity.nap.time, "sleep", sleep) yield sleep def test_call(self, mock_sleep): retrying = Retrying(**self.RETRY_ARGS) with pytest.raises(RetryError): retrying.call(self._fail) assert mock_sleep.call_count == 4 def test_decorated(self, mock_sleep): with pytest.raises(RetryError): self._decorated_fail() assert mock_sleep.call_count == 4 def test_decorated_retry_with(self, mock_sleep): fail_faster = self._decorated_fail.retry_with( stop=tenacity.stop_after_attempt(2), ) with pytest.raises(RetryError): fail_faster() assert mock_sleep.call_count == 1 if __name__ == '__main__': unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1565944916.0 tenacity-6.3.1/tenacity/tests/test_tornado.py0000644000076500000240000000417600000000000017673 0ustar00jdstaff# coding: utf-8 # Copyright 2017 Elisey Zanko # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import unittest from tenacity import RetryError, retry, stop_after_attempt from tenacity import tornadoweb from tenacity.tests.test_tenacity import NoIOErrorAfterCount from tornado import gen from tornado import testing @retry @gen.coroutine def _retryable_coroutine(thing): yield gen.sleep(0.00001) thing.go() @retry(stop=stop_after_attempt(2)) @gen.coroutine def _retryable_coroutine_with_2_attempts(thing): yield gen.sleep(0.00001) thing.go() class TestTornado(testing.AsyncTestCase): @testing.gen_test def test_retry(self): assert gen.is_coroutine_function(_retryable_coroutine) thing = NoIOErrorAfterCount(5) yield _retryable_coroutine(thing) assert thing.counter == thing.count @testing.gen_test def test_stop_after_attempt(self): assert gen.is_coroutine_function(_retryable_coroutine) thing = NoIOErrorAfterCount(2) try: yield _retryable_coroutine_with_2_attempts(thing) except RetryError: assert thing.counter == 2 def test_repr(self): repr(tornadoweb.TornadoRetrying()) def test_old_tornado(self): old_attr = gen.is_coroutine_function try: del gen.is_coroutine_function # is_coroutine_function was introduced in tornado 4.5; # verify that we don't *completely* fall over on old versions @retry def retryable(thing): pass finally: gen.is_coroutine_function = old_attr if __name__ == '__main__': unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1604332774.0 tenacity-6.3.1/tenacity/tornadoweb.py0000644000076500000240000000326300000000000016164 0ustar00jdstaff# -*- coding: utf-8 -*- # Copyright 2017 Elisey Zanko # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from tenacity import BaseRetrying from tenacity import DoAttempt from tenacity import DoSleep from tenacity import RetryCallState from tornado import gen class TornadoRetrying(BaseRetrying): def __init__(self, sleep=gen.sleep, **kwargs): super(TornadoRetrying, self).__init__(**kwargs) self.sleep = sleep @gen.coroutine def __call__(self, fn, *args, **kwargs): self.begin(fn) retry_state = RetryCallState( retry_object=self, fn=fn, args=args, kwargs=kwargs) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): try: result = yield fn(*args, **kwargs) except BaseException: retry_state.set_exception(sys.exc_info()) else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() yield self.sleep(do) else: raise gen.Return(do) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1579091584.0 tenacity-6.3.1/tenacity/wait.py0000644000076500000240000001470500000000000014767 0ustar00jdstaff# Copyright 2016 Julien Danjou # Copyright 2016 Joshua Harlow # 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import abc import random import six from tenacity import _utils from tenacity import compat as _compat @six.add_metaclass(abc.ABCMeta) class wait_base(object): """Abstract base class for wait strategies.""" @abc.abstractmethod def __call__(self, retry_state): pass def __add__(self, other): return wait_combine(self, other) def __radd__(self, other): # make it possible to use multiple waits with the built-in sum function if other == 0: return self return self.__add__(other) class wait_fixed(wait_base): """Wait strategy that waits a fixed amount of time between each retry.""" def __init__(self, wait): self.wait_fixed = wait @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): return self.wait_fixed class wait_none(wait_fixed): """Wait strategy that doesn't wait at all before retrying.""" def __init__(self): super(wait_none, self).__init__(0) class wait_random(wait_base): """Wait strategy that waits a random amount of time between min/max.""" def __init__(self, min=0, max=1): # noqa self.wait_random_min = min self.wait_random_max = max @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): return (self.wait_random_min + (random.random() * (self.wait_random_max - self.wait_random_min))) class wait_combine(wait_base): """Combine several waiting strategies.""" def __init__(self, *strategies): self.wait_funcs = tuple(_compat.wait_func_accept_retry_state(strategy) for strategy in strategies) @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): return sum(x(retry_state=retry_state) for x in self.wait_funcs) class wait_chain(wait_base): """Chain two or more waiting strategies. If all strategies are exhausted, the very last strategy is used thereafter. For example:: @retry(wait=wait_chain(*[wait_fixed(1) for i in range(3)] + [wait_fixed(2) for j in range(5)] + [wait_fixed(5) for k in range(4))) def wait_chained(): print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s thereafter.") """ def __init__(self, *strategies): self.strategies = [_compat.wait_func_accept_retry_state(strategy) for strategy in strategies] @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): wait_func_no = min(max(retry_state.attempt_number, 1), len(self.strategies)) wait_func = self.strategies[wait_func_no - 1] return wait_func(retry_state=retry_state) class wait_incrementing(wait_base): """Wait an incremental amount of time after each attempt. Starting at a starting value and incrementing by a value for each attempt (and restricting the upper limit to some maximum value). """ def __init__(self, start=0, increment=100, max=_utils.MAX_WAIT): # noqa self.start = start self.increment = increment self.max = max @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): result = self.start + ( self.increment * (retry_state.attempt_number - 1) ) return max(0, min(result, self.max)) class wait_exponential(wait_base): """Wait strategy that applies exponential backoff. It allows for a customized multiplier and an ability to restrict the upper and lower limits to some maximum and minimum value. The intervals are fixed (i.e. there is no jitter), so this strategy is suitable for balancing retries against latency when a required resource is unavailable for an unknown duration, but *not* suitable for resolving contention between multiple processes for a shared resource. Use wait_random_exponential for the latter case. """ def __init__(self, multiplier=1, max=_utils.MAX_WAIT, exp_base=2, min=0): # noqa self.multiplier = multiplier self.min = min self.max = max self.exp_base = exp_base @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): try: exp = self.exp_base ** (retry_state.attempt_number - 1) result = self.multiplier * exp except OverflowError: return self.max return max(max(0, self.min), min(result, self.max)) class wait_random_exponential(wait_exponential): """Random wait with exponentially widening window. An exponential backoff strategy used to mediate contention between multiple uncoordinated processes for a shared resource in distributed systems. This is the sense in which "exponential backoff" is meant in e.g. Ethernet networking, and corresponds to the "Full Jitter" algorithm described in this blog post: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ Each retry occurs at a random time in a geometrically expanding interval. It allows for a custom multiplier and an ability to restrict the upper limit of the random interval to some maximum value. Example:: wait_random_exponential(multiplier=0.5, # initial window 0.5s max=60) # max 60s timeout When waiting for an unavailable resource to become available again, as opposed to trying to resolve contention for a shared resource, the wait_exponential strategy (which uses a fixed interval) may be preferable. """ @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): high = super(wait_random_exponential, self).__call__( retry_state=retry_state) return random.uniform(0, high) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1608146453.0228543 tenacity-6.3.1/tenacity.egg-info/0000755000076500000240000000000000000000000015134 5ustar00jdstaff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608146452.0 tenacity-6.3.1/tenacity.egg-info/PKG-INFO0000644000076500000240000000160600000000000016234 0ustar00jdstaffMetadata-Version: 2.1 Name: tenacity Version: 6.3.1 Summary: Retry code until it succeeds Home-page: https://github.com/jd/tenacity Author: Julien Danjou Author-email: julien@danjou.info License: Apache 2.0 Description: Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Utilities Provides-Extra: doc ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608146452.0 tenacity-6.3.1/tenacity.egg-info/SOURCES.txt0000644000076500000240000000166400000000000017027 0ustar00jdstaff.gitignore .mergify.yml .readthedocs.yml LICENSE README.rst reno.yaml setup.cfg setup.py tox.ini .circleci/config.yml doc/source/api.rst doc/source/changelog.rst doc/source/conf.py doc/source/index.rst releasenotes/notes/add-reno-d1ab5710f272650a.yaml releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml tenacity/__init__.py tenacity/_asyncio.py tenacity/_utils.py tenacity/after.py tenacity/before.py tenacity/before_sleep.py tenacity/compat.py tenacity/nap.py tenacity/py.typed tenacity/retry.py tenacity/stop.py tenacity/tornadoweb.py tenacity/wait.py tenacity.egg-info/PKG-INFO tenacity.egg-info/SOURCES.txt tenacity.egg-info/dependency_links.txt tenacity.egg-info/pbr.json tenacity.egg-info/requires.txt tenacity.egg-info/top_level.txt tenacity/tests/__init__.py tenacity/tests/test_asyncio.py tenacity/tests/test_tenacity.py tenacity/tests/test_tornado.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608146452.0 tenacity-6.3.1/tenacity.egg-info/dependency_links.txt0000644000076500000240000000000100000000000021202 0ustar00jdstaff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1565944969.0 tenacity-6.3.1/tenacity.egg-info/pbr.json0000644000076500000240000000005700000000000016614 0ustar00jdstaff{"git_version": "58495e5", "is_release": false}././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608146452.0 tenacity-6.3.1/tenacity.egg-info/requires.txt0000644000076500000240000000016300000000000017534 0ustar00jdstaffsix>=1.9.0 [:python_version == "2.7"] futures>=3.0 monotonic>=0.6 typing>=3.7.4.1 [doc] reno sphinx tornado>=4.5 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1608146452.0 tenacity-6.3.1/tenacity.egg-info/top_level.txt0000644000076500000240000000001100000000000017656 0ustar00jdstafftenacity ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1587115511.0 tenacity-6.3.1/tox.ini0000644000076500000240000000154700000000000013144 0ustar00jdstaff[tox] envlist = py27, py35, py36, py37, py38, pep8, pypy [testenv] usedevelop = True sitepackages = False deps = .[doc] pytest typeguard;python_version>='3.0' commands = py{27,py}: pytest --ignore='tenacity/tests/test_asyncio.py' {posargs} py3{5,6,7,8}: pytest {posargs} py3{5,6,7,8}: sphinx-build -a -E -W -b doctest doc/source doc/build py3{5,6,7,8}: sphinx-build -a -E -W -b html doc/source doc/build [testenv:pep8] basepython = python3 deps = flake8 flake8-import-order flake8-blind-except flake8-builtins flake8-docstrings flake8-rst-docstrings flake8-logging-format commands = flake8 [testenv:reno] basepython = python3 deps = reno commands = reno {posargs} [flake8] exclude = .tox,.eggs show-source = true ignore = D100,D101,D102,D103,D104,D105,D107,G200,G201,W503,W504 enable-extensions=G