You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

tenacity

Package Overview
Dependencies
Maintainers
2
Versions
61
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

tenacity - pypi Package Compare versions

Comparing version
8.2.3
to
8.3.0
+10
.github/dependabot.yml
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'monthly'
groups:
github-actions:
patterns:
- '*'
---
features:
- |
Added a new stop function: stop_before_delay, which will stop execution
if the next sleep time would cause overall delay to exceed the specified delay.
Useful for use cases where you have some upper bound on retry times that you must
not exceed, so returning before that timeout is preferable than returning after that timeout.
---
other:
- |
Add a Dependabot configuration submit PRs monthly (as needed)
to keep GitHub action versions updated.
---
fixes:
- |
Preserve __defaults__ and __kwdefaults__ through retry decorator
import functools
from tenacity import _utils
def test_is_coroutine_callable() -> None:
async def async_func() -> None:
pass
def sync_func() -> None:
pass
class AsyncClass:
async def __call__(self) -> None:
pass
class SyncClass:
def __call__(self) -> None:
pass
lambda_fn = lambda: None # noqa: E731
partial_async_func = functools.partial(async_func)
partial_sync_func = functools.partial(sync_func)
partial_async_class = functools.partial(AsyncClass().__call__)
partial_sync_class = functools.partial(SyncClass().__call__)
partial_lambda_fn = functools.partial(lambda_fn)
assert _utils.is_coroutine_callable(async_func) is True
assert _utils.is_coroutine_callable(sync_func) is False
assert _utils.is_coroutine_callable(AsyncClass) is False
assert _utils.is_coroutine_callable(AsyncClass()) is True
assert _utils.is_coroutine_callable(SyncClass) is False
assert _utils.is_coroutine_callable(SyncClass()) is False
assert _utils.is_coroutine_callable(lambda_fn) is False
assert _utils.is_coroutine_callable(partial_async_func) is True
assert _utils.is_coroutine_callable(partial_sync_func) is False
assert _utils.is_coroutine_callable(partial_async_class) is True
assert _utils.is_coroutine_callable(partial_sync_class) is False
assert _utils.is_coroutine_callable(partial_lambda_fn) is False
+7
-8

@@ -21,4 +21,2 @@ name: Continuous Integration

include:
- python: "3.7"
tox: py37
- python: "3.8"

@@ -32,11 +30,11 @@ tox: py38

tox: py311
- python: "3.11"
- python: "3.12"
tox: py312
- python: "3.12"
tox: pep8
- python: "3.11"
tox: black-ci
- python: "3.11"
tox: mypy
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3.3.0
uses: actions/checkout@v4.1.1
with:

@@ -46,5 +44,6 @@ fetch-depth: 0

- name: Setup Python 🔧
uses: actions/setup-python@v4.5.0
uses: actions/setup-python@v5.0.0
with:
python-version: ${{ matrix.python }}
python-version: ${{ matrix.python }}
allow-prereleases: true

@@ -51,0 +50,0 @@ - name: Build 🔧 & Test 🔍

name: Release deploy
on:
push:
tags:
release:
types:
- published
jobs:
test:
publish:
timeout-minutes: 20
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v3.3.0
uses: actions/checkout@v4.1.1
with:

@@ -18,3 +19,3 @@ fetch-depth: 0

- name: Setup Python 🔧
uses: actions/setup-python@v4.5.0
uses: actions/setup-python@v5.0.0
with:

@@ -21,0 +22,0 @@ python-version: 3.11

queue_rules:
- name: default
conditions: &CheckRuns
- "check-success=test (3.7, py37)"
merge_method: squash
queue_conditions:
- or:
- author = jd
- "#approved-reviews-by >= 1"
- author = dependabot[bot]
- or:
- files ~= ^releasenotes/notes/
- label = no-changelog
- author = dependabot[bot]
- "check-success=test (3.8, py38)"

@@ -9,5 +17,4 @@ - "check-success=test (3.9, py39)"

- "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)"
- "check-success=test (3.12, py312)"
- "check-success=test (3.12, pep8)"

@@ -26,38 +33,8 @@ pull_request_rules:

a changelog entry.
- name: automatic merge without changelog
conditions:
- and: *CheckRuns
- "#approved-reviews-by>=1"
- label=no-changelog
- name: automatic queue
conditions: []
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

@@ -64,0 +41,0 @@ conditions: []

@@ -20,2 +20,5 @@ ===============

.. autoclass:: tenacity.RetryCallState
:members:
After Functions

@@ -22,0 +25,0 @@ ---------------

@@ -127,2 +127,12 @@ Tenacity

If you're on a tight deadline, and exceeding your delay time isn't ok,
then you can give up on retries one attempt before you would exceed the delay.
.. testcode::
@retry(stop=stop_before_delay(10))
def stop_before_10_s():
print("Stopping 1 attempt before 10 seconds")
raise Exception
You can combine several stop conditions by using the `|` operator:

@@ -406,40 +416,4 @@

``retry_state`` argument is an object of `RetryCallState` class:
``retry_state`` argument is an object of :class:`~tenacity.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

@@ -452,3 +426,3 @@ ~~~~~~~~~~~~~~~~~~~~~~

:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation
:return: whether or not retrying should stop

@@ -459,3 +433,3 @@ :rtype: bool

:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation
:return: number of seconds to wait before next retry

@@ -466,3 +440,3 @@ :rtype: float

:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation
:return: whether or not retrying should continue

@@ -473,11 +447,11 @@ :rtype: bool

:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation
.. function:: my_after(retry_state)
:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation
.. function:: my_before_sleep(retry_state)
:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation

@@ -484,0 +458,0 @@ Here's an example with a custom ``before_sleep`` function:

Metadata-Version: 2.1
Name: tenacity
Version: 8.2.3
Version: 8.3.0
Summary: Retry code until it succeeds

@@ -14,3 +14,2 @@ Home-page: https://github.com/jd/tenacity

Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8

@@ -20,7 +19,9 @@ Classifier: Programming Language :: Python :: 3.9

Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Utilities
Requires-Python: >=3.7
Requires-Python: >=3.8
Provides-Extra: doc
Provides-Extra: test
License-File: LICENSE
Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything.

@@ -11,10 +11,10 @@ [build-system]

[tool.black]
line-length = 120
safe = true
target-version = ["py37", "py38", "py39", "py310", "py311"]
[tool.ruff]
line-length = 88
indent-width = 4
target-version = "py38"
[tool.mypy]
strict = true
files = ["tenacity"]
files = ["tenacity", "tests"]
show_error_codes = true

@@ -21,0 +21,0 @@

@@ -127,2 +127,12 @@ Tenacity

If you're on a tight deadline, and exceeding your delay time isn't ok,
then you can give up on retries one attempt before you would exceed the delay.
.. testcode::
@retry(stop=stop_before_delay(10))
def stop_before_10_s():
print("Stopping 1 attempt before 10 seconds")
raise Exception
You can combine several stop conditions by using the `|` operator:

@@ -406,40 +416,4 @@

``retry_state`` argument is an object of `RetryCallState` class:
``retry_state`` argument is an object of :class:`~tenacity.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

@@ -452,3 +426,3 @@ ~~~~~~~~~~~~~~~~~~~~~~

:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation
:return: whether or not retrying should stop

@@ -459,3 +433,3 @@ :rtype: bool

:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation
:return: number of seconds to wait before next retry

@@ -466,3 +440,3 @@ :rtype: float

:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation
:return: whether or not retrying should continue

@@ -473,11 +447,11 @@ :rtype: bool

:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation
.. function:: my_after(retry_state)
:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation
.. function:: my_before_sleep(retry_state)
:param RetryState retry_state: info about current retry invocation
:param RetryCallState retry_state: info about current retry invocation

@@ -484,0 +458,0 @@ Here's an example with a custom ``before_sleep`` function:

@@ -16,3 +16,2 @@ [metadata]

Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8

@@ -22,2 +21,3 @@ Programming Language :: Python :: 3.9

Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Utilities

@@ -27,3 +27,3 @@

install_requires =
python_requires = >=3.7
python_requires = >=3.8
packages = tenacity

@@ -41,3 +41,6 @@

sphinx
test =
pytest
tornado>=4.5
typeguard

@@ -44,0 +47,0 @@ [tool:pytest]

Metadata-Version: 2.1
Name: tenacity
Version: 8.2.3
Version: 8.3.0
Summary: Retry code until it succeeds

@@ -14,3 +14,2 @@ Home-page: https://github.com/jd/tenacity

Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8

@@ -20,7 +19,9 @@ Classifier: Programming Language :: Python :: 3.9

Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Utilities
Requires-Python: >=3.7
Requires-Python: >=3.8
Provides-Extra: doc
Provides-Extra: test
License-File: LICENSE
Tenacity is a general-purpose retrying library to simplify the task of adding retry behavior to just about anything.

@@ -5,2 +5,6 @@

sphinx
[test]
pytest
tornado>=4.5
typeguard

@@ -12,2 +12,3 @@ .editorconfig

tox.ini
.github/dependabot.yml
.github/workflows/ci.yaml

@@ -23,2 +24,4 @@ .github/workflows/deploy.yaml

releasenotes/notes/add-retry_except_exception_type-31b31da1924d55f4.yaml
releasenotes/notes/add-stop-before-delay-a775f88ac872c923.yaml
releasenotes/notes/add-test-extra-55e869261b03e56d.yaml
releasenotes/notes/add_omitted_modules_to_import_all-2ab282f20a2c22f7.yaml

@@ -32,2 +35,3 @@ releasenotes/notes/add_retry_if_exception_cause_type-d16b918ace4ae0ad.yaml

releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml
releasenotes/notes/dependabot-for-github-actions-4d2464f3c0928463.yaml
releasenotes/notes/do_not_package_tests-fe5ac61940b0a5ed.yaml

@@ -46,2 +50,3 @@ releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml

releasenotes/notes/retrycallstate-repr-94947f7b00ee15e1.yaml
releasenotes/notes/some-slug-for-preserve-defaults-86682846dfa18005.yaml
releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml

@@ -72,2 +77,3 @@ releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml

tests/test_tenacity.py
tests/test_tornado.py
tests/test_tornado.py
tests/test_utils.py

@@ -18,4 +18,3 @@ # Copyright 2016-2018 Julien Danjou

# limitations under the License.
import dataclasses
import functools

@@ -54,2 +53,3 @@ import sys

from .stop import stop_after_delay # noqa
from .stop import stop_before_delay # noqa
from .stop import stop_all # noqa

@@ -101,2 +101,25 @@ from .stop import stop_any # noqa

dataclass_kwargs = {}
if sys.version_info >= (3, 10):
dataclass_kwargs.update({"slots": True})
@dataclasses.dataclass(**dataclass_kwargs)
class IterState:
actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field(
default_factory=list
)
retry_run_result: bool = False
delay_since_first_attempt: int = 0
stop_run_result: bool = False
is_explicit_retry: bool = False
def reset(self) -> None:
self.actions = []
self.retry_run_result = False
self.delay_since_first_attempt = 0
self.stop_run_result = False
self.is_explicit_retry = False
class TryAgain(Exception):

@@ -130,3 +153,5 @@ """Always retry the executed function when raised."""

def __repr__(self) -> str:
state_str = ", ".join(f"{field}={getattr(self, field)!r}" for field in self.REPR_FIELDS)
state_str = ", ".join(
f"{field}={getattr(self, field)!r}" for field in self.REPR_FIELDS
)
return f"{self.__class__.__name__}({state_str})"

@@ -227,6 +252,10 @@

after: t.Union[t.Callable[["RetryCallState"], None], object] = _unset,
before_sleep: t.Union[t.Optional[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,
retry_error_callback: t.Union[
t.Optional[t.Callable[["RetryCallState"], t.Any]], object
] = _unset,
) -> "BaseRetrying":

@@ -244,3 +273,5 @@ """Copy this object with some parameters changed if needed."""

retry_error_cls=_first_set(retry_error_cls, self.retry_error_cls),
retry_error_callback=_first_set(retry_error_callback, self.retry_error_callback),
retry_error_callback=_first_set(
retry_error_callback, self.retry_error_callback
),
)

@@ -287,2 +318,10 @@

@property
def iter_state(self) -> IterState:
try:
return self._local.iter_state # type: ignore[no-any-return]
except AttributeError:
self._local.iter_state = IterState()
return self._local.iter_state
def wraps(self, f: WrappedFn) -> WrappedFn:

@@ -294,3 +333,5 @@ """Wrap a function for retrying.

@functools.wraps(f)
@functools.wraps(
f, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__")
)
def wrapped_f(*args: t.Any, **kw: t.Any) -> t.Any:

@@ -313,38 +354,85 @@ return self(f, *args, **kw)

def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None:
self.iter_state.actions.append(fn)
def _run_retry(self, retry_state: "RetryCallState") -> None:
self.iter_state.retry_run_result = self.retry(retry_state)
def _run_wait(self, retry_state: "RetryCallState") -> None:
if self.wait:
sleep = self.wait(retry_state)
else:
sleep = 0.0
retry_state.upcoming_sleep = sleep
def _run_stop(self, retry_state: "RetryCallState") -> None:
self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start
self.iter_state.stop_run_result = self.stop(retry_state)
def iter(self, retry_state: "RetryCallState") -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa
self._begin_iter(retry_state)
result = None
for action in self.iter_state.actions:
result = action(retry_state)
return result
def _begin_iter(self, retry_state: "RetryCallState") -> None: # noqa
self.iter_state.reset()
fut = retry_state.outcome
if fut is None:
if self.before is not None:
self.before(retry_state)
return DoAttempt()
self._add_action_func(self.before)
self._add_action_func(lambda rs: DoAttempt())
return
is_explicit_retry = fut.failed and isinstance(fut.exception(), TryAgain)
if not (is_explicit_retry or self.retry(retry_state)):
return fut.result()
self.iter_state.is_explicit_retry = fut.failed and isinstance(
fut.exception(), TryAgain
)
if not self.iter_state.is_explicit_retry:
self._add_action_func(self._run_retry)
self._add_action_func(self._post_retry_check_actions)
def _post_retry_check_actions(self, retry_state: "RetryCallState") -> None:
if not (self.iter_state.is_explicit_retry or self.iter_state.retry_run_result):
self._add_action_func(lambda rs: rs.outcome.result())
return
if self.after is not None:
self.after(retry_state)
self._add_action_func(self.after)
self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start
if self.stop(retry_state):
self._add_action_func(self._run_wait)
self._add_action_func(self._run_stop)
self._add_action_func(self._post_stop_check_actions)
def _post_stop_check_actions(self, retry_state: "RetryCallState") -> None:
if self.iter_state.stop_run_result:
if self.retry_error_callback:
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()
self._add_action_func(self.retry_error_callback)
return
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
def exc_check(rs: "RetryCallState") -> None:
fut = t.cast(Future, rs.outcome)
retry_exc = self.retry_error_cls(fut)
if self.reraise:
raise retry_exc.reraise()
raise retry_exc from fut.exception()
self._add_action_func(exc_check)
return
def next_action(rs: "RetryCallState") -> None:
sleep = rs.upcoming_sleep
rs.next_action = RetryAction(sleep)
rs.idle_for += sleep
self.statistics["idle_for"] += sleep
self.statistics["attempt_number"] += 1
self._add_action_func(next_action)
if self.before_sleep is not None:
self.before_sleep(retry_state)
self._add_action_func(self.before_sleep)
return DoSleep(sleep)
self._add_action_func(lambda rs: DoSleep(rs.upcoming_sleep))

@@ -403,3 +491,3 @@ def __iter__(self) -> t.Generator[AttemptManager, None, None]:

if sys.version_info[1] >= 9:
if sys.version_info >= (3, 9):
FutureGenericT = futures.Future[t.Any]

@@ -423,3 +511,5 @@ else:

@classmethod
def construct(cls, attempt_number: int, value: t.Any, has_exception: bool) -> "Future":
def construct(
cls, attempt_number: int, value: t.Any, has_exception: bool
) -> "Future":
"""Construct a new Future object."""

@@ -465,2 +555,4 @@ fut = cls(attempt_number)

self.next_action: t.Optional[RetryAction] = None
#: Next sleep time as decided by the retry manager.
self.upcoming_sleep: float = 0.0

@@ -486,3 +578,6 @@ @property

def set_exception(
self, exc_info: t.Tuple[t.Type[BaseException], BaseException, "types.TracebackType| None"]
self,
exc_info: t.Tuple[
t.Type[BaseException], BaseException, "types.TracebackType| None"
],
) -> None:

@@ -509,4 +604,3 @@ ts = time.monotonic()

@t.overload
def retry(func: WrappedFn) -> WrappedFn:
...
def retry(func: WrappedFn) -> WrappedFn: ...

@@ -526,4 +620,3 @@

retry_error_callback: t.Optional[t.Callable[["RetryCallState"], t.Any]] = None,
) -> t.Callable[[WrappedFn], WrappedFn]:
...
) -> t.Callable[[WrappedFn], WrappedFn]: ...

@@ -551,3 +644,7 @@

r = AsyncRetrying(*dargs, **dkw)
elif tornado and hasattr(tornado.gen, "is_coroutine_function") and tornado.gen.is_coroutine_function(f):
elif (
tornado
and hasattr(tornado.gen, "is_coroutine_function")
and tornado.gen.is_coroutine_function(f)
):
r = TornadoRetrying(*dargs, **dkw)

@@ -587,2 +684,3 @@ else:

"stop_after_delay",
"stop_before_delay",
"stop_all",

@@ -589,0 +687,0 @@ "stop_any",

@@ -21,3 +21,2 @@ # Copyright 2016 Étienne Bersac

import typing as t
from asyncio import sleep

@@ -29,2 +28,3 @@ from tenacity import AttemptManager

from tenacity import RetryCallState
from tenacity import _utils

@@ -35,6 +35,17 @@ WrappedFnReturnT = t.TypeVar("WrappedFnReturnT")

def asyncio_sleep(duration: float) -> t.Awaitable[None]:
# Lazy import asyncio as it's expensive (responsible for 25-50% of total import overhead).
import asyncio
return asyncio.sleep(duration)
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:
def __init__(
self,
sleep: t.Callable[[float], t.Awaitable[t.Any]] = asyncio_sleep,
**kwargs: t.Any,
) -> None:
super().__init__(**kwargs)

@@ -50,3 +61,3 @@ self.sleep = sleep

while True:
do = self.iter(retry_state=retry_state)
do = await self.iter(retry_state=retry_state)
if isinstance(do, DoAttempt):

@@ -65,2 +76,43 @@ try:

@classmethod
def _wrap_action_func(cls, fn: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
if _utils.is_coroutine_callable(fn):
return fn
async def inner(*args: t.Any, **kwargs: t.Any) -> t.Any:
return fn(*args, **kwargs)
return inner
def _add_action_func(self, fn: t.Callable[..., t.Any]) -> None:
self.iter_state.actions.append(self._wrap_action_func(fn))
async def _run_retry(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
self.iter_state.retry_run_result = await self._wrap_action_func(self.retry)(
retry_state
)
async def _run_wait(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
if self.wait:
sleep = await self._wrap_action_func(self.wait)(retry_state)
else:
sleep = 0.0
retry_state.upcoming_sleep = sleep
async def _run_stop(self, retry_state: "RetryCallState") -> None: # type: ignore[override]
self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start
self.iter_state.stop_run_result = await self._wrap_action_func(self.stop)(
retry_state
)
async def iter(
self, retry_state: "RetryCallState"
) -> t.Union[DoAttempt, DoSleep, t.Any]: # noqa: A003
self._begin_iter(retry_state)
result = None
for action in self.iter_state.actions:
result = await action(retry_state)
return result
def __iter__(self) -> t.Generator[AttemptManager, None, None]:

@@ -76,3 +128,3 @@ raise TypeError("AsyncRetrying object is not iterable")

while True:
do = self.iter(retry_state=self._retry_state)
do = await self.iter(retry_state=self._retry_state)
if do is None:

@@ -92,3 +144,5 @@ raise StopAsyncIteration

@functools.wraps(fn)
@functools.wraps(
fn, functools.WRAPPER_ASSIGNMENTS + ("__defaults__", "__kwdefaults__")
)
async def async_wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any:

@@ -95,0 +149,0 @@ return await fn(*args, **kwargs)

@@ -16,3 +16,4 @@ # Copyright 2016 Julien Danjou

# limitations under the License.
import functools
import inspect
import sys

@@ -77,2 +78,14 @@ import typing

def to_seconds(time_unit: time_unit_type) -> float:
return float(time_unit.total_seconds() if isinstance(time_unit, timedelta) else time_unit)
return float(
time_unit.total_seconds() if isinstance(time_unit, timedelta) else time_unit
)
def is_coroutine_callable(call: typing.Callable[..., typing.Any]) -> bool:
if inspect.isclass(call):
return False
if inspect.iscoroutinefunction(call):
return True
partial_call = isinstance(call, functools.partial) and call.func
dunder_call = partial_call or getattr(call, "__call__", None)
return inspect.iscoroutinefunction(dunder_call)

@@ -67,3 +67,4 @@ # Copyright 2016 Julien Danjou

log_level,
f"Retrying {fn_name} " f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.",
f"Retrying {fn_name} "
f"in {retry_state.next_action.sleep} seconds as it {verb} {value}.",
exc_info=local_exc_info,

@@ -70,0 +71,0 @@ )

@@ -31,3 +31,5 @@ # Copyright 2016 Julien Danjou

def before_log(logger: "logging.Logger", log_level: int) -> typing.Callable[["RetryCallState"], None]:
def before_log(
logger: "logging.Logger", log_level: int
) -> typing.Callable[["RetryCallState"], None]:
"""Before call strategy that logs to some logger the attempt."""

@@ -34,0 +36,0 @@

@@ -207,3 +207,5 @@ # Copyright 2016–2021 Julien Danjou

if message and match:
raise TypeError(f"{self.__class__.__name__}() takes either 'message' or 'match', not both")
raise TypeError(
f"{self.__class__.__name__}() takes either 'message' or 'match', not both"
)

@@ -225,3 +227,5 @@ # set predicate

else:
raise TypeError(f"{self.__class__.__name__}() missing 1 required argument 'message' or 'match'")
raise TypeError(
f"{self.__class__.__name__}() missing 1 required argument 'message' or 'match'"
)

@@ -228,0 +232,0 @@ super().__init__(predicate)

@@ -95,4 +95,11 @@ # Copyright 2016–2021 Julien Danjou

class stop_after_delay(stop_base):
"""Stop when the time from the first attempt >= limit."""
"""
Stop when the time from the first attempt >= limit.
Note: `max_delay` will be exceeded, so when used with a `wait`, the actual total delay will be greater
than `max_delay` by some of the final sleep period before `max_delay` is exceeded.
If you need stricter timing with waits, consider `stop_before_delay` instead.
"""
def __init__(self, max_delay: _utils.time_unit_type) -> None:

@@ -105,1 +112,21 @@ self.max_delay = _utils.to_seconds(max_delay)

return retry_state.seconds_since_start >= self.max_delay
class stop_before_delay(stop_base):
"""
Stop right before the next attempt would take place after the time from the first attempt >= limit.
Most useful when you are using with a `wait` function like wait_random_exponential, but need to make
sure that the max_delay is not exceeded.
"""
def __init__(self, max_delay: _utils.time_unit_type) -> None:
self.max_delay = _utils.to_seconds(max_delay)
def __call__(self, retry_state: "RetryCallState") -> bool:
if retry_state.seconds_since_start is None:
raise RuntimeError("__call__() called but seconds_since_start is not set")
return (
retry_state.seconds_since_start + retry_state.upcoming_sleep
>= self.max_delay
)

@@ -32,3 +32,7 @@ # Copyright 2017 Elisey Zanko

class TornadoRetrying(BaseRetrying):
def __init__(self, sleep: "typing.Callable[[float], Future[None]]" = gen.sleep, **kwargs: typing.Any) -> None:
def __init__(
self,
sleep: "typing.Callable[[float], Future[None]]" = gen.sleep,
**kwargs: typing.Any,
) -> None:
super().__init__(**kwargs)

@@ -35,0 +39,0 @@ self.sleep = sleep

@@ -44,3 +44,5 @@ # Copyright 2016–2021 Julien Danjou

WaitBaseT = typing.Union[wait_base, typing.Callable[["RetryCallState"], typing.Union[float, int]]]
WaitBaseT = typing.Union[
wait_base, typing.Callable[["RetryCallState"], typing.Union[float, int]]
]

@@ -68,3 +70,5 @@

def __init__(self, min: _utils.time_unit_type = 0, max: _utils.time_unit_type = 1) -> None: # noqa
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)

@@ -74,3 +78,5 @@ 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))
return self.wait_random_min + (
random.random() * (self.wait_random_max - self.wait_random_min)
)

@@ -77,0 +83,0 @@

@@ -0,1 +1,2 @@

# mypy: disable-error-code="no-untyped-def,no-untyped-call"
import logging

@@ -13,3 +14,11 @@ import random

def setUp(self) -> None:
self.log_level = random.choice((logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL))
self.log_level = random.choice(
(
logging.DEBUG,
logging.INFO,
logging.WARNING,
logging.ERROR,
logging.CRITICAL,
)
)
self.previous_attempt_number = random.randint(1, 512)

@@ -25,6 +34,14 @@

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
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 = "<unknown>" if retry_state.fn is None else _utils.get_callback_name(retry_state.fn)
fn_name = (
"<unknown>"
if retry_state.fn is None
else _utils.get_callback_name(retry_state.fn)
)
log.assert_called_once_with(

@@ -45,6 +62,12 @@ self.log_level,

retry_state = test_tenacity.make_retry_state(self.previous_attempt_number, delay_since_first_attempt)
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 = "<unknown>" if retry_state.fn is None else _utils.get_callback_name(retry_state.fn)
fn_name = (
"<unknown>"
if retry_state.fn is None
else _utils.get_callback_name(retry_state.fn)
)
log.assert_called_once_with(

@@ -51,0 +74,0 @@ self.log_level,

@@ -1,2 +0,2 @@

# coding: utf-8
# mypy: disable-error-code="no-untyped-def,no-untyped-call"
# Copyright 2016 Étienne Bersac

@@ -23,2 +23,3 @@ #

import tenacity
from tenacity import AsyncRetrying, RetryError

@@ -92,2 +93,23 @@ from tenacity import _asyncio as tasyncio

def test_retry_preserves_argument_defaults(self):
async def function_with_defaults(a=1):
return a
async def function_with_kwdefaults(*, a=1):
return a
retrying = AsyncRetrying(
wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3)
)
wrapped_defaults_function = retrying.wraps(function_with_defaults)
wrapped_kwdefaults_function = retrying.wraps(function_with_kwdefaults)
self.assertEqual(
function_with_defaults.__defaults__, wrapped_defaults_function.__defaults__
)
self.assertEqual(
function_with_kwdefaults.__kwdefaults__,
wrapped_kwdefaults_function.__kwdefaults__,
)
@asynctest

@@ -104,4 +126,4 @@ async def test_attempt_number_is_correct_for_interleaved_coroutines(self):

await asyncio.gather(
_retryable_coroutine.retry_with(after=after)(thing1),
_retryable_coroutine.retry_with(after=after)(thing2),
_retryable_coroutine.retry_with(after=after)(thing1), # type: ignore[attr-defined]
_retryable_coroutine.retry_with(after=after)(thing2), # type: ignore[attr-defined]
)

@@ -143,3 +165,5 @@

try:
async for attempt in tasyncio.AsyncRetrying(stop=stop_after_attempt(1), reraise=True):
async for attempt in tasyncio.AsyncRetrying(
stop=stop_after_attempt(1), reraise=True
):
with attempt:

@@ -156,3 +180,5 @@ raise CustomError()

try:
async for attempt in tasyncio.AsyncRetrying(stop=stop_after_attempt(1), wait=wait_fixed(1)):
async for attempt in tasyncio.AsyncRetrying(
stop=stop_after_attempt(1), wait=wait_fixed(1)
):
with attempt:

@@ -169,3 +195,8 @@ raise Exception()

attempts = 0
async for attempt in tasyncio.AsyncRetrying(retry=retry_if_result(lambda x: x < 3)):
# mypy doesn't have great lambda support
def lt_3(x: float) -> bool:
return x < 3
async for attempt in tasyncio.AsyncRetrying(retry=retry_if_result(lt_3)):
with attempt:

@@ -189,3 +220,14 @@ attempts += 1

# make sure mypy accepts passing an async sleep function
# https://github.com/jd/tenacity/issues/399
async def my_async_sleep(x: float) -> None:
await asyncio.sleep(x)
@retry(sleep=my_async_sleep)
async def foo():
pass
if __name__ == "__main__":
unittest.main()

@@ -0,1 +1,2 @@

# mypy: disable_error_code="no-untyped-def,no-untyped-call,attr-defined,arg-type,no-any-return,list-item,var-annotated,import,call-overload"
# Copyright 2016–2021 Julien Danjou

@@ -53,3 +54,8 @@ # Copyright 2016 Joshua Harlow

def make_retry_state(previous_attempt_number, delay_since_first_attempt, last_result=None):
def make_retry_state(
previous_attempt_number,
delay_since_first_attempt,
last_result=None,
upcoming_sleep=0,
):
"""Construct RetryCallState for given attempt number & delay.

@@ -59,3 +65,5 @@

"""
required_parameter_unset = previous_attempt_number is _unset or delay_since_first_attempt is _unset
required_parameter_unset = (
previous_attempt_number is _unset or delay_since_first_attempt is _unset
)
if required_parameter_unset:

@@ -74,2 +82,5 @@ raise _make_unset_exception(

retry_state.set_result(None)
retry_state.upcoming_sleep = upcoming_sleep
_set_delay_since_start(retry_state, delay_since_first_attempt)

@@ -92,5 +103,11 @@ return retry_state

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)>")
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)>"
)

@@ -104,3 +121,5 @@

def test_stop_any(self):
stop = tenacity.stop_any(tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4))
stop = tenacity.stop_any(
tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4)
)

@@ -118,3 +137,5 @@ def s(*args):

def test_stop_all(self):
stop = tenacity.stop_all(tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4))
stop = tenacity.stop_all(
tenacity.stop_after_delay(1), tenacity.stop_after_attempt(4)
)

@@ -171,2 +192,17 @@ def s(*args):

def test_stop_before_delay(self):
for delay in (1, datetime.timedelta(seconds=1)):
with self.subTest():
r = Retrying(stop=tenacity.stop_before_delay(delay))
self.assertFalse(
r.stop(make_retry_state(2, 0.999, upcoming_sleep=0.0001))
)
self.assertTrue(r.stop(make_retry_state(2, 1, upcoming_sleep=0.001)))
self.assertTrue(r.stop(make_retry_state(2, 1, upcoming_sleep=1)))
# It should act the same as stop_after_delay if upcoming sleep is 0
self.assertFalse(r.stop(make_retry_state(2, 0.999, upcoming_sleep=0)))
self.assertTrue(r.stop(make_retry_state(2, 1, upcoming_sleep=0)))
self.assertTrue(r.stop(make_retry_state(2, 1.001, upcoming_sleep=0)))
def test_legacy_explicit_stop_type(self):

@@ -198,5 +234,10 @@ Retrying(stop="stop_after_attempt")

def test_incrementing_sleep(self):
for start, increment in ((500, 100), (datetime.timedelta(seconds=500), datetime.timedelta(seconds=100))):
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))
r = Retrying(
wait=tenacity.wait_incrementing(start=start, increment=increment)
)
self.assertEqual(500, r.wait(make_retry_state(1, 6546)))

@@ -207,3 +248,6 @@ self.assertEqual(600, r.wait(make_retry_state(2, 6546)))

def test_random_sleep(self):
for min_, max_ in ((1, 20), (datetime.timedelta(seconds=1), datetime.timedelta(seconds=20))):
for min_, max_ in (
(1, 20),
(datetime.timedelta(seconds=1), datetime.timedelta(seconds=20)),
):
with self.subTest():

@@ -295,3 +339,6 @@ r = Retrying(wait=tenacity.wait_random(min=min_, max=max_))

def test_exponential_with_min_wait_andmax__wait(self):
for min_, max_ in ((10, 100), (datetime.timedelta(seconds=10), datetime.timedelta(seconds=100))):
for min_, max_ in (
(10, 100),
(datetime.timedelta(seconds=10), datetime.timedelta(seconds=100)),
):
with self.subTest():

@@ -323,3 +370,7 @@ r = Retrying(wait=tenacity.wait_exponential(min=min_, max=max_))

def test_wait_combine(self):
r = Retrying(wait=tenacity.wait_combine(tenacity.wait_random(0, 3), tenacity.wait_fixed(5)))
r = Retrying(
wait=tenacity.wait_combine(
tenacity.wait_random(0, 3), tenacity.wait_fixed(5)
)
)
# Test it a few time since it's random

@@ -340,3 +391,7 @@ for i in range(1000):

def test_wait_triple_sum(self):
r = Retrying(wait=tenacity.wait_fixed(1) + tenacity.wait_random(0, 3) + tenacity.wait_fixed(5))
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

@@ -493,3 +548,6 @@ for i in range(1000):

wait=waitfunc,
retry=(tenacity.retry_if_exception_type() | tenacity.retry_if_result(lambda result: result == 123)),
retry=(
tenacity.retry_if_exception_type()
| tenacity.retry_if_result(lambda result: result == 123)
),
)

@@ -578,3 +636,5 @@

def test_retry_and(self):
retry = tenacity.retry_if_result(lambda x: x == 1) & tenacity.retry_if_result(lambda x: isinstance(x, int))
retry = tenacity.retry_if_result(lambda x: x == 1) & tenacity.retry_if_result(
lambda x: isinstance(x, int)
)

@@ -591,3 +651,5 @@ def r(fut):

def test_retry_or(self):
retry = tenacity.retry_if_result(lambda x: x == "foo") | tenacity.retry_if_result(lambda x: isinstance(x, int))
retry = tenacity.retry_if_result(
lambda x: x == "foo"
) | tenacity.retry_if_result(lambda x: isinstance(x, int))

@@ -610,3 +672,5 @@ def r(fut):

self._attempts = 0
Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never)(self._raise_try_again)
Retrying(stop=tenacity.stop_after_attempt(5), retry=tenacity.retry_never)(
self._raise_try_again
)
self.assertEqual(3, self._attempts)

@@ -675,3 +739,3 @@

self.counter += 1
raise IOError("Hi there, I'm an IOError")
raise OSError("Hi there, I'm an IOError")
return True

@@ -718,3 +782,3 @@

except NameError as e:
raise IOError() from e
raise OSError() from e

@@ -732,3 +796,3 @@ return True

def go2(self):
raise IOError("Hi there, I'm an IOError")
raise OSError("Hi there, I'm an IOError")

@@ -744,3 +808,3 @@ def go(self):

self.go2()
except IOError as e:
except OSError as e:
raise NameError() from e

@@ -786,3 +850,3 @@

return True
raise IOError("Hi there, I'm an IOError")
raise OSError("Hi there, I'm an IOError")

@@ -874,3 +938,5 @@

@retry(stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(IOError))
@retry(
stop=tenacity.stop_after_attempt(3), retry=tenacity.retry_if_exception_type(IOError)
)
def _retryable_test_with_exception_type_io_attempt_limit(thing):

@@ -900,3 +966,5 @@ return thing.go()

stop=tenacity.stop_after_attempt(5),
retry=tenacity.retry_if_exception_message(message=NoCustomErrorAfterCount.derived_message),
retry=tenacity.retry_if_exception_message(
message=NoCustomErrorAfterCount.derived_message
),
)

@@ -907,3 +975,7 @@ def _retryable_test_if_exception_message_message(thing):

@retry(retry=tenacity.retry_if_not_exception_message(message=NoCustomErrorAfterCount.derived_message))
@retry(
retry=tenacity.retry_if_not_exception_message(
message=NoCustomErrorAfterCount.derived_message
)
)
def _retryable_test_if_not_exception_message_message(thing):

@@ -913,3 +985,7 @@ return thing.go()

@retry(retry=tenacity.retry_if_exception_message(match=NoCustomErrorAfterCount.derived_message[:3] + ".*"))
@retry(
retry=tenacity.retry_if_exception_message(
match=NoCustomErrorAfterCount.derived_message[:3] + ".*"
)
)
def _retryable_test_if_exception_message_match(thing):

@@ -919,3 +995,7 @@ return thing.go()

@retry(retry=tenacity.retry_if_not_exception_message(match=NoCustomErrorAfterCount.derived_message[:3] + ".*"))
@retry(
retry=tenacity.retry_if_not_exception_message(
match=NoCustomErrorAfterCount.derived_message[:3] + ".*"
)
)
def _retryable_test_if_not_exception_message_match(thing):

@@ -925,3 +1005,7 @@ return thing.go()

@retry(retry=tenacity.retry_if_not_exception_message(message=NameErrorUntilCount.derived_message))
@retry(
retry=tenacity.retry_if_not_exception_message(
message=NameErrorUntilCount.derived_message
)
)
def _retryable_test_not_exception_message_delay(thing):

@@ -976,3 +1060,3 @@ return thing.go()

self.fail("Expected IOError")
except IOError as re:
except OSError as re:
self.assertTrue(isinstance(re, IOError))

@@ -991,3 +1075,5 @@ print(re)

self.assertTrue(_retryable_test_with_exception_type_custom(NoCustomErrorAfterCount(5)))
self.assertTrue(
_retryable_test_with_exception_type_custom(NoCustomErrorAfterCount(5))
)

@@ -1002,3 +1088,5 @@ try:

def test_retry_except_exception_of_type(self):
self.assertTrue(_retryable_test_if_not_exception_type_io(NoNameErrorAfterCount(5)))
self.assertTrue(
_retryable_test_if_not_exception_type_io(NoNameErrorAfterCount(5))
)

@@ -1008,3 +1096,3 @@ try:

self.fail("Expected IOError")
except IOError as err:
except OSError as err:
self.assertTrue(isinstance(err, IOError))

@@ -1015,3 +1103,5 @@ print(err)

try:
self.assertTrue(_retryable_test_with_unless_exception_type_name(NameErrorUntilCount(5)))
self.assertTrue(
_retryable_test_with_unless_exception_type_name(NameErrorUntilCount(5))
)
except NameError as e:

@@ -1027,3 +1117,7 @@ s = _retryable_test_with_unless_exception_type_name.retry.statistics

# no input should catch all subclasses of Exception
self.assertTrue(_retryable_test_with_unless_exception_type_no_input(NameErrorUntilCount(5)))
self.assertTrue(
_retryable_test_with_unless_exception_type_no_input(
NameErrorUntilCount(5)
)
)
except NameError as e:

@@ -1039,3 +1133,5 @@ s = _retryable_test_with_unless_exception_type_no_input.retry.statistics

# two iterations with IOError, one that returns True
_retryable_test_with_unless_exception_type_name_attempt_limit(IOErrorUntilCount(2))
_retryable_test_with_unless_exception_type_name_attempt_limit(
IOErrorUntilCount(2)
)
self.fail("Expected RetryError")

@@ -1048,3 +1144,5 @@ except RetryError as e:

try:
self.assertTrue(_retryable_test_if_exception_message_message(NoCustomErrorAfterCount(3)))
self.assertTrue(
_retryable_test_if_exception_message_message(NoCustomErrorAfterCount(3))
)
except CustomError:

@@ -1056,3 +1154,7 @@ print(_retryable_test_if_exception_message_message.retry.statistics)

try:
self.assertTrue(_retryable_test_if_not_exception_message_message(NoCustomErrorAfterCount(2)))
self.assertTrue(
_retryable_test_if_not_exception_message_message(
NoCustomErrorAfterCount(2)
)
)
except CustomError:

@@ -1064,3 +1166,5 @@ s = _retryable_test_if_not_exception_message_message.retry.statistics

try:
self.assertTrue(_retryable_test_not_exception_message_delay(NameErrorUntilCount(3)))
self.assertTrue(
_retryable_test_not_exception_message_delay(NameErrorUntilCount(3))
)
except NameError:

@@ -1073,3 +1177,5 @@ s = _retryable_test_not_exception_message_delay.retry.statistics

try:
self.assertTrue(_retryable_test_if_exception_message_match(NoCustomErrorAfterCount(3)))
self.assertTrue(
_retryable_test_if_exception_message_match(NoCustomErrorAfterCount(3))
)
except CustomError:

@@ -1080,3 +1186,7 @@ self.fail("CustomError should've been retried from errormessage")

try:
self.assertTrue(_retryable_test_if_not_exception_message_message(NoCustomErrorAfterCount(2)))
self.assertTrue(
_retryable_test_if_not_exception_message_message(
NoCustomErrorAfterCount(2)
)
)
except CustomError:

@@ -1087,3 +1197,5 @@ s = _retryable_test_if_not_exception_message_message.retry.statistics

def test_retry_if_exception_cause_type(self):
self.assertTrue(_retryable_test_with_exception_cause_type(NoNameErrorCauseAfterCount(5)))
self.assertTrue(
_retryable_test_with_exception_cause_type(NoNameErrorCauseAfterCount(5))
)

@@ -1096,2 +1208,23 @@ try:

def test_retry_preserves_argument_defaults(self):
def function_with_defaults(a=1):
return a
def function_with_kwdefaults(*, a=1):
return a
retrying = Retrying(
wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3)
)
wrapped_defaults_function = retrying.wraps(function_with_defaults)
wrapped_kwdefaults_function = retrying.wraps(function_with_kwdefaults)
self.assertEqual(
function_with_defaults.__defaults__, wrapped_defaults_function.__defaults__
)
self.assertEqual(
function_with_kwdefaults.__kwdefaults__,
wrapped_kwdefaults_function.__kwdefaults__,
)
def test_defaults(self):

@@ -1114,3 +1247,5 @@ self.assertTrue(_retryable_default(NoNameErrorAfterCount(5)))

retrying = Retrying(wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3))
retrying = Retrying(
wait=tenacity.wait_fixed(0.01), stop=tenacity.stop_after_attempt(3)
)
h = retrying.wraps(Hello())

@@ -1123,3 +1258,5 @@ self.assertEqual(h(), "Hello")

start = current_time_ms()
result = _retryable_test_with_wait.retry_with(wait=tenacity.wait_fixed(0.1))(NoneReturnUntilAfterCount(5))
result = _retryable_test_with_wait.retry_with(wait=tenacity.wait_fixed(0.1))(
NoneReturnUntilAfterCount(5)
)
t = current_time_ms() - start

@@ -1130,3 +1267,5 @@ assert t >= 500

def test_redefine_stop(self):
result = _retryable_test_with_stop.retry_with(stop=tenacity.stop_after_attempt(5))(NoneReturnUntilAfterCount(4))
result = _retryable_test_with_stop.retry_with(
stop=tenacity.stop_after_attempt(5)
)(NoneReturnUntilAfterCount(4))
assert result is True

@@ -1146,3 +1285,3 @@

def return_text(retry_state):
return "Calling %s keeps raising errors after %s attempts" % (
return "Calling {} keeps raising errors after {} attempts".format(
retry_state.fn.__name__,

@@ -1237,3 +1376,6 @@ retry_state.attempt_number,

etalon_re = r"^Retrying .* in 0\.01 seconds as it raised " r"(IO|OS)Error: Hi there, I'm an IOError\.$"
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)

@@ -1255,3 +1397,5 @@ fmt = logging.Formatter().format

try:
_before_sleep = tenacity.before_sleep_log(logger, logging.INFO, exc_info=True)
_before_sleep = tenacity.before_sleep_log(
logger, logging.INFO, exc_info=True
)
retrying = Retrying(

@@ -1286,3 +1430,5 @@ wait=tenacity.wait_fixed(0.01),

try:
_before_sleep = tenacity.before_sleep_log(logger, logging.INFO, exc_info=exc_info)
_before_sleep = tenacity.before_sleep_log(
logger, logging.INFO, exc_info=exc_info
)
_retry = tenacity.retry_if_result(lambda result: result is None)

@@ -1570,3 +1716,5 @@ retrying = Retrying(

class TestRetryTyping(unittest.TestCase):
@pytest.mark.skipif(sys.version_info < (3, 0), reason="typeguard not supported for python 2")
@pytest.mark.skipif(
sys.version_info < (3, 0), reason="typeguard not supported for python 2"
)
def test_retry_type_annotations(self):

@@ -1573,0 +1721,0 @@ """The decorator should maintain types of decorated functions."""

@@ -1,2 +0,2 @@

# coding: utf-8
# mypy: disable-error-code="no-untyped-def,no-untyped-call"
# Copyright 2017 Elisey Zanko

@@ -41,3 +41,3 @@ #

class TestTornado(testing.AsyncTestCase):
class TestTornado(testing.AsyncTestCase): # type: ignore[misc]
@testing.gen_test

@@ -44,0 +44,0 @@ def test_retry(self):

+11
-35
[tox]
envlist = py3{7,8,9,10,11}, pep8, pypy3
envlist = py3{8,9,10,11,12}, pep8, pypy3
skip_missing_interpreters = True

@@ -9,26 +9,15 @@

deps =
.[test]
.[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
py3{8,9,10,11,12},pypy3: pytest {posargs}
py3{8,9,10,11,12},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build
py3{8,9,10,11,12},pypy3: sphinx-build -a -E -W -b html doc/source doc/build
[testenv:pep8]
basepython = python3
deps = flake8
flake8-import-order
flake8-blind-except
flake8-builtins
flake8-docstrings
flake8-rst-docstrings
flake8-logging-format
commands = flake8
[testenv:black]
deps =
black
deps = ruff
commands =
black .
ruff check . {posargs}
ruff format --check . {posargs}

@@ -38,22 +27,9 @@ [testenv:mypy]

mypy>=1.0.0
pytest # for stubs
commands =
mypy tenacity
mypy {posargs}
[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
commands = reno {posargs}