././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1722255145.1237128 tenacity-9.0.0/0000755000175100001770000000000014651703451012753 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/.editorconfig0000644000175100001770000000044614651703440015432 0ustar00runnerdockerroot = true [*] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.{py,pyx,pxd,pyi}] indent_size = 4 max_line_length = 120 [*.ini] indent_size = 4 [*.rst] max_line_length = 150 [Makefile] indent_style = tab ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1722255145.1157126 tenacity-9.0.0/.github/0000755000175100001770000000000014651703451014313 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/.github/dependabot.yml0000644000175100001770000000027314651703440017143 0ustar00runnerdockerversion: 2 updates: - package-ecosystem: 'github-actions' directory: '/' schedule: interval: 'monthly' groups: github-actions: patterns: - '*' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1722255145.1157126 tenacity-9.0.0/.github/workflows/0000755000175100001770000000000014651703451016350 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/.github/workflows/ci.yaml0000644000175100001770000000214514651703440017627 0ustar00runnerdockername: Continuous Integration permissions: read-all on: pull_request: branches: - main concurrency: # yamllint disable-line rule:line-length group: "${{ github.workflow }}-${{ github.head_ref || github.run_id }}" cancel-in-progress: true jobs: test: timeout-minutes: 20 runs-on: ubuntu-20.04 strategy: matrix: include: - python: "3.8" tox: py38 - python: "3.9" tox: py39 - python: "3.10" tox: py310 - python: "3.11" tox: py311 - python: "3.12" tox: py312,py312-trio - python: "3.12" tox: pep8 - python: "3.11" tox: mypy steps: - name: Checkout πŸ›ŽοΈ uses: actions/checkout@v4.1.7 with: fetch-depth: 0 - name: Setup Python πŸ”§ uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python }} allow-prereleases: true - name: Build πŸ”§ & Test πŸ” run: | pip install tox tox -e ${{ matrix.tox }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/.github/workflows/deploy.yaml0000644000175100001770000000136214651703440020530 0ustar00runnerdockername: Release deploy on: release: types: - published jobs: publish: timeout-minutes: 20 runs-on: ubuntu-latest steps: - name: Checkout πŸ›ŽοΈ uses: actions/checkout@v4.1.7 with: fetch-depth: 0 - name: Setup Python πŸ”§ uses: actions/setup-python@v5.1.0 with: python-version: 3.11 - name: Build πŸ”§ & Deploy πŸš€ env: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} run: | pip install tox twine wheel echo -e "[pypi]" >> ~/.pypirc echo -e "username = __token__" >> ~/.pypirc echo -e "password = $PYPI_TOKEN" >> ~/.pypirc python setup.py sdist bdist_wheel twine upload dist/* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/.gitignore0000644000175100001770000000013414651703440014737 0ustar00runnerdocker.idea dist *.pyc *.egg-info build .tox/ AUTHORS ChangeLog .eggs/ doc/_build /.pytest_cache ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/.mergify.yml0000644000175100001770000000211114651703440015207 0ustar00runnerdockerqueue_rules: - name: default merge_method: squash queue_conditions: - or: - author = jd - "#approved-reviews-by >= 1" - author = dependabot[bot] - or: - files ~= ^releasenotes/notes/ - label = no-changelog - author = dependabot[bot] - "check-success=test (3.8, py38)" - "check-success=test (3.9, py39)" - "check-success=test (3.10, py310)" - "check-success=test (3.11, py311)" - "check-success=test (3.12, py312,py312-trio)" - "check-success=test (3.12, pep8)" pull_request_rules: - name: warn on no changelog conditions: - -files~=^releasenotes/notes/ - label!=no-changelog - -closed actions: comment: message: > ⚠️ No release notes detected. Please make sure to use [reno](https://docs.openstack.org/reno/latest/user/usage.html) to add a changelog entry. - name: automatic queue conditions: [] actions: queue: - name: dismiss reviews conditions: [] actions: dismiss_reviews: {} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/.readthedocs.yml0000644000175100001770000000014614651703440016040 0ustar00runnerdockerversion: 2 python: install: - method: pip path: . extra_requirements: - doc ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/LICENSE0000644000175100001770000002613514651703440013765 0ustar00runnerdocker 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=1722255145.1237128 tenacity-9.0.0/PKG-INFO0000644000175100001770000000166514651703451014060 0ustar00runnerdockerMetadata-Version: 2.1 Name: tenacity Version: 9.0.0 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 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Utilities Requires-Python: >=3.8 Provides-Extra: doc Provides-Extra: test License-File: LICENSE Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/README.rst0000644000175100001770000004255514651703440014453 0ustar00runnerdockerTenacity ======== .. 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/endpoint.svg?url=https://api.mergify.com/badges/jd/tenacity&style=flat :target: https://mergify.io :alt: Mergify Status **Please refer to the** `tenacity documentation `_ **for a better experience.** 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_gonna_give_you_up(): 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 If you're on a tight deadline, and exceeding your delay time isn't ok, then you can give up on retries one attempt before you would exceed the delay. .. testcode:: @retry(stop=stop_before_delay(10)) def stop_before_10_s(): print("Stopping 1 attempt before 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:: class ClientError(Exception): """Some type of client error.""" @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 @retry(retry=retry_if_not_exception_type(ClientError)) def might_client_error(): print("Retry forever with no wait if any error other than ClientError occurs. Immediately raise ClientError.") 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") See also these methods: .. testcode:: retry_if_exception retry_if_exception_type retry_if_not_exception_type retry_unless_exception_type retry_if_result retry_if_not_result retry_if_exception_message retry_if_not_exception_message retry_any retry_all 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 ~~~~~~~~~~~~~~ Normally when your function fails its final time (and will not be retried again based on your settings), a `RetryError` is raised. The exception your code encountered will be shown somewhere in the *middle* of the stack trace. If you would rather see the exception your code encountered at the *end* of the stack trace (where it is most visible), you can set `reraise=True`. .. 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 import sys 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 import sys 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 import sys 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 `statistics` attribute attached to the function: .. 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.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 RetryCallState ~~~~~~~~~~~~~~ ``retry_state`` argument is an object of :class:`~tenacity.RetryCallState` class. Other Custom Callbacks ~~~~~~~~~~~~~~~~~~~~~~ It's also possible to define custom callbacks for other keyword arguments. .. function:: my_stop(retry_state) :param RetryCallState retry_state: info about current retry invocation :return: whether or not retrying should stop :rtype: bool .. function:: my_wait(retry_state) :param RetryCallState retry_state: info about current retry invocation :return: number of seconds to wait before next retry :rtype: float .. function:: my_retry(retry_state) :param RetryCallState retry_state: info about current retry invocation :return: whether or not retrying should continue :rtype: bool .. function:: my_before(retry_state) :param RetryCallState retry_state: info about current retry invocation .. function:: my_after(retry_state) :param RetryCallState retry_state: info about current retry invocation .. function:: my_before_sleep(retry_state) :param RetryCallState 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 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.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') You may also want to change the behaviour of a decorated function temporarily, like in tests to avoid unnecessary wait times. You can modify/patch the `retry` attribute attached to the function. Bear in mind this is a write-only attribute, statistics should be read from the function `statistics` attribute. .. testcode:: @retry(stop=stop_after_attempt(3), wait=wait_fixed(3)) def raise_my_exception(): raise MyException("Fail") from unittest import mock with mock.patch.object(raise_my_exception.retry, "wait", wait_fixed(0)): try: raise_my_exception() except Exception: pass print(raise_my_exception.statistics) .. testoutput:: :hide: ... 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 In both cases, you may want to set the result to the attempt so it's available in retry strategies like ``retry_if_result``. This can be done accessing the ``retry_state`` property: .. testcode:: from tenacity import AsyncRetrying, retry_if_result async def function(): async for attempt in AsyncRetrying(retry=retry_if_result(lambda x: x < 3)): with attempt: result = 1 # Some complex calculation, function call, etc. if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(result) return result Async and retry ~~~~~~~~~~~~~~~ Finally, ``retry`` works also on asyncio, Trio, and Tornado (>= 4.5) coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_asyncio_function(loop): await loop.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry async def my_async_trio_function(): await trio.socket.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry @tornado.gen.coroutine def my_async_tornado_function(http_client, url): yield http_client.fetch(url) You can even use alternative event loops such as `curio` by passing the correct sleep function: .. code-block:: python @retry(sleep=curio.sleep) async def my_async_curio_function(): 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 **main** 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=1722255145.1117125 tenacity-9.0.0/doc/0000755000175100001770000000000014651703451013520 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1722255145.1157126 tenacity-9.0.0/doc/source/0000755000175100001770000000000014651703451015020 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/doc/source/api.rst0000644000175100001770000000300714651703440016321 0ustar00runnerdocker=============== API Reference =============== Retry Main API -------------- .. autofunction:: tenacity.retry :noindex: .. autoclass:: tenacity.Retrying :members: .. autoclass:: tenacity.AsyncRetrying :members: .. autoclass:: tenacity.tornadoweb.TornadoRetrying :members: .. autoclass:: tenacity.RetryCallState :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=1722255136.0 tenacity-9.0.0/doc/source/changelog.rst0000644000175100001770000000005014651703440017472 0ustar00runnerdockerChangelog ========= .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/doc/source/conf.py0000644000175100001770000000237214651703440016321 0ustar00runnerdocker# 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=1722255136.0 tenacity-9.0.0/doc/source/index.rst0000644000175100001770000004255514651703440016672 0ustar00runnerdockerTenacity ======== .. 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/endpoint.svg?url=https://api.mergify.com/badges/jd/tenacity&style=flat :target: https://mergify.io :alt: Mergify Status **Please refer to the** `tenacity documentation `_ **for a better experience.** 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_gonna_give_you_up(): 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 If you're on a tight deadline, and exceeding your delay time isn't ok, then you can give up on retries one attempt before you would exceed the delay. .. testcode:: @retry(stop=stop_before_delay(10)) def stop_before_10_s(): print("Stopping 1 attempt before 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:: class ClientError(Exception): """Some type of client error.""" @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 @retry(retry=retry_if_not_exception_type(ClientError)) def might_client_error(): print("Retry forever with no wait if any error other than ClientError occurs. Immediately raise ClientError.") 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") See also these methods: .. testcode:: retry_if_exception retry_if_exception_type retry_if_not_exception_type retry_unless_exception_type retry_if_result retry_if_not_result retry_if_exception_message retry_if_not_exception_message retry_any retry_all 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 ~~~~~~~~~~~~~~ Normally when your function fails its final time (and will not be retried again based on your settings), a `RetryError` is raised. The exception your code encountered will be shown somewhere in the *middle* of the stack trace. If you would rather see the exception your code encountered at the *end* of the stack trace (where it is most visible), you can set `reraise=True`. .. 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 import sys 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 import sys 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 import sys 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 `statistics` attribute attached to the function: .. 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.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 RetryCallState ~~~~~~~~~~~~~~ ``retry_state`` argument is an object of :class:`~tenacity.RetryCallState` class. Other Custom Callbacks ~~~~~~~~~~~~~~~~~~~~~~ It's also possible to define custom callbacks for other keyword arguments. .. function:: my_stop(retry_state) :param RetryCallState retry_state: info about current retry invocation :return: whether or not retrying should stop :rtype: bool .. function:: my_wait(retry_state) :param RetryCallState retry_state: info about current retry invocation :return: number of seconds to wait before next retry :rtype: float .. function:: my_retry(retry_state) :param RetryCallState retry_state: info about current retry invocation :return: whether or not retrying should continue :rtype: bool .. function:: my_before(retry_state) :param RetryCallState retry_state: info about current retry invocation .. function:: my_after(retry_state) :param RetryCallState retry_state: info about current retry invocation .. function:: my_before_sleep(retry_state) :param RetryCallState 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 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.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') You may also want to change the behaviour of a decorated function temporarily, like in tests to avoid unnecessary wait times. You can modify/patch the `retry` attribute attached to the function. Bear in mind this is a write-only attribute, statistics should be read from the function `statistics` attribute. .. testcode:: @retry(stop=stop_after_attempt(3), wait=wait_fixed(3)) def raise_my_exception(): raise MyException("Fail") from unittest import mock with mock.patch.object(raise_my_exception.retry, "wait", wait_fixed(0)): try: raise_my_exception() except Exception: pass print(raise_my_exception.statistics) .. testoutput:: :hide: ... 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 In both cases, you may want to set the result to the attempt so it's available in retry strategies like ``retry_if_result``. This can be done accessing the ``retry_state`` property: .. testcode:: from tenacity import AsyncRetrying, retry_if_result async def function(): async for attempt in AsyncRetrying(retry=retry_if_result(lambda x: x < 3)): with attempt: result = 1 # Some complex calculation, function call, etc. if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(result) return result Async and retry ~~~~~~~~~~~~~~~ Finally, ``retry`` works also on asyncio, Trio, and Tornado (>= 4.5) coroutines. Sleeps are done asynchronously too. .. code-block:: python @retry async def my_asyncio_function(loop): await loop.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry async def my_async_trio_function(): await trio.socket.getaddrinfo('8.8.8.8', 53) .. code-block:: python @retry @tornado.gen.coroutine def my_async_tornado_function(http_client, url): yield http_client.fetch(url) You can even use alternative event loops such as `curio` by passing the correct sleep function: .. code-block:: python @retry(sleep=curio.sleep) async def my_async_curio_function(): 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 **main** 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/pyproject.toml0000644000175100001770000000120114651703440015657 0ustar00runnerdocker[build-system] # Minimum requirements for the build system to execute. # PEP 508 specifications for PEP 518. # Banned setuptools versions have well-known issues requires = [ "setuptools >= 21.0.0,!=24.0.0,!=34.0.0,!=34.0.1,!=34.0.2,!=34.0.3,!=34.1.0,!=34.1.1,!=34.2.0,!=34.3.0,!=34.3.1,!=34.3.2,!=36.2.0", # PSF/ZPL "setuptools_scm[toml]>=3.4", ] build-backend="setuptools.build_meta" [tool.ruff] line-length = 88 indent-width = 4 target-version = "py38" [tool.mypy] strict = true files = ["tenacity", "tests"] show_error_codes = true [[tool.mypy.overrides]] module = "tornado.*" ignore_missing_imports = true [tool.setuptools_scm] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1722255145.1117125 tenacity-9.0.0/releasenotes/0000755000175100001770000000000014651703451015444 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1722255145.1197126 tenacity-9.0.0/releasenotes/notes/0000755000175100001770000000000014651703451016574 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/Fix-tests-for-typeguard-3.x-6eebfea546b6207e.yaml0000644000175100001770000000007014651703440027115 0ustar00runnerdocker--- fixes: - "Fixes test failures with typeguard 3.x" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml0000644000175100001770000000024514651703440032060 0ustar00runnerdocker--- other: - "Use `black` for code formatting and validate using `black --check`. Code compatibility: py26-py39." - "Enforce maximal line length to 120 symbols" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/add-async-actions-b249c527d99723bb.yaml0000644000175100001770000000023614651703440025125 0ustar00runnerdocker--- features: - | Added the ability to use async functions for retries. This way, you can now use asyncio coroutines for retry strategy predicates. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/add-reno-d1ab5710f272650a.yaml0000644000175100001770000000005614651703440023267 0ustar00runnerdocker--- features: - Add reno (changelog system) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml0000644000175100001770000000020314651703440030157 0ustar00runnerdocker--- features: - Add ``retry_if_not_exception_type()`` that allows to retry if a raised exception doesn't match given exceptions. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml0000644000175100001770000000055414651703440025706 0ustar00runnerdocker--- features: - | Added a new stop function: stop_before_delay, which will stop execution if the next sleep time would cause overall delay to exceed the specified delay. Useful for use cases where you have some upper bound on retry times that you must not exceed, so returning before that timeout is preferable than returning after that timeout.././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/add-test-extra-55e869261b03e56d.yaml0000644000175100001770000000004414651703440024367 0ustar00runnerdocker--- other: - Add a \"test\" extra ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml0000644000175100001770000000014514651703440030604 0ustar00runnerdocker--- other: - Add `retry_if_exception_cause_type`and `wait_exponential_jitter` to __all__ of init.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml0000644000175100001770000000027514651703440031040 0ustar00runnerdocker--- features: - | Add a new `retry_base` class called `retry_if_exception_cause_type` that checks, recursively, if any of the causes of the raised exception is of a certain type. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/added_a_link_to_documentation-eefaf8f074b539f8.yaml0000644000175100001770000000022614651703440030065 0ustar00runnerdocker--- other: - | Added a link to the documentation, as code snippets are not being rendered properly Changed branch name to main in index.rst ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/after_log-50f4d73b24ce9203.yaml0000644000175100001770000000013414651703440023546 0ustar00runnerdocker--- fixes: - "Fix after_log logger format: function name was used with delay formatting." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml0000644000175100001770000000013714651703440026512 0ustar00runnerdocker--- other: - Unit tests can now mock ``nap.sleep()`` for testing in all tenacity usage styles././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/annotate_code-197b93130df14042.yaml0000644000175100001770000000007514651703440024250 0ustar00runnerdocker--- other: - Add type annotations to cover all public API. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml0000644000175100001770000000012414651703440027724 0ustar00runnerdocker--- features: - Add an ``exc_info`` option to the ``before_sleep_log()`` strategy.././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml0000644000175100001770000000007714651703440026242 0ustar00runnerdocker--- prelude: > Clarify usage of `reraise` keyword argument ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/dependabot-for-github-actions-4d2464f3c0928463.yaml0000644000175100001770000000017714651703440027273 0ustar00runnerdocker--- other: - | Add a Dependabot configuration submit PRs monthly (as needed) to keep GitHub action versions updated. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml0000644000175100001770000000006314651703440026202 0ustar00runnerdocker--- other: - Do not package tests with tenacity. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml0000644000175100001770000000011714651703440030103 0ustar00runnerdocker--- other: - | Drop support for deprecated Python versions (2.7 and 3.5) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml0000644000175100001770000000042214651703440024644 0ustar00runnerdocker--- upgrade: - "Removed `BaseRetrying.call`: was long time deprecated and produced `DeprecationWarning`" - "Removed `BaseRetrying.fn`: was noted as deprecated" - "API change: `BaseRetrying.begin()` do not require arguments anymore as it not setting `BaseRetrying.fn`" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml0000644000175100001770000000012214651703440027044 0ustar00runnerdocker--- features: - Explicitly export convenience symbols from tenacity root module ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml0000644000175100001770000000012714651703440027166 0ustar00runnerdocker--- fixes: - | Fix async loop with retrying code block when result is available. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml0000644000175100001770000000013114651703440027022 0ustar00runnerdocker--- fixes: - | Avoid overwriting local contexts when applying the retry decorator. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml0000644000175100001770000000032614651703440027476 0ustar00runnerdocker--- fixes: - | Restore the value of the `retry` attribute for wrapped functions. Also, clarify that those attributes are write-only and statistics should be read from the function attribute directly. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml0000644000175100001770000000014314651703440026065 0ustar00runnerdocker--- fixes: - Fix setuptools config to include tenacity.asyncio package in release distributions. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml0000644000175100001770000000031314651703440025274 0ustar00runnerdocker--- fixes: - | Argument `wait` was improperly annotated, making mypy checks fail. Now it's annotated as `typing.Union[wait_base, typing.Callable[["RetryCallState"], typing.Union[float, int]]]` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml0000644000175100001770000000012414651703440023664 0ustar00runnerdocker--- fixes: - "Fix issue #288 : __name__ and other attributes for async functions" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml0000644000175100001770000000017714651703440027277 0ustar00runnerdocker--- fixes: - | Use str.format to format the logs internally to make logging compatible with other logger such as loguru. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/no-async-iter-6132a42e52348a75.yaml0000644000175100001770000000041014651703440024131 0ustar00runnerdocker--- fixes: - | `AsyncRetrying` was erroneously implementing `__iter__()`, making tenacity retrying mechanism working but in a synchronous fashion and not waiting as expected. This interface has been removed, `__aiter__()` should be used instead. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/pr320-py3-only-wheel-tag.yaml0000644000175100001770000000017514651703440023672 0ustar00runnerdocker--- other: >- Corrected the PyPI-published wheel tag to match the metadata saying that the release is Python 3 only. ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/py36_plus-c425fb3aa17c6682.yaml0000644000175100001770000000014314651703440023531 0ustar00runnerdocker--- features: - Most part of the code is type annotated. - Python 3.10 support has been added. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/remove-py36-876c0416cf279d15.yaml0000644000175100001770000000010114651703440023627 0ustar00runnerdocker--- upgrade: - | Support for PythonΒ 3.6 has been removed. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml0000644000175100001770000000014014651703440025624 0ustar00runnerdocker--- features: - Add a ``__repr__`` method to ``RetryCallState`` objects for easier debugging. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml0000644000175100001770000000012614651703440027620 0ustar00runnerdocker--- fixes: - | Preserve __defaults__ and __kwdefaults__ through retry decorator ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml0000644000175100001770000000011714651703440025732 0ustar00runnerdocker--- fixes: Sphinx build error where Sphinx complains about an undefined class. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml0000644000175100001770000000011114651703440030322 0ustar00runnerdocker--- features: - Add ``datetime.timedelta`` as accepted wait unit type. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml0000644000175100001770000000016414651703440025530 0ustar00runnerdocker--- features: - | - accept ``datetime.timedelta`` instances as argument to ``tenacity.stop.stop_after_delay`` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml0000644000175100001770000000027014651703440025432 0ustar00runnerdocker--- features: - | If you're using `Trio `__, then ``@retry`` now works automatically. It's no longer necessary to pass ``sleep=trio.sleep``. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml0000644000175100001770000000010614651703440027213 0ustar00runnerdocker--- fixes: - | Respects `min` arg for `wait_random_exponential` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml0000644000175100001770000000024014651703440027200 0ustar00runnerdocker--- features: - | Implement a wait.wait_exponential_jitter per Google's storage retry guide. See https://cloud.google.com/storage/docs/retry-strategy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/reno.yaml0000644000175100001770000000005114651703440014574 0ustar00runnerdocker--- unreleased_version_title: Unreleased ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1722255145.1237128 tenacity-9.0.0/setup.cfg0000644000175100001770000000220214651703451014570 0ustar00runnerdocker[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 :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Topic :: Utilities [options] install_requires = python_requires = >=3.8 packages = find: [options.packages.find] include = tenacity* exclude = tests [options.package_data] tenacity = py.typed [options.extras_require] doc = reno sphinx test = pytest tornado>=4.5 typeguard [tool:pytest] filterwarnings = once::DeprecationWarning [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/setup.py0000644000175100001770000000124214651703440014462 0ustar00runnerdocker#!/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=1722255145.1237128 tenacity-9.0.0/tenacity/0000755000175100001770000000000014651703451014573 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/__init__.py0000644000175100001770000005672514651703440016721 0ustar00runnerdocker# 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. import dataclasses import functools import sys import threading import time import typing as t import warnings from abc import ABC, abstractmethod from concurrent import futures from . import _utils # Import all built-in retry strategies for easier usage. from .retry import retry_base # noqa 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_exception_cause_type # noqa from .retry import retry_if_not_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_before_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 from .wait import wait_exponential_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 try: import tornado except ImportError: tornado = None if t.TYPE_CHECKING: import types from . import asyncio as tasyncio from .retry import RetryBaseT from .stop import StopBaseT from .wait import WaitBaseT WrappedFnReturnT = t.TypeVar("WrappedFnReturnT") WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any]) dataclass_kwargs = {} if sys.version_info >= (3, 10): dataclass_kwargs.update({"slots": True}) @dataclasses.dataclass(**dataclass_kwargs) class IterState: actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field( default_factory=list ) retry_run_result: bool = False delay_since_first_attempt: int = 0 stop_run_result: bool = False is_explicit_retry: bool = False def reset(self) -> None: self.actions = [] self.retry_run_result = False self.delay_since_first_attempt = 0 self.stop_run_result = False self.is_explicit_retry = False class TryAgain(Exception): """Always retry the executed function when raised.""" NO_RESULT = object() class DoAttempt: pass class DoSleep(float): pass class BaseAction: """Base class for representing actions to take by retry object. Concrete implementations must define: - __init__: to initialize all necessary fields - REPR_FIELDS: class variable specifying attributes to include in repr(self) - NAME: for identification in retry object methods and callbacks """ REPR_FIELDS: t.Sequence[str] = () NAME: t.Optional[str] = None def __repr__(self) -> str: state_str = ", ".join( f"{field}={getattr(self, field)!r}" for field in self.REPR_FIELDS ) return f"{self.__class__.__name__}({state_str})" def __str__(self) -> str: return repr(self) class RetryAction(BaseAction): REPR_FIELDS = ("sleep",) NAME = "retry" def __init__(self, sleep: t.SupportsFloat) -> None: self.sleep = float(sleep) _unset = object() def _first_set(first: t.Union[t.Any, object], second: t.Any) -> t.Any: return second if first is _unset else first class RetryError(Exception): """Encapsulates the last attempt instance right before giving up.""" def __init__(self, last_attempt: "Future") -> None: self.last_attempt = last_attempt super().__init__(last_attempt) def reraise(self) -> t.NoReturn: if self.last_attempt.failed: raise self.last_attempt.result() raise self def __str__(self) -> str: return f"{self.__class__.__name__}[{self.last_attempt}]" class AttemptManager: """Manage attempt context.""" def __init__(self, retry_state: "RetryCallState"): self.retry_state = retry_state def __enter__(self) -> None: pass def __exit__( self, exc_type: t.Optional[t.Type[BaseException]], exc_value: t.Optional[BaseException], traceback: t.Optional["types.TracebackType"], ) -> t.Optional[bool]: if exc_type is not None and exc_value is not None: 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) return None class BaseRetrying(ABC): def __init__( self, sleep: t.Callable[[t.Union[int, float]], None] = sleep, stop: "StopBaseT" = stop_never, wait: "WaitBaseT" = wait_none(), retry: "RetryBaseT" = retry_if_exception_type(), before: t.Callable[["RetryCallState"], None] = before_nothing, after: t.Callable[["RetryCallState"], None] = after_nothing, before_sleep: t.Optional[t.Callable[["RetryCallState"], None]] = None, reraise: bool = False, retry_error_cls: t.Type[RetryError] = RetryError, retry_error_callback: t.Optional[t.Callable[["RetryCallState"], t.Any]] = 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 def copy( self, sleep: t.Union[t.Callable[[t.Union[int, float]], None], object] = _unset, stop: t.Union["StopBaseT", object] = _unset, wait: t.Union["WaitBaseT", object] = _unset, retry: t.Union[retry_base, object] = _unset, before: t.Union[t.Callable[["RetryCallState"], None], object] = _unset, after: t.Union[t.Callable[["RetryCallState"], None], object] = _unset, before_sleep: t.Union[ t.Optional[t.Callable[["RetryCallState"], None]], object ] = _unset, reraise: t.Union[bool, object] = _unset, retry_error_cls: t.Union[t.Type[RetryError], object] = _unset, retry_error_callback: t.Union[ t.Optional[t.Callable[["RetryCallState"], t.Any]], object ] = _unset, ) -> "BaseRetrying": """Copy this object with some parameters changed if needed.""" return self.__class__( sleep=_first_set(sleep, self.sleep), stop=_first_set(stop, self.stop), wait=_first_set(wait, self.wait), retry=_first_set(retry, self.retry), before=_first_set(before, self.before), after=_first_set(after, self.after), before_sleep=_first_set(before_sleep, self.before_sleep), reraise=_first_set(reraise, self.reraise), retry_error_cls=_first_set(retry_error_cls, self.retry_error_cls), retry_error_callback=_first_set( retry_error_callback, self.retry_error_callback ), ) def __repr__(self) -> str: return ( f"<{self.__class__.__name__} object at 0x{id(self):x} (" f"stop={self.stop}, " f"wait={self.wait}, " f"sleep={self.sleep}, " f"retry={self.retry}, " f"before={self.before}, " f"after={self.after})>" ) @property def statistics(self) -> t.Dict[str, t.Any]: """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 # type: ignore[no-any-return] except AttributeError: self._local.statistics = t.cast(t.Dict[str, t.Any], {}) return self._local.statistics @property def iter_state(self) -> IterState: try: return self._local.iter_state # type: ignore[no-any-return] except AttributeError: self._local.iter_state = IterState() return self._local.iter_state def wraps(self, f: WrappedFn) -> WrappedFn: """Wrap a function for retrying. :param f: A function to wraps for retrying. """ @functools.wraps( f, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__") ) def wrapped_f(*args: t.Any, **kw: t.Any) -> t.Any: # Always create a copy to prevent overwriting the local contexts when # calling the same wrapped functions multiple times in the same stack copy = self.copy() wrapped_f.statistics = copy.statistics # type: ignore[attr-defined] return copy(f, *args, **kw) def retry_with(*args: t.Any, **kwargs: t.Any) -> WrappedFn: return self.copy(*args, **kwargs).wraps(f) # Preserve attributes wrapped_f.retry = self # type: ignore[attr-defined] wrapped_f.retry_with = retry_with # type: ignore[attr-defined] wrapped_f.statistics = {} # type: ignore[attr-defined] return wrapped_f # type: ignore[return-value] def begin(self) -> None: self.statistics.clear() self.statistics["start_time"] = time.monotonic() self.statistics["attempt_number"] = 1 self.statistics["idle_for"] = 0 def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None: self.iter_state.actions.append(fn) def _run_retry(self, retry_state: "RetryCallState") -> None: self.iter_state.retry_run_result = self.retry(retry_state) def _run_wait(self, retry_state: "RetryCallState") -> None: if self.wait: sleep = self.wait(retry_state) else: sleep = 0.0 retry_state.upcoming_sleep = sleep def _run_stop(self, retry_state: "RetryCallState") -> None: self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start self.iter_state.stop_run_result = self.stop(retry_state) def iter(self, retry_state: "RetryCallState") -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa self._begin_iter(retry_state) result = None for action in self.iter_state.actions: result = action(retry_state) return result def _begin_iter(self, retry_state: "RetryCallState") -> None: # noqa self.iter_state.reset() fut = retry_state.outcome if fut is None: if self.before is not None: self._add_action_func(self.before) self._add_action_func(lambda rs: DoAttempt()) return self.iter_state.is_explicit_retry = fut.failed and isinstance( fut.exception(), TryAgain ) if not self.iter_state.is_explicit_retry: self._add_action_func(self._run_retry) self._add_action_func(self._post_retry_check_actions) def _post_retry_check_actions(self, retry_state: "RetryCallState") -> None: if not (self.iter_state.is_explicit_retry or self.iter_state.retry_run_result): self._add_action_func(lambda rs: rs.outcome.result()) return if self.after is not None: self._add_action_func(self.after) self._add_action_func(self._run_wait) self._add_action_func(self._run_stop) self._add_action_func(self._post_stop_check_actions) def _post_stop_check_actions(self, retry_state: "RetryCallState") -> None: if self.iter_state.stop_run_result: if self.retry_error_callback: self._add_action_func(self.retry_error_callback) return def exc_check(rs: "RetryCallState") -> None: fut = t.cast(Future, rs.outcome) retry_exc = self.retry_error_cls(fut) if self.reraise: raise retry_exc.reraise() raise retry_exc from fut.exception() self._add_action_func(exc_check) return def next_action(rs: "RetryCallState") -> None: sleep = rs.upcoming_sleep rs.next_action = RetryAction(sleep) rs.idle_for += sleep self.statistics["idle_for"] += sleep self.statistics["attempt_number"] += 1 self._add_action_func(next_action) if self.before_sleep is not None: self._add_action_func(self.before_sleep) self._add_action_func(lambda rs: DoSleep(rs.upcoming_sleep)) def __iter__(self) -> t.Generator[AttemptManager, None, None]: self.begin() 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, fn: t.Callable[..., WrappedFnReturnT], *args: t.Any, **kwargs: t.Any, ) -> WrappedFnReturnT: pass class Retrying(BaseRetrying): """Retrying controller.""" def __call__( self, fn: t.Callable[..., WrappedFnReturnT], *args: t.Any, **kwargs: t.Any, ) -> WrappedFnReturnT: self.begin() 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: # noqa: B902 retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type] else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() self.sleep(do) else: return do # type: ignore[no-any-return] if sys.version_info >= (3, 9): FutureGenericT = futures.Future[t.Any] else: FutureGenericT = futures.Future class Future(FutureGenericT): """Encapsulates a (future or past) attempted call to a target function.""" def __init__(self, attempt_number: int) -> None: super().__init__() self.attempt_number = attempt_number @property def failed(self) -> bool: """Return whether a exception is being held in this future.""" return self.exception() is not None @classmethod def construct( cls, attempt_number: int, value: t.Any, has_exception: bool ) -> "Future": """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: """State related to a single call wrapped with Retrying.""" def __init__( self, retry_object: BaseRetrying, fn: t.Optional[WrappedFn], args: t.Any, kwargs: t.Any, ) -> None: #: Retry call start timestamp self.start_time = time.monotonic() #: 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: int = 1 #: Last outcome (result or exception) produced by the function self.outcome: t.Optional[Future] = None #: Timestamp of the last outcome self.outcome_timestamp: t.Optional[float] = None #: Time spent sleeping in retries self.idle_for: float = 0.0 #: Next action as decided by the retry manager self.next_action: t.Optional[RetryAction] = None #: Next sleep time as decided by the retry manager. self.upcoming_sleep: float = 0.0 @property def seconds_since_start(self) -> t.Optional[float]: if self.outcome_timestamp is None: return None return self.outcome_timestamp - self.start_time def prepare_for_next_attempt(self) -> None: self.outcome = None self.outcome_timestamp = None self.attempt_number += 1 self.next_action = None def set_result(self, val: t.Any) -> None: ts = time.monotonic() fut = Future(self.attempt_number) fut.set_result(val) self.outcome, self.outcome_timestamp = fut, ts def set_exception( self, exc_info: t.Tuple[ t.Type[BaseException], BaseException, "types.TracebackType| None" ], ) -> None: ts = time.monotonic() fut = Future(self.attempt_number) fut.set_exception(exc_info[1]) self.outcome, self.outcome_timestamp = fut, ts def __repr__(self) -> str: if self.outcome is None: result = "none yet" elif self.outcome.failed: exception = self.outcome.exception() result = f"failed ({exception.__class__.__name__} {exception})" else: result = f"returned {self.outcome.result()}" slept = float(round(self.idle_for, 2)) clsname = self.__class__.__name__ return f"<{clsname} {id(self)}: attempt #{self.attempt_number}; slept for {slept}; last result: {result}>" @t.overload def retry(func: WrappedFn) -> WrappedFn: ... @t.overload def retry( sleep: t.Callable[[t.Union[int, float]], t.Union[None, t.Awaitable[None]]] = sleep, stop: "StopBaseT" = stop_never, wait: "WaitBaseT" = wait_none(), retry: "t.Union[RetryBaseT, tasyncio.retry.RetryBaseT]" = retry_if_exception_type(), before: t.Callable[ ["RetryCallState"], t.Union[None, t.Awaitable[None]] ] = before_nothing, after: t.Callable[ ["RetryCallState"], t.Union[None, t.Awaitable[None]] ] = after_nothing, before_sleep: t.Optional[ t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] ] = None, reraise: bool = False, retry_error_cls: t.Type["RetryError"] = RetryError, retry_error_callback: t.Optional[ t.Callable[["RetryCallState"], t.Union[t.Any, t.Awaitable[t.Any]]] ] = None, ) -> t.Callable[[WrappedFn], WrappedFn]: ... def retry(*dargs: t.Any, **dkw: t.Any) -> t.Any: """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: WrappedFn) -> WrappedFn: if isinstance(f, retry_base): warnings.warn( f"Got retry_base instance ({f.__class__.__name__}) as callable argument, " f"this will probably hang indefinitely (did you mean retry={f.__class__.__name__}(...)?)" ) r: "BaseRetrying" if _utils.is_coroutine_callable(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 from tenacity.asyncio import AsyncRetrying # noqa:E402,I100 if tornado: from tenacity.tornadoweb import TornadoRetrying __all__ = [ "retry_base", "retry_all", "retry_always", "retry_any", "retry_if_exception", "retry_if_exception_type", "retry_if_exception_cause_type", "retry_if_not_exception_type", "retry_if_not_result", "retry_if_result", "retry_never", "retry_unless_exception_type", "retry_if_exception_message", "retry_if_not_exception_message", "sleep", "sleep_using_event", "stop_after_attempt", "stop_after_delay", "stop_before_delay", "stop_all", "stop_any", "stop_never", "stop_when_event_set", "wait_chain", "wait_combine", "wait_exponential", "wait_fixed", "wait_incrementing", "wait_none", "wait_random", "wait_random_exponential", "wait_full_jitter", "wait_exponential_jitter", "before_log", "before_nothing", "after_log", "after_nothing", "before_sleep_log", "before_sleep_nothing", "retry", "WrappedFn", "TryAgain", "NO_RESULT", "DoAttempt", "DoSleep", "BaseAction", "RetryAction", "RetryError", "AttemptManager", "BaseRetrying", "Retrying", "Future", "RetryCallState", "AsyncRetrying", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/_utils.py0000644000175100001770000000554414651703440016452 0ustar00runnerdocker# 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 functools import inspect import sys import typing from datetime import timedelta # sys.maxsize: # An integer giving the maximum value a variable of type Py_ssize_t can take. MAX_WAIT = sys.maxsize / 2 def find_ordinal(pos_num: int) -> str: # 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 4 <= pos_num <= 20: return "th" else: return find_ordinal(pos_num % 10) def to_ordinal(pos_num: int) -> str: return f"{pos_num}{find_ordinal(pos_num)}" def get_callback_name(cb: typing.Callable[..., typing.Any]) -> str: """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__) 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) time_unit_type = typing.Union[int, float, timedelta] def to_seconds(time_unit: time_unit_type) -> float: return float( time_unit.total_seconds() if isinstance(time_unit, timedelta) else time_unit ) def is_coroutine_callable(call: typing.Callable[..., typing.Any]) -> bool: if inspect.isclass(call): return False if inspect.iscoroutinefunction(call): return True partial_call = isinstance(call, functools.partial) and call.func dunder_call = partial_call or getattr(call, "__call__", None) return inspect.iscoroutinefunction(dunder_call) def wrap_to_async_func( call: typing.Callable[..., typing.Any], ) -> typing.Callable[..., typing.Awaitable[typing.Any]]: if is_coroutine_callable(call): return call async def inner(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: return call(*args, **kwargs) return inner ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/after.py0000644000175100001770000000317214651703440016247 0ustar00runnerdocker# 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 typing from tenacity import _utils if typing.TYPE_CHECKING: import logging from tenacity import RetryCallState def after_nothing(retry_state: "RetryCallState") -> None: """After call strategy that does nothing.""" def after_log( logger: "logging.Logger", log_level: int, sec_format: str = "%0.3f", ) -> typing.Callable[["RetryCallState"], None]: """After call strategy that logs to some logger the finished attempt.""" def log_it(retry_state: "RetryCallState") -> None: if retry_state.fn is None: # NOTE(sileht): can't really happen, but we must please mypy fn_name = "" else: fn_name = _utils.get_callback_name(retry_state.fn) logger.log( log_level, f"Finished call to '{fn_name}' " f"after {sec_format % retry_state.seconds_since_start}(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) return log_it ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1722255145.1237128 tenacity-9.0.0/tenacity/asyncio/0000755000175100001770000000000014651703451016240 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/asyncio/__init__.py0000644000175100001770000001713514651703440020356 0ustar00runnerdocker# 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 functools import sys import typing as t import tenacity from tenacity import AttemptManager from tenacity import BaseRetrying from tenacity import DoAttempt from tenacity import DoSleep from tenacity import RetryCallState from tenacity import RetryError from tenacity import after_nothing from tenacity import before_nothing from tenacity import _utils # Import all built-in retry strategies for easier usage. from .retry import RetryBaseT from .retry import retry_all # noqa from .retry import retry_any # noqa from .retry import retry_if_exception # noqa from .retry import retry_if_result # noqa from ..retry import RetryBaseT as SyncRetryBaseT if t.TYPE_CHECKING: from tenacity.stop import StopBaseT from tenacity.wait import WaitBaseT WrappedFnReturnT = t.TypeVar("WrappedFnReturnT") WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]]) def _portable_async_sleep(seconds: float) -> t.Awaitable[None]: # If trio is already imported, then importing it is cheap. # If trio isn't already imported, then it's definitely not running, so we # can skip further checks. if "trio" in sys.modules: # If trio is available, then sniffio is too import trio import sniffio if sniffio.current_async_library() == "trio": return trio.sleep(seconds) # Otherwise, assume asyncio # Lazy import asyncio as it's expensive (responsible for 25-50% of total import overhead). import asyncio return asyncio.sleep(seconds) class AsyncRetrying(BaseRetrying): def __init__( self, sleep: t.Callable[ [t.Union[int, float]], t.Union[None, t.Awaitable[None]] ] = _portable_async_sleep, stop: "StopBaseT" = tenacity.stop.stop_never, wait: "WaitBaseT" = tenacity.wait.wait_none(), retry: "t.Union[SyncRetryBaseT, RetryBaseT]" = tenacity.retry_if_exception_type(), before: t.Callable[ ["RetryCallState"], t.Union[None, t.Awaitable[None]] ] = before_nothing, after: t.Callable[ ["RetryCallState"], t.Union[None, t.Awaitable[None]] ] = after_nothing, before_sleep: t.Optional[ t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] ] = None, reraise: bool = False, retry_error_cls: t.Type["RetryError"] = RetryError, retry_error_callback: t.Optional[ t.Callable[["RetryCallState"], t.Union[t.Any, t.Awaitable[t.Any]]] ] = None, ) -> None: super().__init__( sleep=sleep, # type: ignore[arg-type] stop=stop, wait=wait, retry=retry, # type: ignore[arg-type] before=before, # type: ignore[arg-type] after=after, # type: ignore[arg-type] before_sleep=before_sleep, # type: ignore[arg-type] reraise=reraise, retry_error_cls=retry_error_cls, retry_error_callback=retry_error_callback, ) async def __call__( # type: ignore[override] self, fn: WrappedFn, *args: t.Any, **kwargs: t.Any ) -> WrappedFnReturnT: self.begin() retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs) while True: do = await self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): try: result = await fn(*args, **kwargs) except BaseException: # noqa: B902 retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type] else: retry_state.set_result(result) elif isinstance(do, DoSleep): retry_state.prepare_for_next_attempt() await self.sleep(do) # type: ignore[misc] else: return do # type: ignore[no-any-return] def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None: self.iter_state.actions.append(_utils.wrap_to_async_func(fn)) async def _run_retry(self, retry_state: "RetryCallState") -> None: # type: ignore[override] self.iter_state.retry_run_result = await _utils.wrap_to_async_func(self.retry)( retry_state ) async def _run_wait(self, retry_state: "RetryCallState") -> None: # type: ignore[override] if self.wait: sleep = await _utils.wrap_to_async_func(self.wait)(retry_state) else: sleep = 0.0 retry_state.upcoming_sleep = sleep async def _run_stop(self, retry_state: "RetryCallState") -> None: # type: ignore[override] self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start self.iter_state.stop_run_result = await _utils.wrap_to_async_func(self.stop)( retry_state ) async def iter( self, retry_state: "RetryCallState" ) -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa: A003 self._begin_iter(retry_state) result = None for action in self.iter_state.actions: result = await action(retry_state) return result def __iter__(self) -> t.Generator[AttemptManager, None, None]: raise TypeError("AsyncRetrying object is not iterable") def __aiter__(self) -> "AsyncRetrying": self.begin() self._retry_state = RetryCallState(self, fn=None, args=(), kwargs={}) return self async def __anext__(self) -> AttemptManager: while True: do = await 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) # type: ignore[misc] else: raise StopAsyncIteration def wraps(self, fn: WrappedFn) -> WrappedFn: wrapped = super().wraps(fn) # Ensure wrapper is recognized as a coroutine function. @functools.wraps( fn, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__") ) async def async_wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any: # Always create a copy to prevent overwriting the local contexts when # calling the same wrapped functions multiple times in the same stack copy = self.copy() async_wrapped.statistics = copy.statistics # type: ignore[attr-defined] return await copy(fn, *args, **kwargs) # Preserve attributes async_wrapped.retry = self # type: ignore[attr-defined] async_wrapped.retry_with = wrapped.retry_with # type: ignore[attr-defined] async_wrapped.statistics = {} # type: ignore[attr-defined] return async_wrapped # type: ignore[return-value] __all__ = [ "retry_all", "retry_any", "retry_if_exception", "retry_if_result", "WrappedFn", "AsyncRetrying", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/asyncio/retry.py0000644000175100001770000001022414651703440017754 0ustar00runnerdocker# Copyright 2016–2021 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 typing from tenacity import _utils from tenacity import retry_base if typing.TYPE_CHECKING: from tenacity import RetryCallState class async_retry_base(retry_base): """Abstract base class for async retry strategies.""" @abc.abstractmethod async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override] pass def __and__( # type: ignore[override] self, other: "typing.Union[retry_base, async_retry_base]" ) -> "retry_all": return retry_all(self, other) def __rand__( # type: ignore[misc,override] self, other: "typing.Union[retry_base, async_retry_base]" ) -> "retry_all": return retry_all(other, self) def __or__( # type: ignore[override] self, other: "typing.Union[retry_base, async_retry_base]" ) -> "retry_any": return retry_any(self, other) def __ror__( # type: ignore[misc,override] self, other: "typing.Union[retry_base, async_retry_base]" ) -> "retry_any": return retry_any(other, self) RetryBaseT = typing.Union[ async_retry_base, typing.Callable[["RetryCallState"], typing.Awaitable[bool]] ] class retry_if_exception(async_retry_base): """Retry strategy that retries if an exception verifies a predicate.""" def __init__( self, predicate: typing.Callable[[BaseException], typing.Awaitable[bool]] ) -> None: self.predicate = predicate async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override] if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") if retry_state.outcome.failed: exception = retry_state.outcome.exception() if exception is None: raise RuntimeError("outcome failed but the exception is None") return await self.predicate(exception) else: return False class retry_if_result(async_retry_base): """Retries if the result verifies a predicate.""" def __init__( self, predicate: typing.Callable[[typing.Any], typing.Awaitable[bool]] ) -> None: self.predicate = predicate async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override] if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") if not retry_state.outcome.failed: return await self.predicate(retry_state.outcome.result()) else: return False class retry_any(async_retry_base): """Retries if any of the retries condition is valid.""" def __init__(self, *retries: typing.Union[retry_base, async_retry_base]) -> None: self.retries = retries async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override] result = False for r in self.retries: result = result or await _utils.wrap_to_async_func(r)(retry_state) if result: break return result class retry_all(async_retry_base): """Retries if all the retries condition are valid.""" def __init__(self, *retries: typing.Union[retry_base, async_retry_base]) -> None: self.retries = retries async def __call__(self, retry_state: "RetryCallState") -> bool: # type: ignore[override] result = True for r in self.retries: result = result and await _utils.wrap_to_async_func(r)(retry_state) if not result: break return result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/before.py0000644000175100001770000000301014651703440016377 0ustar00runnerdocker# 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 typing from tenacity import _utils if typing.TYPE_CHECKING: import logging from tenacity import RetryCallState def before_nothing(retry_state: "RetryCallState") -> None: """Before call strategy that does nothing.""" def before_log( logger: "logging.Logger", log_level: int ) -> typing.Callable[["RetryCallState"], None]: """Before call strategy that logs to some logger the attempt.""" def log_it(retry_state: "RetryCallState") -> None: if retry_state.fn is None: # NOTE(sileht): can't really happen, but we must please mypy fn_name = "" else: fn_name = _utils.get_callback_name(retry_state.fn) logger.log( log_level, f"Starting call to '{fn_name}', " f"this is the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) return log_it ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/before_sleep.py0000644000175100001770000000447014651703440017602 0ustar00runnerdocker# 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 typing from tenacity import _utils if typing.TYPE_CHECKING: import logging from tenacity import RetryCallState def before_sleep_nothing(retry_state: "RetryCallState") -> None: """Before call strategy that does nothing.""" def before_sleep_log( logger: "logging.Logger", log_level: int, exc_info: bool = False, ) -> typing.Callable[["RetryCallState"], None]: """Before call strategy that logs to some logger the attempt.""" def log_it(retry_state: "RetryCallState") -> None: local_exc_info: BaseException | bool | None if retry_state.outcome is None: raise RuntimeError("log_it() called before outcome was set") if retry_state.next_action is None: raise RuntimeError("log_it() called before next_action was set") if retry_state.outcome.failed: ex = retry_state.outcome.exception() verb, value = "raised", f"{ex.__class__.__name__}: {ex}" if exc_info: local_exc_info = retry_state.outcome.exception() 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 if retry_state.fn is None: # NOTE(sileht): can't really happen, but we must please mypy fn_name = "" else: fn_name = _utils.get_callback_name(retry_state.fn) logger.log( log_level, f"Retrying {fn_name} " f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.", exc_info=local_exc_info, ) return log_it ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/nap.py0000644000175100001770000000254714651703440015731 0ustar00runnerdocker# 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 import typing if typing.TYPE_CHECKING: import threading def sleep(seconds: float) -> None: """ 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: """Sleep strategy that waits on an event to be set.""" def __init__(self, event: "threading.Event") -> None: self.event = event def __call__(self, timeout: typing.Optional[float]) -> None: # 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=1722255136.0 tenacity-9.0.0/tenacity/py.typed0000644000175100001770000000000014651703440016256 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/retry.py0000644000175100001770000002143214651703440016312 0ustar00runnerdocker# Copyright 2016–2021 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 typing if typing.TYPE_CHECKING: from tenacity import RetryCallState class retry_base(abc.ABC): """Abstract base class for retry strategies.""" @abc.abstractmethod def __call__(self, retry_state: "RetryCallState") -> bool: pass def __and__(self, other: "retry_base") -> "retry_all": return other.__rand__(self) def __rand__(self, other: "retry_base") -> "retry_all": return retry_all(other, self) def __or__(self, other: "retry_base") -> "retry_any": return other.__ror__(self) def __ror__(self, other: "retry_base") -> "retry_any": return retry_any(other, self) RetryBaseT = typing.Union[retry_base, typing.Callable[["RetryCallState"], bool]] class _retry_never(retry_base): """Retry strategy that never rejects any result.""" def __call__(self, retry_state: "RetryCallState") -> bool: return False retry_never = _retry_never() class _retry_always(retry_base): """Retry strategy that always rejects any result.""" def __call__(self, retry_state: "RetryCallState") -> bool: 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: typing.Callable[[BaseException], bool]) -> None: self.predicate = predicate def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") if retry_state.outcome.failed: exception = retry_state.outcome.exception() if exception is None: raise RuntimeError("outcome failed but the exception is None") return self.predicate(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: typing.Union[ typing.Type[BaseException], typing.Tuple[typing.Type[BaseException], ...], ] = Exception, ) -> None: self.exception_types = exception_types super().__init__(lambda e: isinstance(e, exception_types)) class retry_if_not_exception_type(retry_if_exception): """Retries except an exception has been raised of one or more types.""" def __init__( self, exception_types: typing.Union[ typing.Type[BaseException], typing.Tuple[typing.Type[BaseException], ...], ] = Exception, ) -> None: self.exception_types = exception_types super().__init__(lambda e: not 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: typing.Union[ typing.Type[BaseException], typing.Tuple[typing.Type[BaseException], ...], ] = Exception, ) -> None: self.exception_types = exception_types super().__init__(lambda e: not isinstance(e, exception_types)) def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") # always retry if no exception was raised if not retry_state.outcome.failed: return True exception = retry_state.outcome.exception() if exception is None: raise RuntimeError("outcome failed but the exception is None") return self.predicate(exception) class retry_if_exception_cause_type(retry_base): """Retries if any of the causes of the raised exception is of one or more types. The check on the type of the cause of the exception is done recursively (until finding an exception in the chain that has no `__cause__`) """ def __init__( self, exception_types: typing.Union[ typing.Type[BaseException], typing.Tuple[typing.Type[BaseException], ...], ] = Exception, ) -> None: self.exception_cause_types = exception_types def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__ called before outcome was set") if retry_state.outcome.failed: exc = retry_state.outcome.exception() while exc is not None: if isinstance(exc.__cause__, self.exception_cause_types): return True exc = exc.__cause__ return False class retry_if_result(retry_base): """Retries if the result verifies a predicate.""" def __init__(self, predicate: typing.Callable[[typing.Any], bool]) -> None: self.predicate = predicate def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") 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: typing.Callable[[typing.Any], bool]) -> None: self.predicate = predicate def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") 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: typing.Optional[str] = None, match: typing.Optional[str] = None, ) -> None: if message and match: raise TypeError( f"{self.__class__.__name__}() takes either 'message' or 'match', not both" ) # set predicate if message: def message_fnc(exception: BaseException) -> bool: return message == str(exception) predicate = message_fnc elif match: prog = re.compile(match) def match_fnc(exception: BaseException) -> bool: return bool(prog.match(str(exception))) predicate = match_fnc else: raise TypeError( f"{self.__class__.__name__}() missing 1 required argument 'message' or 'match'" ) super().__init__(predicate) class retry_if_not_exception_message(retry_if_exception_message): """Retries until an exception message equals or matches.""" def __init__( self, message: typing.Optional[str] = None, match: typing.Optional[str] = None, ) -> None: super().__init__(message, match) # invert predicate if_predicate = self.predicate self.predicate = lambda *args_, **kwargs_: not if_predicate(*args_, **kwargs_) def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.outcome is None: raise RuntimeError("__call__() called before outcome was set") if not retry_state.outcome.failed: return True exception = retry_state.outcome.exception() if exception is None: raise RuntimeError("outcome failed but the exception is None") return self.predicate(exception) class retry_any(retry_base): """Retries if any of the retries condition is valid.""" def __init__(self, *retries: retry_base) -> None: self.retries = retries def __call__(self, retry_state: "RetryCallState") -> bool: 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: retry_base) -> None: self.retries = retries def __call__(self, retry_state: "RetryCallState") -> bool: return all(r(retry_state) for r in self.retries) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/stop.py0000644000175100001770000001002114651703440016122 0ustar00runnerdocker# Copyright 2016–2021 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 typing from tenacity import _utils if typing.TYPE_CHECKING: import threading from tenacity import RetryCallState class stop_base(abc.ABC): """Abstract base class for stop strategies.""" @abc.abstractmethod def __call__(self, retry_state: "RetryCallState") -> bool: pass def __and__(self, other: "stop_base") -> "stop_all": return stop_all(self, other) def __or__(self, other: "stop_base") -> "stop_any": return stop_any(self, other) StopBaseT = typing.Union[stop_base, typing.Callable[["RetryCallState"], bool]] class stop_any(stop_base): """Stop if any of the stop condition is valid.""" def __init__(self, *stops: stop_base) -> None: self.stops = stops def __call__(self, retry_state: "RetryCallState") -> bool: 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: stop_base) -> None: self.stops = stops def __call__(self, retry_state: "RetryCallState") -> bool: return all(x(retry_state) for x in self.stops) class _stop_never(stop_base): """Never stop.""" def __call__(self, retry_state: "RetryCallState") -> bool: return False stop_never = _stop_never() class stop_when_event_set(stop_base): """Stop when the given event is set.""" def __init__(self, event: "threading.Event") -> None: self.event = event def __call__(self, retry_state: "RetryCallState") -> bool: return self.event.is_set() class stop_after_attempt(stop_base): """Stop when the previous attempt >= max_attempt.""" def __init__(self, max_attempt_number: int) -> None: self.max_attempt_number = max_attempt_number def __call__(self, retry_state: "RetryCallState") -> bool: return retry_state.attempt_number >= self.max_attempt_number class stop_after_delay(stop_base): """ Stop when the time from the first attempt >= limit. Note: `max_delay` will be exceeded, so when used with a `wait`, the actual total delay will be greater than `max_delay` by some of the final sleep period before `max_delay` is exceeded. If you need stricter timing with waits, consider `stop_before_delay` instead. """ def __init__(self, max_delay: _utils.time_unit_type) -> None: self.max_delay = _utils.to_seconds(max_delay) def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.seconds_since_start is None: raise RuntimeError("__call__() called but seconds_since_start is not set") return retry_state.seconds_since_start >= self.max_delay class stop_before_delay(stop_base): """ Stop right before the next attempt would take place after the time from the first attempt >= limit. Most useful when you are using with a `wait` function like wait_random_exponential, but need to make sure that the max_delay is not exceeded. """ def __init__(self, max_delay: _utils.time_unit_type) -> None: self.max_delay = _utils.to_seconds(max_delay) def __call__(self, retry_state: "RetryCallState") -> bool: if retry_state.seconds_since_start is None: raise RuntimeError("__call__() called but seconds_since_start is not set") return ( retry_state.seconds_since_start + retry_state.upcoming_sleep >= self.max_delay ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tenacity/tornadoweb.py0000644000175100001770000000411514651703440017310 0ustar00runnerdocker# 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 import typing from tenacity import BaseRetrying from tenacity import DoAttempt from tenacity import DoSleep from tenacity import RetryCallState from tornado import gen if typing.TYPE_CHECKING: from tornado.concurrent import Future _RetValT = typing.TypeVar("_RetValT") class TornadoRetrying(BaseRetrying): def __init__( self, sleep: "typing.Callable[[float], Future[None]]" = gen.sleep, **kwargs: typing.Any, ) -> None: super().__init__(**kwargs) self.sleep = sleep @gen.coroutine # type: ignore[misc] def __call__( self, fn: "typing.Callable[..., typing.Union[typing.Generator[typing.Any, typing.Any, _RetValT], Future[_RetValT]]]", *args: typing.Any, **kwargs: typing.Any, ) -> "typing.Generator[typing.Any, typing.Any, _RetValT]": self.begin() 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: # noqa: B902 retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type] 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=1722255136.0 tenacity-9.0.0/tenacity/wait.py0000644000175100001770000001756114651703440016121 0ustar00runnerdocker# Copyright 2016–2021 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 typing from tenacity import _utils if typing.TYPE_CHECKING: from tenacity import RetryCallState class wait_base(abc.ABC): """Abstract base class for wait strategies.""" @abc.abstractmethod def __call__(self, retry_state: "RetryCallState") -> float: pass def __add__(self, other: "wait_base") -> "wait_combine": return wait_combine(self, other) def __radd__(self, other: "wait_base") -> typing.Union["wait_combine", "wait_base"]: # make it possible to use multiple waits with the built-in sum function if other == 0: # type: ignore[comparison-overlap] return self return self.__add__(other) WaitBaseT = typing.Union[ wait_base, typing.Callable[["RetryCallState"], typing.Union[float, int]] ] class wait_fixed(wait_base): """Wait strategy that waits a fixed amount of time between each retry.""" def __init__(self, wait: _utils.time_unit_type) -> None: self.wait_fixed = _utils.to_seconds(wait) def __call__(self, retry_state: "RetryCallState") -> float: return self.wait_fixed class wait_none(wait_fixed): """Wait strategy that doesn't wait at all before retrying.""" def __init__(self) -> None: super().__init__(0) class wait_random(wait_base): """Wait strategy that waits a random amount of time between min/max.""" def __init__( self, min: _utils.time_unit_type = 0, max: _utils.time_unit_type = 1 ) -> None: # noqa self.wait_random_min = _utils.to_seconds(min) self.wait_random_max = _utils.to_seconds(max) def __call__(self, retry_state: "RetryCallState") -> float: 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: wait_base) -> None: self.wait_funcs = strategies def __call__(self, retry_state: "RetryCallState") -> float: 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: wait_base) -> None: self.strategies = strategies def __call__(self, retry_state: "RetryCallState") -> float: 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: _utils.time_unit_type = 0, increment: _utils.time_unit_type = 100, max: _utils.time_unit_type = _utils.MAX_WAIT, # noqa ) -> None: self.start = _utils.to_seconds(start) self.increment = _utils.to_seconds(increment) self.max = _utils.to_seconds(max) def __call__(self, retry_state: "RetryCallState") -> float: 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: typing.Union[int, float] = 1, max: _utils.time_unit_type = _utils.MAX_WAIT, # noqa exp_base: typing.Union[int, float] = 2, min: _utils.time_unit_type = 0, # noqa ) -> None: self.multiplier = multiplier self.min = _utils.to_seconds(min) self.max = _utils.to_seconds(max) self.exp_base = exp_base def __call__(self, retry_state: "RetryCallState") -> float: 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. """ def __call__(self, retry_state: "RetryCallState") -> float: high = super().__call__(retry_state=retry_state) return random.uniform(self.min, high) class wait_exponential_jitter(wait_base): """Wait strategy that applies exponential backoff and jitter. It allows for a customized initial wait, maximum wait and jitter. This implements the strategy described here: https://cloud.google.com/storage/docs/retry-strategy The wait time is min(initial * 2**n + random.uniform(0, jitter), maximum) where n is the retry count. """ def __init__( self, initial: float = 1, max: float = _utils.MAX_WAIT, # noqa exp_base: float = 2, jitter: float = 1, ) -> None: self.initial = initial self.max = max self.exp_base = exp_base self.jitter = jitter def __call__(self, retry_state: "RetryCallState") -> float: jitter = random.uniform(0, self.jitter) try: exp = self.exp_base ** (retry_state.attempt_number - 1) result = self.initial * exp + jitter except OverflowError: result = self.max return max(0, min(result, self.max)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1722255145.1237128 tenacity-9.0.0/tenacity.egg-info/0000755000175100001770000000000014651703451016265 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255145.0 tenacity-9.0.0/tenacity.egg-info/PKG-INFO0000644000175100001770000000166514651703451017372 0ustar00runnerdockerMetadata-Version: 2.1 Name: tenacity Version: 9.0.0 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 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Topic :: Utilities Requires-Python: >=3.8 Provides-Extra: doc Provides-Extra: test License-File: LICENSE Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255145.0 tenacity-9.0.0/tenacity.egg-info/SOURCES.txt0000644000175100001770000000643514651703451020161 0ustar00runnerdocker.editorconfig .gitignore .mergify.yml .readthedocs.yml LICENSE README.rst pyproject.toml reno.yaml setup.cfg setup.py tox.ini .github/dependabot.yml .github/workflows/ci.yaml .github/workflows/deploy.yaml doc/source/api.rst doc/source/changelog.rst doc/source/conf.py doc/source/index.rst releasenotes/notes/Fix-tests-for-typeguard-3.x-6eebfea546b6207e.yaml releasenotes/notes/Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml releasenotes/notes/add-async-actions-b249c527d99723bb.yaml releasenotes/notes/add-reno-d1ab5710f272650a.yaml releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml releasenotes/notes/add-test-extra-55e869261b03e56d.yaml releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml releasenotes/notes/added_a_link_to_documentation-eefaf8f074b539f8.yaml releasenotes/notes/after_log-50f4d73b24ce9203.yaml releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml releasenotes/notes/annotate_code-197b93130df14042.yaml releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml releasenotes/notes/dependabot-for-github-actions-4d2464f3c0928463.yaml releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml releasenotes/notes/fix-retry-wrapper-attributes-f7a3a45b8e90f257.yaml releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml releasenotes/notes/no-async-iter-6132a42e52348a75.yaml releasenotes/notes/pr320-py3-only-wheel-tag.yaml releasenotes/notes/py36_plus-c425fb3aa17c6682.yaml releasenotes/notes/remove-py36-876c0416cf279d15.yaml releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml releasenotes/notes/trio-support-retry-22bd544800cd1f36.yaml releasenotes/notes/wait-random-exponential-min-2a4b7eed9f002436.yaml releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml tenacity/__init__.py tenacity/_utils.py tenacity/after.py tenacity/before.py tenacity/before_sleep.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/requires.txt tenacity.egg-info/top_level.txt tenacity/asyncio/__init__.py tenacity/asyncio/retry.py tests/__init__.py tests/test_after.py tests/test_asyncio.py tests/test_issue_478.py tests/test_tenacity.py tests/test_tornado.py tests/test_utils.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255145.0 tenacity-9.0.0/tenacity.egg-info/dependency_links.txt0000644000175100001770000000000114651703451022333 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255145.0 tenacity-9.0.0/tenacity.egg-info/requires.txt0000644000175100001770000000007114651703451020663 0ustar00runnerdocker [doc] reno sphinx [test] pytest tornado>=4.5 typeguard ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255145.0 tenacity-9.0.0/tenacity.egg-info/top_level.txt0000644000175100001770000000001114651703451021007 0ustar00runnerdockertenacity ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1722255145.1237128 tenacity-9.0.0/tests/0000755000175100001770000000000014651703451014115 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tests/__init__.py0000644000175100001770000000000014651703440016212 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tests/test_after.py0000644000175100001770000000477414651703440016641 0ustar00runnerdocker# mypy: disable-error-code="no-untyped-def,no-untyped-call" import logging import random import unittest.mock from tenacity import _utils # noqa from tenacity import after_log from . import test_tenacity class TestAfterLogFormat(unittest.TestCase): def setUp(self) -> None: self.log_level = random.choice( ( logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL, ) ) self.previous_attempt_number = random.randint(1, 512) def test_01_default(self): """Test log formatting.""" log = unittest.mock.MagicMock(spec="logging.Logger.log") logger = unittest.mock.MagicMock(spec="logging.Logger", log=log) sec_format = "%0.3f" delay_since_first_attempt = 0.1 retry_state = test_tenacity.make_retry_state( self.previous_attempt_number, delay_since_first_attempt ) fun = after_log( logger=logger, log_level=self.log_level ) # use default sec_format fun(retry_state) fn_name = ( "" if retry_state.fn is None else _utils.get_callback_name(retry_state.fn) ) log.assert_called_once_with( self.log_level, f"Finished call to '{fn_name}' " f"after {sec_format % retry_state.seconds_since_start}(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) def test_02_custom_sec_format(self): """Test log formatting with custom int format..""" log = unittest.mock.MagicMock(spec="logging.Logger.log") logger = unittest.mock.MagicMock(spec="logging.Logger", log=log) sec_format = "%.1f" delay_since_first_attempt = 0.1 retry_state = test_tenacity.make_retry_state( self.previous_attempt_number, delay_since_first_attempt ) fun = after_log(logger=logger, log_level=self.log_level, sec_format=sec_format) fun(retry_state) fn_name = ( "" if retry_state.fn is None else _utils.get_callback_name(retry_state.fn) ) log.assert_called_once_with( self.log_level, f"Finished call to '{fn_name}' " f"after {sec_format % retry_state.seconds_since_start}(s), " f"this was the {_utils.to_ordinal(retry_state.attempt_number)} time calling it.", ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tests/test_asyncio.py0000644000175100001770000003357114651703440017202 0ustar00runnerdocker# mypy: disable-error-code="no-untyped-def,no-untyped-call" # 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 from functools import wraps from unittest import mock try: import trio except ImportError: have_trio = False else: have_trio = True import pytest import tenacity from tenacity import AsyncRetrying, RetryError from tenacity import asyncio as tasyncio from tenacity import retry, retry_if_exception, retry_if_result, stop_after_attempt from tenacity.wait import wait_fixed from .test_tenacity import NoIOErrorAfterCount, current_time_ms def asynctest(callable_): @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) return thing.go() class TestAsyncio(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_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") def test_retry_preserves_argument_defaults(self): async def function_with_defaults(a=1): return a async def function_with_kwdefaults(*, a=1): return a retrying = AsyncRetrying( wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3) ) wrapped_defaults_function = retrying.wraps(function_with_defaults) wrapped_kwdefaults_function = retrying.wraps(function_with_kwdefaults) self.assertEqual( function_with_defaults.__defaults__, wrapped_defaults_function.__defaults__ ) self.assertEqual( function_with_kwdefaults.__kwdefaults__, wrapped_kwdefaults_function.__kwdefaults__, ) @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), # type: ignore[attr-defined] _retryable_coroutine.retry_with(after=after)(thing2), # type: ignore[attr-defined] ) # 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] @unittest.skipIf(not have_trio, "trio not installed") class TestTrio(unittest.TestCase): def test_trio_basic(self): thing = NoIOErrorAfterCount(5) @retry async def trio_function(): await trio.sleep(0.00001) return thing.go() trio.run(trio_function) assert thing.counter == thing.count 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) @asynctest async def test_retry_with_result(self): async def test(): attempts = 0 # mypy doesn't have great lambda support def lt_3(x: float) -> bool: return x < 3 async for attempt in tasyncio.AsyncRetrying(retry=retry_if_result(lt_3)): with attempt: attempts += 1 attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(3, result) @asynctest async def test_retry_with_async_result(self): async def test(): attempts = 0 async def lt_3(x: float) -> bool: return x < 3 async for attempt in tasyncio.AsyncRetrying( retry=tasyncio.retry_if_result(lt_3) ): with attempt: attempts += 1 assert attempt.retry_state.outcome # help mypy if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(3, result) @asynctest async def test_retry_with_async_exc(self): async def test(): attempts = 0 class CustomException(Exception): pass async def is_exc(e: BaseException) -> bool: return isinstance(e, CustomException) async for attempt in tasyncio.AsyncRetrying( retry=tasyncio.retry_if_exception(is_exc) ): with attempt: attempts += 1 if attempts < 3: raise CustomException() assert attempt.retry_state.outcome # help mypy if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(3, result) @asynctest async def test_retry_with_async_result_or(self): async def test(): attempts = 0 async def lt_3(x: float) -> bool: return x < 3 class CustomException(Exception): pass def is_exc(e: BaseException) -> bool: return isinstance(e, CustomException) retry_strategy = tasyncio.retry_if_result(lt_3) | retry_if_exception(is_exc) async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy): with attempt: attempts += 1 if 2 < attempts < 4: raise CustomException() assert attempt.retry_state.outcome # help mypy if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(4, result) @asynctest async def test_retry_with_async_result_ror(self): async def test(): attempts = 0 def lt_3(x: float) -> bool: return x < 3 class CustomException(Exception): pass async def is_exc(e: BaseException) -> bool: return isinstance(e, CustomException) retry_strategy = retry_if_result(lt_3) | tasyncio.retry_if_exception(is_exc) async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy): with attempt: attempts += 1 if 2 < attempts < 4: raise CustomException() assert attempt.retry_state.outcome # help mypy if not attempt.retry_state.outcome.failed: attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(4, result) @asynctest async def test_retry_with_async_result_and(self): async def test(): attempts = 0 async def lt_3(x: float) -> bool: return x < 3 def gt_0(x: float) -> bool: return x > 0 retry_strategy = tasyncio.retry_if_result(lt_3) & retry_if_result(gt_0) async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy): with attempt: attempts += 1 attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(3, result) @asynctest async def test_retry_with_async_result_rand(self): async def test(): attempts = 0 async def lt_3(x: float) -> bool: return x < 3 def gt_0(x: float) -> bool: return x > 0 retry_strategy = retry_if_result(gt_0) & tasyncio.retry_if_result(lt_3) async for attempt in tasyncio.AsyncRetrying(retry=retry_strategy): with attempt: attempts += 1 attempt.retry_state.set_result(attempts) return attempts result = await test() self.assertEqual(3, result) @asynctest async def test_async_retying_iterator(self): thing = NoIOErrorAfterCount(5) with pytest.raises(TypeError): for attempts in AsyncRetrying(): with attempts: await _async_function(thing) class TestDecoratorWrapper(unittest.TestCase): @asynctest async def test_retry_function_attributes(self): """Test that the wrapped function attributes are exposed as intended. - statistics contains the value for the latest function run - retry object can be modified to change its behaviour (useful to patch in tests) - retry object statistics do not contain valid information """ self.assertTrue( await _retryable_coroutine_with_2_attempts(NoIOErrorAfterCount(1)) ) expected_stats = { "attempt_number": 2, "delay_since_first_attempt": mock.ANY, "idle_for": mock.ANY, "start_time": mock.ANY, } self.assertEqual( _retryable_coroutine_with_2_attempts.statistics, # type: ignore[attr-defined] expected_stats, ) self.assertEqual( _retryable_coroutine_with_2_attempts.retry.statistics, # type: ignore[attr-defined] {}, ) with mock.patch.object( _retryable_coroutine_with_2_attempts.retry, # type: ignore[attr-defined] "stop", tenacity.stop_after_attempt(1), ): try: self.assertTrue( await _retryable_coroutine_with_2_attempts(NoIOErrorAfterCount(2)) ) except RetryError as exc: expected_stats = { "attempt_number": 1, "delay_since_first_attempt": mock.ANY, "idle_for": mock.ANY, "start_time": mock.ANY, } self.assertEqual( _retryable_coroutine_with_2_attempts.statistics, # type: ignore[attr-defined] expected_stats, ) self.assertEqual(exc.last_attempt.attempt_number, 1) self.assertEqual( _retryable_coroutine_with_2_attempts.retry.statistics, # type: ignore[attr-defined] {}, ) else: self.fail("RetryError should have been raised after 1 attempt") # make sure mypy accepts passing an async sleep function # https://github.com/jd/tenacity/issues/399 async def my_async_sleep(x: float) -> None: await asyncio.sleep(x) @retry(sleep=my_async_sleep) async def foo(): pass if __name__ == "__main__": unittest.main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tests/test_issue_478.py0000644000175100001770000000626614651703440017270 0ustar00runnerdockerimport asyncio import typing import unittest from functools import wraps from tenacity import RetryCallState, retry def asynctest( callable_: typing.Callable[..., typing.Any], ) -> typing.Callable[..., typing.Any]: @wraps(callable_) def wrapper(*a: typing.Any, **kw: typing.Any) -> typing.Any: loop = asyncio.get_event_loop() return loop.run_until_complete(callable_(*a, **kw)) return wrapper MAX_RETRY_FIX_ATTEMPTS = 2 class TestIssue478(unittest.TestCase): def test_issue(self) -> None: results = [] def do_retry(retry_state: RetryCallState) -> bool: outcome = retry_state.outcome assert outcome ex = outcome.exception() _subject_: str = retry_state.args[0] if _subject_ == "Fix": # no retry on fix failure return False if retry_state.attempt_number >= MAX_RETRY_FIX_ATTEMPTS: return False if ex: do_fix_work() return True return False @retry(reraise=True, retry=do_retry) def _do_work(subject: str) -> None: if subject == "Error": results.append(f"{subject} is not working") raise Exception(f"{subject} is not working") results.append(f"{subject} is working") def do_any_work(subject: str) -> None: _do_work(subject) def do_fix_work() -> None: _do_work("Fix") try: do_any_work("Error") except Exception as exc: assert str(exc) == "Error is not working" else: assert False, "No exception caught" assert results == [ "Error is not working", "Fix is working", "Error is not working", ] @asynctest async def test_async(self) -> None: results = [] async def do_retry(retry_state: RetryCallState) -> bool: outcome = retry_state.outcome assert outcome ex = outcome.exception() _subject_: str = retry_state.args[0] if _subject_ == "Fix": # no retry on fix failure return False if retry_state.attempt_number >= MAX_RETRY_FIX_ATTEMPTS: return False if ex: await do_fix_work() return True return False @retry(reraise=True, retry=do_retry) async def _do_work(subject: str) -> None: if subject == "Error": results.append(f"{subject} is not working") raise Exception(f"{subject} is not working") results.append(f"{subject} is working") async def do_any_work(subject: str) -> None: await _do_work(subject) async def do_fix_work() -> None: await _do_work("Fix") try: await do_any_work("Error") except Exception as exc: assert str(exc) == "Error is not working" else: assert False, "No exception caught" assert results == [ "Error is not working", "Fix is working", "Error is not working", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tests/test_tenacity.py0000644000175100001770000016661514651703440017363 0ustar00runnerdocker# mypy: disable_error_code="no-untyped-def,no-untyped-call,attr-defined,arg-type,no-any-return,list-item,var-annotated,import,call-overload" # Copyright 2016–2021 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 datetime import logging import re import sys import time import typing import unittest import warnings from contextlib import contextmanager from copy import copy from fractions import Fraction from unittest import mock import pytest import tenacity from tenacity import RetryCallState, RetryError, Retrying, retry _unset = object() def _make_unset_exception(func_name, **kwargs): missing = [] for k, v in kwargs.items(): 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, upcoming_sleep=0, ): """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, ) 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) retry_state.upcoming_sleep = upcoming_sleep _set_delay_since_start(retry_state, delay_since_first_attempt) return retry_state class TestBase(unittest.TestCase): def test_retrying_repr(self): class ConcreteRetrying(tenacity.BaseRetrying): def __call__(self, fn, *args, **kwargs): pass repr(ConcreteRetrying()) def test_callstate_repr(self): rs = RetryCallState(None, None, (), {}) rs.idle_for = 1.1111111 assert repr(rs).endswith("attempt #1; slept for 1.11; last result: none yet>") rs = make_retry_state(2, 5) assert repr(rs).endswith( "attempt #2; slept for 0.0; last result: returned None>" ) rs = make_retry_state( 0, 0, last_result=tenacity.Future.construct(1, ValueError("aaa"), True) ) assert repr(rs).endswith( "attempt #0; slept for 0.0; last result: failed (ValueError aaa)>" ) 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): for delay in (1, datetime.timedelta(seconds=1)): with self.subTest(): r = Retrying(stop=tenacity.stop_after_delay(delay)) 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_stop_before_delay(self): for delay in (1, datetime.timedelta(seconds=1)): with self.subTest(): r = Retrying(stop=tenacity.stop_before_delay(delay)) self.assertFalse( r.stop(make_retry_state(2, 0.999, upcoming_sleep=0.0001)) ) self.assertTrue(r.stop(make_retry_state(2, 1, upcoming_sleep=0.001))) self.assertTrue(r.stop(make_retry_state(2, 1, upcoming_sleep=1))) # It should act the same as stop_after_delay if upcoming sleep is 0 self.assertFalse(r.stop(make_retry_state(2, 0.999, upcoming_sleep=0))) self.assertTrue(r.stop(make_retry_state(2, 1, upcoming_sleep=0))) self.assertTrue(r.stop(make_retry_state(2, 1.001, upcoming_sleep=0))) def test_legacy_explicit_stop_type(self): Retrying(stop="stop_after_attempt") 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(make_retry_state(18, 9879))) def test_fixed_sleep(self): for wait in (1, datetime.timedelta(seconds=1)): with self.subTest(): r = Retrying(wait=tenacity.wait_fixed(wait)) self.assertEqual(1, r.wait(make_retry_state(12, 6546))) def test_incrementing_sleep(self): for start, increment in ( (500, 100), (datetime.timedelta(seconds=500), datetime.timedelta(seconds=100)), ): with self.subTest(): r = Retrying( wait=tenacity.wait_incrementing(start=start, increment=increment) ) self.assertEqual(500, r.wait(make_retry_state(1, 6546))) self.assertEqual(600, r.wait(make_retry_state(2, 6546))) self.assertEqual(700, r.wait(make_retry_state(3, 6546))) def test_random_sleep(self): for min_, max_ in ( (1, 20), (datetime.timedelta(seconds=1), datetime.timedelta(seconds=20)), ): with self.subTest(): r = Retrying(wait=tenacity.wait_random(min=min_, max=max_)) times = set() for _ in range(1000): times.add(r.wait(make_retry_state(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_withoutmin_(self): r = Retrying(wait=tenacity.wait_random(max=2)) times = set() times.add(r.wait(make_retry_state(1, 6546))) times.add(r.wait(make_retry_state(1, 6546))) times.add(r.wait(make_retry_state(1, 6546))) times.add(r.wait(make_retry_state(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(make_retry_state(1, 0)), 1) self.assertEqual(r.wait(make_retry_state(2, 0)), 2) self.assertEqual(r.wait(make_retry_state(3, 0)), 4) self.assertEqual(r.wait(make_retry_state(4, 0)), 8) self.assertEqual(r.wait(make_retry_state(5, 0)), 16) self.assertEqual(r.wait(make_retry_state(6, 0)), 32) self.assertEqual(r.wait(make_retry_state(7, 0)), 64) self.assertEqual(r.wait(make_retry_state(8, 0)), 128) def test_exponential_with_max_wait(self): r = Retrying(wait=tenacity.wait_exponential(max=40)) self.assertEqual(r.wait(make_retry_state(1, 0)), 1) self.assertEqual(r.wait(make_retry_state(2, 0)), 2) self.assertEqual(r.wait(make_retry_state(3, 0)), 4) self.assertEqual(r.wait(make_retry_state(4, 0)), 8) self.assertEqual(r.wait(make_retry_state(5, 0)), 16) self.assertEqual(r.wait(make_retry_state(6, 0)), 32) self.assertEqual(r.wait(make_retry_state(7, 0)), 40) self.assertEqual(r.wait(make_retry_state(8, 0)), 40) self.assertEqual(r.wait(make_retry_state(50, 0)), 40) def test_exponential_with_min_wait(self): r = Retrying(wait=tenacity.wait_exponential(min=20)) self.assertEqual(r.wait(make_retry_state(1, 0)), 20) self.assertEqual(r.wait(make_retry_state(2, 0)), 20) self.assertEqual(r.wait(make_retry_state(3, 0)), 20) self.assertEqual(r.wait(make_retry_state(4, 0)), 20) self.assertEqual(r.wait(make_retry_state(5, 0)), 20) self.assertEqual(r.wait(make_retry_state(6, 0)), 32) self.assertEqual(r.wait(make_retry_state(7, 0)), 64) self.assertEqual(r.wait(make_retry_state(8, 0)), 128) self.assertEqual(r.wait(make_retry_state(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(make_retry_state(1, 0)), 1) self.assertEqual(r.wait(make_retry_state(2, 0)), 2) self.assertEqual(r.wait(make_retry_state(3, 0)), 4) self.assertEqual(r.wait(make_retry_state(4, 0)), 8) self.assertEqual(r.wait(make_retry_state(5, 0)), 16) self.assertEqual(r.wait(make_retry_state(6, 0)), 32) self.assertEqual(r.wait(make_retry_state(7, 0)), 50) self.assertEqual(r.wait(make_retry_state(8, 0)), 50) self.assertEqual(r.wait(make_retry_state(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(make_retry_state(1, 0)), 20) self.assertEqual(r.wait(make_retry_state(2, 0)), 20) self.assertEqual(r.wait(make_retry_state(3, 0)), 20) self.assertEqual(r.wait(make_retry_state(4, 0)), 20) self.assertEqual(r.wait(make_retry_state(5, 0)), 32) self.assertEqual(r.wait(make_retry_state(6, 0)), 64) self.assertEqual(r.wait(make_retry_state(7, 0)), 128) self.assertEqual(r.wait(make_retry_state(8, 0)), 256) self.assertEqual(r.wait(make_retry_state(20, 0)), 1048576) def test_exponential_with_min_wait_andmax__wait(self): for min_, max_ in ( (10, 100), (datetime.timedelta(seconds=10), datetime.timedelta(seconds=100)), ): with self.subTest(): r = Retrying(wait=tenacity.wait_exponential(min=min_, max=max_)) self.assertEqual(r.wait(make_retry_state(1, 0)), 10) self.assertEqual(r.wait(make_retry_state(2, 0)), 10) self.assertEqual(r.wait(make_retry_state(3, 0)), 10) self.assertEqual(r.wait(make_retry_state(4, 0)), 10) self.assertEqual(r.wait(make_retry_state(5, 0)), 16) self.assertEqual(r.wait(make_retry_state(6, 0)), 32) self.assertEqual(r.wait(make_retry_state(7, 0)), 64) self.assertEqual(r.wait(make_retry_state(8, 0)), 100) self.assertEqual(r.wait(make_retry_state(9, 0)), 100) self.assertEqual(r.wait(make_retry_state(20, 0)), 100) def test_legacy_explicit_wait_type(self): Retrying(wait="exponential_sleep") 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 range(1000): w = r.wait(make_retry_state(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 range(1000): w = r.wait(make_retry_state(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 range(1000): w = r.wait(make_retry_state(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 _ in range(1000): w = r.wait(make_retry_state(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 range(2)] + [tenacity.wait_fixed(4) for i in range(2)] + [tenacity.wait_fixed(8) for i in range(1)] ) ) for i in range(10): w = r.wait(make_retry_state(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 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 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) # max wait max_wait = 5 fn = tenacity.wait_random_exponential(10, max_wait) for _ in range(1000): self._assert_inclusive_range(fn(make_retry_state(1, 0)), 0.00, max_wait) # min wait min_wait = 5 fn = tenacity.wait_random_exponential(min=min_wait) for _ in range(1000): self._assert_inclusive_range(fn(make_retry_state(1, 0)), min_wait, 5) # Default arguments exist fn = tenacity.wait_random_exponential() fn(make_retry_state(0, 0)) def test_wait_random_exponential_statistically(self): fn = tenacity.wait_random_exponential(0.5, 60.0) attempt = [] for i in range(10): attempt.append([fn(make_retry_state(i, 0)) for _ in 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_exponential_jitter(self): fn = tenacity.wait_exponential_jitter(max=60) for _ in range(1000): self._assert_inclusive_range(fn(make_retry_state(1, 0)), 1, 2) self._assert_inclusive_range(fn(make_retry_state(2, 0)), 2, 3) self._assert_inclusive_range(fn(make_retry_state(3, 0)), 4, 5) self._assert_inclusive_range(fn(make_retry_state(4, 0)), 8, 9) self._assert_inclusive_range(fn(make_retry_state(5, 0)), 16, 17) self._assert_inclusive_range(fn(make_retry_state(6, 0)), 32, 33) self.assertEqual(fn(make_retry_state(7, 0)), 60) self.assertEqual(fn(make_retry_state(8, 0)), 60) self.assertEqual(fn(make_retry_state(9, 0)), 60) fn = tenacity.wait_exponential_jitter(10, 5) for _ in range(1000): self.assertEqual(fn(make_retry_state(1, 0)), 5) # Default arguments exist fn = tenacity.wait_exponential_jitter() fn(make_retry_state(0, 0)) 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: """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: """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 OSError("Hi there, I'm an IOError") return True class NoNameErrorAfterCount: """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 NoNameErrorCauseAfterCount: """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go2(self): raise NameError("Hi there, I'm a NameError") def go(self): """Raise an IOError with a NameError as cause until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 try: self.go2() except NameError as e: raise OSError() from e return True class NoIOErrorCauseAfterCount: """Holds counter state for invoking a method several times in a row.""" def __init__(self, count): self.counter = 0 self.count = count def go2(self): raise OSError("Hi there, I'm an IOError") def go(self): """Raise a NameError with an IOError as cause until after count threshold has been crossed. Then return True. """ if self.counter < self.count: self.counter += 1 try: self.go2() except OSError as e: raise NameError() from e return True class NameErrorUntilCount: """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: """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 OSError("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: """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().__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_cause_type(NameError)) def _retryable_test_with_exception_cause_type(thing): return thing.go() @retry(retry=tenacity.retry_if_exception_type(IOError)) def _retryable_test_with_exception_type_io(thing): return thing.go() @retry(retry=tenacity.retry_if_not_exception_type(IOError)) def _retryable_test_if_not_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_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 OSError 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_except_exception_of_type(self): self.assertTrue( _retryable_test_if_not_exception_type_io(NoNameErrorAfterCount(5)) ) try: _retryable_test_if_not_exception_type_io(NoIOErrorAfterCount(5)) self.fail("Expected IOError") except OSError as err: self.assertTrue(isinstance(err, IOError)) print(err) 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.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.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.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.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.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.statistics self.assertTrue(s["attempt_number"] == 1) def test_retry_if_exception_cause_type(self): self.assertTrue( _retryable_test_with_exception_cause_type(NoNameErrorCauseAfterCount(5)) ) try: _retryable_test_with_exception_cause_type(NoIOErrorCauseAfterCount(5)) self.fail("Expected exception without NameError as cause") except NameError: pass def test_retry_preserves_argument_defaults(self): def function_with_defaults(a=1): return a def function_with_kwdefaults(*, a=1): return a retrying = Retrying( wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3) ) wrapped_defaults_function = retrying.wraps(function_with_defaults) wrapped_kwdefaults_function = retrying.wraps(function_with_kwdefaults) self.assertEqual( function_with_defaults.__defaults__, wrapped_defaults_function.__defaults__ ) self.assertEqual( function_with_kwdefaults.__kwdefaults__, wrapped_kwdefaults_function.__kwdefaults__, ) 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 funсtools.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: 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_function_attributes(self): """Test that the wrapped function attributes are exposed as intended. - statistics contains the value for the latest function run - retry object can be modified to change its behaviour (useful to patch in tests) - retry object statistics do not contain valid information """ self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2))) expected_stats = { "attempt_number": 3, "delay_since_first_attempt": mock.ANY, "idle_for": mock.ANY, "start_time": mock.ANY, } self.assertEqual(_retryable_test_with_stop.statistics, expected_stats) self.assertEqual(_retryable_test_with_stop.retry.statistics, {}) with mock.patch.object( _retryable_test_with_stop.retry, "stop", tenacity.stop_after_attempt(1) ): try: self.assertTrue(_retryable_test_with_stop(NoneReturnUntilAfterCount(2))) except RetryError as exc: expected_stats = { "attempt_number": 1, "delay_since_first_attempt": mock.ANY, "idle_for": mock.ANY, "start_time": mock.ANY, } self.assertEqual(_retryable_test_with_stop.statistics, expected_stats) self.assertEqual(exc.last_attempt.attempt_number, 1) self.assertEqual(_retryable_test_with_stop.retry.statistics, {}) else: self.fail("RetryError should have been raised after 1 attempt") class TestRetryWith: def test_redefine_wait(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 assert t >= 500 assert result is True def test_redefine_stop(self): result = _retryable_test_with_stop.retry_with( stop=tenacity.stop_after_attempt(5) )(NoneReturnUntilAfterCount(4)) assert result is True def test_retry_error_cls_should_be_preserved(self): @retry(stop=tenacity.stop_after_attempt(10), retry_error_cls=ValueError) def _retryable(): raise Exception("raised for test purposes") with pytest.raises(Exception) as exc_ctx: _retryable.retry_with(stop=tenacity.stop_after_attempt(2))() assert exc_ctx.type is ValueError, "Should remap to specific exception type" def test_retry_error_callback_should_be_preserved(self): def return_text(retry_state): return "Calling {} keeps raising errors after {} attempts".format( retry_state.fn.__name__, retry_state.attempt_number, ) @retry(stop=tenacity.stop_after_attempt(10), retry_error_callback=return_text) def _retryable(): raise Exception("raised for test purposes") result = _retryable.retry_with(stop=tenacity.stop_after_attempt(5))() assert result == "Calling _retryable keeps raising errors after 5 attempts" 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 _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.assertRegex(fmt(handler.records[0]), etalon_re) self.assertRegex(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_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.assertRegex(fmt(handler.records[0]), etalon_re) self.assertRegex(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.assertRegex(fmt(handler.records[0]), etalon_re) self.assertRegex(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.statistics) _foobar() self.assertEqual(1, _foobar.statistics["attempt_number"]) def test_stats_failing(self): @retry(stop=tenacity.stop_after_attempt(2)) def _foobar(): raise ValueError(42) self.assertEqual({}, _foobar.statistics) try: _foobar() except Exception: # noqa: B902 pass self.assertEqual(2, _foobar.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(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 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, typing.Callable[[int], str]) check_type(with_raw_result, str) check_type(with_constructor, typing.Callable[[int], str]) check_type(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: call_count = 0 def __call__(self, seconds): self.call_count += 1 sleep = MockSleep() monkeypatch.setattr(tenacity.nap.time, "sleep", sleep) yield sleep 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=1722255136.0 tenacity-9.0.0/tests/test_tornado.py0000644000175100001770000000426414651703440017200 0ustar00runnerdocker# mypy: disable-error-code="no-untyped-def,no-untyped-call" # 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 tornado import gen from tornado import testing from .test_tenacity import NoIOErrorAfterCount @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): # type: ignore[misc] @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=1722255136.0 tenacity-9.0.0/tests/test_utils.py0000644000175100001770000000264614651703440016674 0ustar00runnerdockerimport functools from tenacity import _utils def test_is_coroutine_callable() -> None: async def async_func() -> None: pass def sync_func() -> None: pass class AsyncClass: async def __call__(self) -> None: pass class SyncClass: def __call__(self) -> None: pass lambda_fn = lambda: None # noqa: E731 partial_async_func = functools.partial(async_func) partial_sync_func = functools.partial(sync_func) partial_async_class = functools.partial(AsyncClass().__call__) partial_sync_class = functools.partial(SyncClass().__call__) partial_lambda_fn = functools.partial(lambda_fn) assert _utils.is_coroutine_callable(async_func) is True assert _utils.is_coroutine_callable(sync_func) is False assert _utils.is_coroutine_callable(AsyncClass) is False assert _utils.is_coroutine_callable(AsyncClass()) is True assert _utils.is_coroutine_callable(SyncClass) is False assert _utils.is_coroutine_callable(SyncClass()) is False assert _utils.is_coroutine_callable(lambda_fn) is False assert _utils.is_coroutine_callable(partial_async_func) is True assert _utils.is_coroutine_callable(partial_sync_func) is False assert _utils.is_coroutine_callable(partial_async_class) is True assert _utils.is_coroutine_callable(partial_sync_class) is False assert _utils.is_coroutine_callable(partial_lambda_fn) is False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722255136.0 tenacity-9.0.0/tox.ini0000644000175100001770000000127214651703440014266 0ustar00runnerdocker[tox] envlist = py3{8,9,10,11,12,12-trio}, pep8, pypy3 skip_missing_interpreters = True [testenv] usedevelop = True sitepackages = False deps = .[test] .[doc] trio: trio commands = py3{8,9,10,11,12},pypy3: pytest {posargs} py3{8,9,10,11,12},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build py3{8,9,10,11,12},pypy3: sphinx-build -a -E -W -b html doc/source doc/build [testenv:pep8] basepython = python3 deps = ruff commands = ruff check . {posargs} ruff format --check . {posargs} [testenv:mypy] deps = mypy>=1.0.0 pytest # for stubs trio commands = mypy {posargs} [testenv:reno] basepython = python3 deps = reno commands = reno {posargs}