././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1692019368.2142208
tenacity-8.2.3/ 0000755 0001751 0000172 00000000000 14466425250 012754 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/.editorconfig 0000644 0001751 0000172 00000000446 14466425217 015440 0 ustar 00runner docker root = 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
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1692019368.202221
tenacity-8.2.3/.github/ 0000755 0001751 0000172 00000000000 14466425250 014314 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1692019368.202221
tenacity-8.2.3/.github/workflows/ 0000755 0001751 0000172 00000000000 14466425250 016351 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/.github/workflows/ci.yaml 0000644 0001751 0000172 00000002154 14466425217 017635 0 ustar 00runner docker name: 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.7"
tox: py37
- python: "3.8"
tox: py38
- python: "3.9"
tox: py39
- python: "3.10"
tox: py310
- python: "3.11"
tox: py311
- python: "3.11"
tox: pep8
- python: "3.11"
tox: black-ci
- python: "3.11"
tox: mypy
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3.3.0
with:
fetch-depth: 0
- name: Setup Python 🔧
uses: actions/setup-python@v4.5.0
with:
python-version: ${{ matrix.python }}
- name: Build 🔧 & Test 🔍
run: |
pip install tox
tox -e ${{ matrix.tox }}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/.github/workflows/deploy.yaml 0000644 0001751 0000172 00000001330 14466425217 020531 0 ustar 00runner docker name: Release deploy
on:
push:
tags:
jobs:
test:
timeout-minutes: 20
runs-on: ubuntu-20.04
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3.3.0
with:
fetch-depth: 0
- name: Setup Python 🔧
uses: actions/setup-python@v4.5.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/*
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/.gitignore 0000644 0001751 0000172 00000000134 14466425217 014745 0 ustar 00runner docker .idea
dist
*.pyc
*.egg-info
build
.tox/
AUTHORS
ChangeLog
.eggs/
doc/_build
/.pytest_cache
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/.mergify.yml 0000644 0001751 0000172 00000003260 14466425217 015223 0 ustar 00runner docker queue_rules:
- name: default
conditions: &CheckRuns
- "check-success=test (3.7, py37)"
- "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.11, black-ci)"
- "check-success=test (3.11, pep8)"
- "check-success=test (3.11, mypy)"
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 merge without changelog
conditions:
- and: *CheckRuns
- "#approved-reviews-by>=1"
- label=no-changelog
actions:
queue:
name: default
method: squash
- name: automatic merge with changelog
conditions:
- and: *CheckRuns
- "#approved-reviews-by>=1"
- files~=^releasenotes/notes/
actions:
queue:
name: default
method: squash
- name: automatic merge for jd without changelog
conditions:
- author=jd
- and: *CheckRuns
- label=no-changelog
actions:
queue:
name: default
method: squash
- name: automatic merge for jd with changelog
conditions:
- author=jd
- and: *CheckRuns
- files~=^releasenotes/notes/
actions:
queue:
name: default
method: squash
- name: dismiss reviews
conditions: []
actions:
dismiss_reviews: {}
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/.readthedocs.yml 0000644 0001751 0000172 00000000146 14466425217 016046 0 ustar 00runner docker version: 2
python:
install:
- method: pip
path: .
extra_requirements:
- doc
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/LICENSE 0000644 0001751 0000172 00000026135 14466425217 013773 0 ustar 00runner docker
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. ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1692019368.2142208
tenacity-8.2.3/PKG-INFO 0000644 0001751 0000172 00000001637 14466425250 014060 0 ustar 00runner docker Metadata-Version: 2.1
Name: tenacity
Version: 8.2.3
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.7
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: Topic :: Utilities
Requires-Python: >=3.7
Provides-Extra: doc
License-File: LICENSE
Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/README.rst 0000644 0001751 0000172 00000041667 14466425217 014464 0 ustar 00runner docker Tenacity
========
.. 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
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
`retry` attribute attached to the function and its `statistics` attribute:
.. testcode::
@retry(stop=stop_after_attempt(3))
def raise_my_exception():
raise MyException("Fail")
try:
raise_my_exception()
except Exception:
pass
print(raise_my_exception.retry.statistics)
.. testoutput::
:hide:
...
Custom Callbacks
~~~~~~~~~~~~~~~~
You can also define your own callbacks. The callback should accept one
parameter called ``retry_state`` that contains all information about current
retry invocation.
For example, you can call a custom callback function after all retries failed,
without raising an exception (or you can re-raise or do anything really)
.. testcode::
def return_last_value(retry_state):
"""return the result of the last call attempt"""
return retry_state.outcome.result()
def is_false(value):
"""Return True if value is False"""
return value is False
# will return False after trying 3 times to get a different result
@retry(stop=stop_after_attempt(3),
retry_error_callback=return_last_value,
retry=retry_if_result(is_false))
def eventually_return_false():
return False
RetryCallState
~~~~~~~~~~~~~~
``retry_state`` argument is an object of `RetryCallState` class:
.. autoclass:: tenacity.RetryCallState
Constant attributes:
.. autoattribute:: start_time(float)
:annotation:
.. autoattribute:: retry_object(BaseRetrying)
:annotation:
.. autoattribute:: fn(callable)
:annotation:
.. autoattribute:: args(tuple)
:annotation:
.. autoattribute:: kwargs(dict)
:annotation:
Variable attributes:
.. autoattribute:: attempt_number(int)
:annotation:
.. autoattribute:: outcome(tenacity.Future or None)
:annotation:
.. autoattribute:: outcome_timestamp(float or None)
:annotation:
.. autoattribute:: idle_for(float)
:annotation:
.. autoattribute:: next_action(tenacity.RetryAction or None)
:annotation:
Other Custom Callbacks
~~~~~~~~~~~~~~~~~~~~~~
It's also possible to define custom callbacks for other keyword arguments.
.. function:: my_stop(retry_state)
:param RetryState retry_state: info about current retry invocation
:return: whether or not retrying should stop
:rtype: bool
.. function:: my_wait(retry_state)
:param RetryState retry_state: info about current retry invocation
:return: number of seconds to wait before next retry
:rtype: float
.. function:: my_retry(retry_state)
:param RetryState retry_state: info about current retry invocation
:return: whether or not retrying should continue
:rtype: bool
.. function:: my_before(retry_state)
:param RetryState retry_state: info about current retry invocation
.. function:: my_after(retry_state)
:param RetryState retry_state: info about current retry invocation
.. function:: my_before_sleep(retry_state)
:param RetryState retry_state: info about current retry invocation
Here's an example with a custom ``before_sleep`` function:
.. testcode::
import logging
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
logger = logging.getLogger(__name__)
def my_before_sleep(retry_state):
if retry_state.attempt_number < 1:
loglevel = logging.INFO
else:
loglevel = logging.WARNING
logger.log(
loglevel, 'Retrying %s: attempt %s ended with: %s',
retry_state.fn, retry_state.attempt_number, retry_state.outcome)
@retry(stop=stop_after_attempt(3), before_sleep=my_before_sleep)
def raise_my_exception():
raise MyException("Fail")
try:
raise_my_exception()
except RetryError:
pass
Changing Arguments at Run Time
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can change the arguments of a retry decorator as needed when calling it by
using the `retry_with` function attached to the wrapped function:
.. testcode::
@retry(stop=stop_after_attempt(3))
def raise_my_exception():
raise MyException("Fail")
try:
raise_my_exception.retry_with(stop=stop_after_attempt(4))()
except Exception:
pass
print(raise_my_exception.retry.statistics)
.. testoutput::
:hide:
...
If you want to use variables to set up the retry parameters, you don't have
to use the `retry` decorator - you can instead use `Retrying` directly:
.. testcode::
def never_good_enough(arg1):
raise Exception('Invalid argument: {}'.format(arg1))
def try_never_good_enough(max_attempts=3):
retryer = Retrying(stop=stop_after_attempt(max_attempts), reraise=True)
retryer(never_good_enough, 'I really do try')
Retrying code block
~~~~~~~~~~~~~~~~~~~
Tenacity allows you to retry a code block without the need to wraps it in an
isolated function. This makes it easy to isolate failing block while sharing
context. The trick is to combine a for loop and a context manager.
.. testcode::
from tenacity import Retrying, RetryError, stop_after_attempt
try:
for attempt in Retrying(stop=stop_after_attempt(3)):
with attempt:
raise Exception('My code is failing!')
except RetryError:
pass
You can configure every details of retry policy by configuring the Retrying
object.
With async code you can use AsyncRetrying.
.. testcode::
from tenacity import AsyncRetrying, RetryError, stop_after_attempt
async def function():
try:
async for attempt in AsyncRetrying(stop=stop_after_attempt(3)):
with attempt:
raise Exception('My code is failing!')
except RetryError:
pass
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 and Tornado (>= 4.5) coroutines.
Sleeps are done asynchronously too.
.. code-block:: python
@retry
async def my_async_function(loop):
await loop.getaddrinfo('8.8.8.8', 53)
.. code-block:: python
@retry
@tornado.gen.coroutine
def my_async_function(http_client, url):
yield http_client.fetch(url)
You can even use alternative event loops such as `curio` or `Trio` by passing the correct sleep function:
.. code-block:: python
@retry(sleep=trio.sleep)
async def my_async_function(loop):
await asks.get('https://example.org')
Contribute
----------
#. Check for open issues or open a fresh issue to start a discussion around a
feature idea or a bug.
#. Fork `the repository`_ on GitHub to start making your changes to the
**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
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1692019368.202221
tenacity-8.2.3/doc/ 0000755 0001751 0000172 00000000000 14466425250 013521 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1692019368.2062209
tenacity-8.2.3/doc/source/ 0000755 0001751 0000172 00000000000 14466425250 015021 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/doc/source/api.rst 0000644 0001751 0000172 00000002722 14466425217 016332 0 ustar 00runner docker ===============
API Reference
===============
Retry Main API
--------------
.. autofunction:: tenacity.retry
:noindex:
.. autoclass:: tenacity.Retrying
:members:
.. autoclass:: tenacity.AsyncRetrying
:members:
.. autoclass:: tenacity.tornadoweb.TornadoRetrying
:members:
After Functions
---------------
Those functions can be used as the `after` keyword argument of
:py:func:`tenacity.retry`.
.. automodule:: tenacity.after
:members:
Before Functions
----------------
Those functions can be used as the `before` keyword argument of
:py:func:`tenacity.retry`.
.. automodule:: tenacity.before
:members:
Before Sleep Functions
----------------------
Those functions can be used as the `before_sleep` keyword argument of
:py:func:`tenacity.retry`.
.. automodule:: tenacity.before_sleep
:members:
Nap Functions
-------------
Those functions can be used as the `sleep` keyword argument of
:py:func:`tenacity.retry`.
.. automodule:: tenacity.nap
:members:
Retry Functions
---------------
Those functions can be used as the `retry` keyword argument of
:py:func:`tenacity.retry`.
.. automodule:: tenacity.retry
:members:
Stop Functions
--------------
Those functions can be used as the `stop` keyword argument of
:py:func:`tenacity.retry`.
.. automodule:: tenacity.stop
:members:
Wait Functions
--------------
Those functions can be used as the `wait` keyword argument of
:py:func:`tenacity.retry`.
.. automodule:: tenacity.wait
:members:
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/doc/source/changelog.rst 0000644 0001751 0000172 00000000050 14466425217 017500 0 ustar 00runner docker Changelog
=========
.. release-notes::
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/doc/source/conf.py 0000644 0001751 0000172 00000002372 14466425217 016327 0 ustar 00runner docker # 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 =
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/doc/source/index.rst 0000644 0001751 0000172 00000041667 14466425217 016703 0 ustar 00runner docker Tenacity
========
.. 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
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
`retry` attribute attached to the function and its `statistics` attribute:
.. testcode::
@retry(stop=stop_after_attempt(3))
def raise_my_exception():
raise MyException("Fail")
try:
raise_my_exception()
except Exception:
pass
print(raise_my_exception.retry.statistics)
.. testoutput::
:hide:
...
Custom Callbacks
~~~~~~~~~~~~~~~~
You can also define your own callbacks. The callback should accept one
parameter called ``retry_state`` that contains all information about current
retry invocation.
For example, you can call a custom callback function after all retries failed,
without raising an exception (or you can re-raise or do anything really)
.. testcode::
def return_last_value(retry_state):
"""return the result of the last call attempt"""
return retry_state.outcome.result()
def is_false(value):
"""Return True if value is False"""
return value is False
# will return False after trying 3 times to get a different result
@retry(stop=stop_after_attempt(3),
retry_error_callback=return_last_value,
retry=retry_if_result(is_false))
def eventually_return_false():
return False
RetryCallState
~~~~~~~~~~~~~~
``retry_state`` argument is an object of `RetryCallState` class:
.. autoclass:: tenacity.RetryCallState
Constant attributes:
.. autoattribute:: start_time(float)
:annotation:
.. autoattribute:: retry_object(BaseRetrying)
:annotation:
.. autoattribute:: fn(callable)
:annotation:
.. autoattribute:: args(tuple)
:annotation:
.. autoattribute:: kwargs(dict)
:annotation:
Variable attributes:
.. autoattribute:: attempt_number(int)
:annotation:
.. autoattribute:: outcome(tenacity.Future or None)
:annotation:
.. autoattribute:: outcome_timestamp(float or None)
:annotation:
.. autoattribute:: idle_for(float)
:annotation:
.. autoattribute:: next_action(tenacity.RetryAction or None)
:annotation:
Other Custom Callbacks
~~~~~~~~~~~~~~~~~~~~~~
It's also possible to define custom callbacks for other keyword arguments.
.. function:: my_stop(retry_state)
:param RetryState retry_state: info about current retry invocation
:return: whether or not retrying should stop
:rtype: bool
.. function:: my_wait(retry_state)
:param RetryState retry_state: info about current retry invocation
:return: number of seconds to wait before next retry
:rtype: float
.. function:: my_retry(retry_state)
:param RetryState retry_state: info about current retry invocation
:return: whether or not retrying should continue
:rtype: bool
.. function:: my_before(retry_state)
:param RetryState retry_state: info about current retry invocation
.. function:: my_after(retry_state)
:param RetryState retry_state: info about current retry invocation
.. function:: my_before_sleep(retry_state)
:param RetryState retry_state: info about current retry invocation
Here's an example with a custom ``before_sleep`` function:
.. testcode::
import logging
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
logger = logging.getLogger(__name__)
def my_before_sleep(retry_state):
if retry_state.attempt_number < 1:
loglevel = logging.INFO
else:
loglevel = logging.WARNING
logger.log(
loglevel, 'Retrying %s: attempt %s ended with: %s',
retry_state.fn, retry_state.attempt_number, retry_state.outcome)
@retry(stop=stop_after_attempt(3), before_sleep=my_before_sleep)
def raise_my_exception():
raise MyException("Fail")
try:
raise_my_exception()
except RetryError:
pass
Changing Arguments at Run Time
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can change the arguments of a retry decorator as needed when calling it by
using the `retry_with` function attached to the wrapped function:
.. testcode::
@retry(stop=stop_after_attempt(3))
def raise_my_exception():
raise MyException("Fail")
try:
raise_my_exception.retry_with(stop=stop_after_attempt(4))()
except Exception:
pass
print(raise_my_exception.retry.statistics)
.. testoutput::
:hide:
...
If you want to use variables to set up the retry parameters, you don't have
to use the `retry` decorator - you can instead use `Retrying` directly:
.. testcode::
def never_good_enough(arg1):
raise Exception('Invalid argument: {}'.format(arg1))
def try_never_good_enough(max_attempts=3):
retryer = Retrying(stop=stop_after_attempt(max_attempts), reraise=True)
retryer(never_good_enough, 'I really do try')
Retrying code block
~~~~~~~~~~~~~~~~~~~
Tenacity allows you to retry a code block without the need to wraps it in an
isolated function. This makes it easy to isolate failing block while sharing
context. The trick is to combine a for loop and a context manager.
.. testcode::
from tenacity import Retrying, RetryError, stop_after_attempt
try:
for attempt in Retrying(stop=stop_after_attempt(3)):
with attempt:
raise Exception('My code is failing!')
except RetryError:
pass
You can configure every details of retry policy by configuring the Retrying
object.
With async code you can use AsyncRetrying.
.. testcode::
from tenacity import AsyncRetrying, RetryError, stop_after_attempt
async def function():
try:
async for attempt in AsyncRetrying(stop=stop_after_attempt(3)):
with attempt:
raise Exception('My code is failing!')
except RetryError:
pass
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 and Tornado (>= 4.5) coroutines.
Sleeps are done asynchronously too.
.. code-block:: python
@retry
async def my_async_function(loop):
await loop.getaddrinfo('8.8.8.8', 53)
.. code-block:: python
@retry
@tornado.gen.coroutine
def my_async_function(http_client, url):
yield http_client.fetch(url)
You can even use alternative event loops such as `curio` or `Trio` by passing the correct sleep function:
.. code-block:: python
@retry(sleep=trio.sleep)
async def my_async_function(loop):
await asks.get('https://example.org')
Contribute
----------
#. Check for open issues or open a fresh issue to start a discussion around a
feature idea or a bug.
#. Fork `the repository`_ on GitHub to start making your changes to the
**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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/pyproject.toml 0000644 0001751 0000172 00000001231 14466425217 015670 0 ustar 00runner docker [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.black]
line-length = 120
safe = true
target-version = ["py37", "py38", "py39", "py310", "py311"]
[tool.mypy]
strict = true
files = ["tenacity"]
show_error_codes = true
[[tool.mypy.overrides]]
module = "tornado.*"
ignore_missing_imports = true
[tool.setuptools_scm]
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1692019368.202221
tenacity-8.2.3/releasenotes/ 0000755 0001751 0000172 00000000000 14466425250 015445 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1692019368.2102208
tenacity-8.2.3/releasenotes/notes/ 0000755 0001751 0000172 00000000000 14466425250 016575 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/Fix-tests-for-typeguard-3.x-6eebfea546b6207e.yaml 0000644 0001751 0000172 00000000070 14466425217 027123 0 ustar 00runner docker ---
fixes:
- "Fixes test failures with typeguard 3.x"
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/Use--for-formatting-and-validate-using-black-39ec9d57d4691778.yaml0000644 0001751 0000172 00000000245 14466425217 032066 0 ustar 00runner docker ---
other:
- "Use `black` for code formatting and validate using `black --check`. Code compatibility: py26-py39."
- "Enforce maximal line length to 120 symbols"
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/add-reno-d1ab5710f272650a.yaml 0000644 0001751 0000172 00000000056 14466425217 023275 0 ustar 00runner docker ---
features:
- Add reno (changelog system)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml 0000644 0001751 0000172 00000000203 14466425217 030165 0 ustar 00runner docker ---
features:
- Add ``retry_if_not_exception_type()`` that allows to retry if a raised exception doesn't match given exceptions.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml 0000644 0001751 0000172 00000000145 14466425217 030612 0 ustar 00runner docker ---
other:
- Add `retry_if_exception_cause_type`and `wait_exponential_jitter` to __all__ of init.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml 0000644 0001751 0000172 00000000275 14466425217 031046 0 ustar 00runner docker ---
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.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/added_a_link_to_documentation-eefaf8f074b539f8.yaml 0000644 0001751 0000172 00000000226 14466425217 030073 0 ustar 00runner docker ---
other:
- |
Added a link to the documentation, as code snippets are not being rendered properly
Changed branch name to main in index.rst
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/after_log-50f4d73b24ce9203.yaml 0000644 0001751 0000172 00000000134 14466425217 023554 0 ustar 00runner docker ---
fixes:
- "Fix after_log logger format: function name was used with delay formatting."
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/allow-mocking-of-nap-sleep-6679c50e702446f1.yaml 0000644 0001751 0000172 00000000137 14466425217 026520 0 ustar 00runner docker ---
other:
- Unit tests can now mock ``nap.sleep()`` for testing in all tenacity usage styles ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/annotate_code-197b93130df14042.yaml 0000644 0001751 0000172 00000000075 14466425217 024256 0 ustar 00runner docker ---
other:
- Add type annotations to cover all public API.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml 0000644 0001751 0000172 00000000124 14466425217 027732 0 ustar 00runner docker ---
features:
- Add an ``exc_info`` option to the ``before_sleep_log()`` strategy. ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml 0000644 0001751 0000172 00000000077 14466425217 026250 0 ustar 00runner docker ---
prelude: >
Clarify usage of `reraise` keyword argument
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml 0000644 0001751 0000172 00000000063 14466425217 026210 0 ustar 00runner docker ---
other:
- Do not package tests with tenacity.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml 0000644 0001751 0000172 00000000117 14466425217 030111 0 ustar 00runner docker ---
other:
- |
Drop support for deprecated Python versions (2.7 and 3.5)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml 0000644 0001751 0000172 00000000422 14466425217 024652 0 ustar 00runner docker ---
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`"
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml 0000644 0001751 0000172 00000000122 14466425217 027052 0 ustar 00runner docker ---
features:
- Explicitly export convenience symbols from tenacity root module
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml 0000644 0001751 0000172 00000000127 14466425217 027174 0 ustar 00runner docker ---
fixes:
- |
Fix async loop with retrying code block when result is available.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml 0000644 0001751 0000172 00000000313 14466425217 025302 0 ustar 00runner docker ---
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]]]`
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml 0000644 0001751 0000172 00000000124 14466425217 023672 0 ustar 00runner docker ---
fixes:
- "Fix issue #288 : __name__ and other attributes for async functions"
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml 0000644 0001751 0000172 00000000177 14466425217 027305 0 ustar 00runner docker ---
fixes:
- |
Use str.format to format the logs internally to make logging compatible with other logger such as loguru.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/no-async-iter-6132a42e52348a75.yaml 0000644 0001751 0000172 00000000410 14466425217 024137 0 ustar 00runner docker ---
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.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/pr320-py3-only-wheel-tag.yaml 0000644 0001751 0000172 00000000175 14466425217 023700 0 ustar 00runner docker ---
other: >-
Corrected the PyPI-published wheel tag to match the
metadata saying that the release is Python 3 only.
...
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/py36_plus-c425fb3aa17c6682.yaml 0000644 0001751 0000172 00000000143 14466425217 023537 0 ustar 00runner docker ---
features:
- Most part of the code is type annotated.
- Python 3.10 support has been added.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/remove-py36-876c0416cf279d15.yaml 0000644 0001751 0000172 00000000101 14466425217 023635 0 ustar 00runner docker ---
upgrade:
- |
Support for Python 3.6 has been removed.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml 0000644 0001751 0000172 00000000140 14466425217 025632 0 ustar 00runner docker ---
features:
- Add a ``__repr__`` method to ``RetryCallState`` objects for easier debugging.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml 0000644 0001751 0000172 00000000117 14466425217 025740 0 ustar 00runner docker ---
fixes: Sphinx build error where Sphinx complains about an undefined class.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml 0000644 0001751 0000172 00000000111 14466425217 030330 0 ustar 00runner docker ---
features:
- Add ``datetime.timedelta`` as accepted wait unit type.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml 0000644 0001751 0000172 00000000164 14466425217 025536 0 ustar 00runner docker ---
features:
- |
- accept ``datetime.timedelta`` instances as argument to ``tenacity.stop.stop_after_delay``
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml 0000644 0001751 0000172 00000000240 14466425217 027206 0 ustar 00runner docker ---
features:
- |
Implement a wait.wait_exponential_jitter per Google's storage retry guide.
See https://cloud.google.com/storage/docs/retry-strategy
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/reno.yaml 0000644 0001751 0000172 00000000051 14466425217 014602 0 ustar 00runner docker ---
unreleased_version_title: Unreleased
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1692019368.2142208
tenacity-8.2.3/setup.cfg 0000644 0001751 0000172 00000002125 14466425250 014575 0 ustar 00runner docker [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.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Topic :: Utilities
[options]
install_requires =
python_requires = >=3.7
packages = tenacity
[options.packages.find]
exclude = tests
[options.package_data]
tenacity = py.typed
[options.extras_require]
doc =
reno
sphinx
tornado>=4.5
[tool:pytest]
filterwarnings =
once::DeprecationWarning
[egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/setup.py 0000644 0001751 0000172 00000001242 14466425217 014470 0 ustar 00runner docker #!/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,
)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1692019368.2102208
tenacity-8.2.3/tenacity/ 0000755 0001751 0000172 00000000000 14466425250 014574 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/__init__.py 0000644 0001751 0000172 00000047366 14466425217 016730 0 ustar 00runner docker # 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 functools
import sys
import threading
import time
import typing as t
import warnings
from abc import ABC, abstractmethod
from concurrent import futures
from inspect import iscoroutinefunction
# 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_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 .retry import RetryBaseT
from .stop import StopBaseT
from .wait import WaitBaseT
WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any])
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
def wraps(self, f: WrappedFn) -> WrappedFn:
"""Wrap a function for retrying.
:param f: A function to wraps for retrying.
"""
@functools.wraps(f)
def wrapped_f(*args: t.Any, **kw: t.Any) -> t.Any:
return self(f, *args, **kw)
def retry_with(*args: t.Any, **kwargs: t.Any) -> WrappedFn:
return self.copy(*args, **kwargs).wraps(f)
wrapped_f.retry = self # type: ignore[attr-defined]
wrapped_f.retry_with = retry_with # 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 iter(self, retry_state: "RetryCallState") -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa
fut = retry_state.outcome
if fut is None:
if self.before is not None:
self.before(retry_state)
return DoAttempt()
is_explicit_retry = fut.failed and isinstance(fut.exception(), TryAgain)
if not (is_explicit_retry or self.retry(retry_state)):
return fut.result()
if self.after is not None:
self.after(retry_state)
self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start
if self.stop(retry_state):
if self.retry_error_callback:
return self.retry_error_callback(retry_state)
retry_exc = self.retry_error_cls(fut)
if self.reraise:
raise retry_exc.reraise()
raise retry_exc from fut.exception()
if self.wait:
sleep = self.wait(retry_state)
else:
sleep = 0.0
retry_state.next_action = RetryAction(sleep)
retry_state.idle_for += sleep
self.statistics["idle_for"] += sleep
self.statistics["attempt_number"] += 1
if self.before_sleep is not None:
self.before_sleep(retry_state)
return DoSleep(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[1] >= 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
@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.Optional[t.Awaitable[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,
) -> 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 iscoroutinefunction(f):
r = AsyncRetrying(*dargs, **dkw)
elif tornado and hasattr(tornado.gen, "is_coroutine_function") and tornado.gen.is_coroutine_function(f):
r = TornadoRetrying(*dargs, **dkw)
else:
r = Retrying(*dargs, **dkw)
return r.wraps(f)
return wrap
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_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",
]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/_asyncio.py 0000644 0001751 0000172 00000006643 14466425217 016766 0 ustar 00runner docker # 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
from asyncio import sleep
from tenacity import AttemptManager
from tenacity import BaseRetrying
from tenacity import DoAttempt
from tenacity import DoSleep
from tenacity import RetryCallState
WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")
WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Awaitable[t.Any]])
class AsyncRetrying(BaseRetrying):
sleep: t.Callable[[float], t.Awaitable[t.Any]]
def __init__(self, sleep: t.Callable[[float], t.Awaitable[t.Any]] = sleep, **kwargs: t.Any) -> None:
super().__init__(**kwargs)
self.sleep = sleep
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 = 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)
else:
return do # type: ignore[no-any-return]
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 = self.iter(retry_state=self._retry_state)
if do is None:
raise StopAsyncIteration
elif isinstance(do, DoAttempt):
return AttemptManager(retry_state=self._retry_state)
elif isinstance(do, DoSleep):
self._retry_state.prepare_for_next_attempt()
await self.sleep(do)
else:
raise StopAsyncIteration
def wraps(self, fn: WrappedFn) -> WrappedFn:
fn = super().wraps(fn)
# Ensure wrapper is recognized as a coroutine function.
@functools.wraps(fn)
async def async_wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any:
return await fn(*args, **kwargs)
# Preserve attributes
async_wrapped.retry = fn.retry # type: ignore[attr-defined]
async_wrapped.retry_with = fn.retry_with # type: ignore[attr-defined]
return async_wrapped # type: ignore[return-value]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/_utils.py 0000644 0001751 0000172 00000004203 14466425217 016447 0 ustar 00runner docker # Copyright 2016 Julien Danjou
# Copyright 2016 Joshua Harlow
# Copyright 2013-2014 Ray Holder
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/after.py 0000644 0001751 0000172 00000003172 14466425217 016255 0 ustar 00runner docker # 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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/before.py 0000644 0001751 0000172 00000003002 14466425217 016406 0 ustar 00runner docker # 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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/before_sleep.py 0000644 0001751 0000172 00000004454 14466425217 017612 0 ustar 00runner docker # 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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/nap.py 0000644 0001751 0000172 00000002547 14466425217 015737 0 ustar 00runner docker # 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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/py.typed 0000644 0001751 0000172 00000000000 14466425217 016264 0 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/retry.py 0000644 0001751 0000172 00000021036 14466425217 016320 0 ustar 00runner docker # 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 retry_all(self, other)
def __or__(self, other: "retry_base") -> "retry_any":
return retry_any(self, other)
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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/stop.py 0000644 0001751 0000172 00000005766 14466425217 016154 0 ustar 00runner docker # 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."""
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
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/tornadoweb.py 0000644 0001751 0000172 00000004056 14466425217 017322 0 ustar 00runner docker # 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)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tenacity/wait.py 0000644 0001751 0000172 00000017500 14466425217 016120 0 ustar 00runner docker # 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(0, 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))
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1692019368.2102208
tenacity-8.2.3/tenacity.egg-info/ 0000755 0001751 0000172 00000000000 14466425250 016266 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019368.0
tenacity-8.2.3/tenacity.egg-info/PKG-INFO 0000644 0001751 0000172 00000001637 14466425250 017372 0 ustar 00runner docker Metadata-Version: 2.1
Name: tenacity
Version: 8.2.3
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.7
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: Topic :: Utilities
Requires-Python: >=3.7
Provides-Extra: doc
License-File: LICENSE
Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019368.0
tenacity-8.2.3/tenacity.egg-info/SOURCES.txt 0000644 0001751 0000172 00000005053 14466425250 020155 0 ustar 00runner docker .editorconfig
.gitignore
.mergify.yml
.readthedocs.yml
LICENSE
README.rst
pyproject.toml
reno.yaml
setup.cfg
setup.py
tox.ini
.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-reno-d1ab5710f272650a.yaml
releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.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/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-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/sphinx_define_error-642c9cd5c165d39a.yaml
releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml
releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml
releasenotes/notes/wait_exponential_jitter-6ffc81dddcbaa6d3.yaml
tenacity/__init__.py
tenacity/_asyncio.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
tests/__init__.py
tests/test_after.py
tests/test_asyncio.py
tests/test_tenacity.py
tests/test_tornado.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019368.0
tenacity-8.2.3/tenacity.egg-info/dependency_links.txt 0000644 0001751 0000172 00000000001 14466425250 022334 0 ustar 00runner docker
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019368.0
tenacity-8.2.3/tenacity.egg-info/requires.txt 0000644 0001751 0000172 00000000040 14466425250 020660 0 ustar 00runner docker
[doc]
reno
sphinx
tornado>=4.5
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019368.0
tenacity-8.2.3/tenacity.egg-info/top_level.txt 0000644 0001751 0000172 00000000011 14466425250 021010 0 ustar 00runner docker tenacity
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1692019368.2142208
tenacity-8.2.3/tests/ 0000755 0001751 0000172 00000000000 14466425250 014116 5 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tests/__init__.py 0000644 0001751 0000172 00000000000 14466425217 016220 0 ustar 00runner docker ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tests/test_after.py 0000644 0001751 0000172 00000004251 14466425217 016635 0 ustar 00runner docker 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.",
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tests/test_asyncio.py 0000644 0001751 0000172 00000012522 14466425217 017201 0 ustar 00runner docker # coding: utf-8
# Copyright 2016 Étienne Bersac
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import inspect
import unittest
from functools import wraps
import pytest
from tenacity import AsyncRetrying, RetryError
from tenacity import _asyncio as tasyncio
from tenacity import retry, 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)
thing.go()
class TestAsync(unittest.TestCase):
@asynctest
async def test_retry(self):
thing = NoIOErrorAfterCount(5)
await _retryable_coroutine(thing)
assert thing.counter == thing.count
@asynctest
async def test_iscoroutinefunction(self):
assert asyncio.iscoroutinefunction(_retryable_coroutine)
assert inspect.iscoroutinefunction(_retryable_coroutine)
@asynctest
async def test_retry_using_async_retying(self):
thing = NoIOErrorAfterCount(5)
retrying = AsyncRetrying()
await retrying(_async_function, thing)
assert thing.counter == thing.count
@asynctest
async def test_stop_after_attempt(self):
thing = NoIOErrorAfterCount(2)
try:
await _retryable_coroutine_with_2_attempts(thing)
except RetryError:
assert thing.counter == 2
def test_repr(self):
repr(tasyncio.AsyncRetrying())
def test_retry_attributes(self):
assert hasattr(_retryable_coroutine, "retry")
assert hasattr(_retryable_coroutine, "retry_with")
@asynctest
async def test_attempt_number_is_correct_for_interleaved_coroutines(self):
attempts = []
def after(retry_state):
attempts.append((retry_state.args[0], retry_state.attempt_number))
thing1 = NoIOErrorAfterCount(3)
thing2 = NoIOErrorAfterCount(3)
await asyncio.gather(
_retryable_coroutine.retry_with(after=after)(thing1),
_retryable_coroutine.retry_with(after=after)(thing2),
)
# There's no waiting on retry, only a wait in the coroutine, so the
# executions should be interleaved.
even_thing_attempts = attempts[::2]
things, attempt_nos1 = zip(*even_thing_attempts)
assert len(set(things)) == 1
assert list(attempt_nos1) == [1, 2, 3]
odd_thing_attempts = attempts[1::2]
things, attempt_nos2 = zip(*odd_thing_attempts)
assert len(set(things)) == 1
assert list(attempt_nos2) == [1, 2, 3]
class TestContextManager(unittest.TestCase):
@asynctest
async def test_do_max_attempts(self):
attempts = 0
retrying = tasyncio.AsyncRetrying(stop=stop_after_attempt(3))
try:
async for attempt in retrying:
with attempt:
attempts += 1
raise Exception
except RetryError:
pass
assert attempts == 3
@asynctest
async def test_reraise(self):
class CustomError(Exception):
pass
try:
async for attempt in tasyncio.AsyncRetrying(stop=stop_after_attempt(1), reraise=True):
with attempt:
raise CustomError()
except CustomError:
pass
else:
raise Exception
@asynctest
async def test_sleeps(self):
start = current_time_ms()
try:
async for attempt in tasyncio.AsyncRetrying(stop=stop_after_attempt(1), wait=wait_fixed(1)):
with attempt:
raise Exception()
except RetryError:
pass
t = current_time_ms() - start
self.assertLess(t, 1.1)
@asynctest
async def test_retry_with_result(self):
async def test():
attempts = 0
async for attempt in tasyncio.AsyncRetrying(retry=retry_if_result(lambda x: x < 3)):
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)
if __name__ == "__main__":
unittest.main()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tests/test_tenacity.py 0000644 0001751 0000172 00000155250 14466425217 017362 0 ustar 00runner docker # 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
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):
"""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)
_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_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)
fn = tenacity.wait_random_exponential(10, 5)
for _ in range(1000):
self._assert_inclusive_range(fn(make_retry_state(1, 0)), 0.00, 5.00)
# Default arguments exist
fn = tenacity.wait_random_exponential()
fn(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 IOError("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 IOError() 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 IOError("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 IOError 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 IOError("Hi there, I'm an IOError")
class CustomError(Exception):
"""This is a custom exception class.
Note that For Python 2.x, we don't strictly need to extend BaseException,
however, Python 3.x will complain. While this test suite won't run
correctly under Python 3.x without extending from the Python exception
hierarchy, the actual module code is backwards compatible Python 2.x and
will allow for cases where exception classes don't extend from the
hierarchy.
"""
def __init__(self, value):
self.value = value
def __str__(self):
return self.value
class NoCustomErrorAfterCount:
"""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 IOError as re:
self.assertTrue(isinstance(re, IOError))
print(re)
def test_retry_if_exception_of_type(self):
self.assertTrue(_retryable_test_with_exception_type_io(NoIOErrorAfterCount(5)))
try:
_retryable_test_with_exception_type_io(NoNameErrorAfterCount(5))
self.fail("Expected NameError")
except NameError as n:
self.assertTrue(isinstance(n, NameError))
print(n)
self.assertTrue(_retryable_test_with_exception_type_custom(NoCustomErrorAfterCount(5)))
try:
_retryable_test_with_exception_type_custom(NoNameErrorAfterCount(5))
self.fail("Expected NameError")
except NameError as n:
self.assertTrue(isinstance(n, NameError))
print(n)
def test_retry_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 IOError 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.retry.statistics
self.assertTrue(s["attempt_number"] == 6)
print(e)
else:
self.fail("Expected NameError")
def test_retry_until_exception_of_type_no_type(self):
try:
# no input should catch all subclasses of Exception
self.assertTrue(_retryable_test_with_unless_exception_type_no_input(NameErrorUntilCount(5)))
except NameError as e:
s = _retryable_test_with_unless_exception_type_no_input.retry.statistics
self.assertTrue(s["attempt_number"] == 6)
print(e)
else:
self.fail("Expected NameError")
def test_retry_until_exception_of_type_wrong_exception(self):
try:
# two iterations with IOError, one that returns True
_retryable_test_with_unless_exception_type_name_attempt_limit(IOErrorUntilCount(2))
self.fail("Expected RetryError")
except RetryError as e:
self.assertTrue(isinstance(e, RetryError))
print(e)
def test_retry_if_exception_message(self):
try:
self.assertTrue(_retryable_test_if_exception_message_message(NoCustomErrorAfterCount(3)))
except CustomError:
print(_retryable_test_if_exception_message_message.retry.statistics)
self.fail("CustomError should've been retried from errormessage")
def test_retry_if_not_exception_message(self):
try:
self.assertTrue(_retryable_test_if_not_exception_message_message(NoCustomErrorAfterCount(2)))
except CustomError:
s = _retryable_test_if_not_exception_message_message.retry.statistics
self.assertTrue(s["attempt_number"] == 1)
def test_retry_if_not_exception_message_delay(self):
try:
self.assertTrue(_retryable_test_not_exception_message_delay(NameErrorUntilCount(3)))
except NameError:
s = _retryable_test_not_exception_message_delay.retry.statistics
print(s["attempt_number"])
self.assertTrue(s["attempt_number"] == 4)
def test_retry_if_exception_message_match(self):
try:
self.assertTrue(_retryable_test_if_exception_message_match(NoCustomErrorAfterCount(3)))
except CustomError:
self.fail("CustomError should've been retried from errormessage")
def test_retry_if_not_exception_message_match(self):
try:
self.assertTrue(_retryable_test_if_not_exception_message_message(NoCustomErrorAfterCount(2)))
except CustomError:
s = _retryable_test_if_not_exception_message_message.retry.statistics
self.assertTrue(s["attempt_number"] == 1)
def test_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_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")
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 %s keeps raising errors after %s attempts" % (
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.retry.statistics)
_foobar()
self.assertEqual(1, _foobar.retry.statistics["attempt_number"])
def test_stats_failing(self):
@retry(stop=tenacity.stop_after_attempt(2))
def _foobar():
raise ValueError(42)
self.assertEqual({}, _foobar.retry.statistics)
try:
_foobar()
except Exception: # noqa: B902
pass
self.assertEqual(2, _foobar.retry.statistics["attempt_number"])
class TestRetryErrorCallback(unittest.TestCase):
def setUp(self):
self._attempt_number = 0
self._callback_called = False
def _callback(self, fut):
self._callback_called = True
return fut
def test_retry_error_callback(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()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tests/test_tornado.py 0000644 0001751 0000172 00000004162 14466425217 017203 0 ustar 00runner docker # coding: utf-8
# Copyright 2017 Elisey Zanko
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
from tenacity import RetryError, retry, stop_after_attempt
from tenacity import tornadoweb
from 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):
@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()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692019343.0
tenacity-8.2.3/tox.ini 0000644 0001751 0000172 00000002070 14466425217 014271 0 ustar 00runner docker [tox]
envlist = py3{7,8,9,10,11}, pep8, pypy3
skip_missing_interpreters = True
[testenv]
usedevelop = True
sitepackages = False
deps =
.[doc]
pytest
typeguard
commands =
py3{7,8,9,10,11},pypy3: pytest {posargs}
py3{7,8,9,10,11},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build
py3{7,8,9,10,11},pypy3: sphinx-build -a -E -W -b html doc/source doc/build
[testenv:pep8]
basepython = python3
deps = flake8
flake8-import-order
flake8-blind-except
flake8-builtins
flake8-docstrings
flake8-rst-docstrings
flake8-logging-format
commands = flake8
[testenv:black]
deps =
black
commands =
black .
[testenv:mypy]
deps =
mypy>=1.0.0
commands =
mypy tenacity
[testenv:black-ci]
deps =
black
{[testenv:black]deps}
commands =
black --check --diff .
[testenv:reno]
basepython = python3
deps = reno
commands = reno {posargs}
[flake8]
exclude = .tox,.eggs
show-source = true
ignore = D100,D101,D102,D103,D104,D105,D107,G200,G201,W503,W504,E501
enable-extensions=G
max-line-length = 120