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
9.1.2
to
9.1.3
+7
releasenotes/notes/async-sleep-retrying-32de5866f5d041.yaml
---
fixes:
- |
Passing an async ``sleep`` callable (e.g. ``trio.sleep``) to ``@retry``
now correctly uses ``AsyncRetrying``, even when the decorated function is
synchronous. Previously, the async sleep would silently not be awaited,
resulting in no delay between retries.
---
upgrade:
- |
Python 3.9 has reached end-of-life and is no longer supported.
The minimum supported version is now Python 3.10.
---
other:
- |
Accept non-standard logger in helpers logging something (eg: structlog, loguru...)
---
features:
- Python 3.14 support has been added.
+8
-8

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

include:
- python: "3.9"
tox: py39
- python: "3.10"

@@ -30,11 +28,13 @@ tox: py310

tox: py312
- python: "3.12"
- python: "3.13"
tox: py313
- python: "3.14"
tox: py314,py314-trio
- python: "3.14"
tox: pep8
- python: "3.13"
tox: py313,py313-trio
- python: "3.11"
- python: "3.14"
tox: mypy
steps:
- name: Checkout 🛎️
uses: actions/checkout@v4.2.2
uses: actions/checkout@v6.0.2
with:

@@ -44,3 +44,3 @@ fetch-depth: 0

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

@@ -47,0 +47,0 @@ python-version: ${{ matrix.python }}

@@ -16,3 +16,3 @@ name: upload release to PyPI

steps:
- uses: actions/checkout@v4.2.2
- uses: actions/checkout@v6.0.2
with:

@@ -22,5 +22,5 @@ fetch-depth: 0

- uses: actions/setup-python@v5.5.0
- uses: actions/setup-python@v6.2.0
with:
python-version: 3.13
python-version: 3.14

@@ -27,0 +27,0 @@ - name: Install build

queue_rules:
- name: default
merge_method: squash
autoqueue: true
queue_conditions:

@@ -9,31 +10,10 @@ - or:

- author = dependabot[bot]
- or:
- files ~= ^releasenotes/notes/
- label = no-changelog
- author = dependabot[bot]
- "check-success=test (3.9, py39)"
- "check-success=test (3.10, py310)"
- "check-success=test (3.11, py311)"
- "check-success=test (3.12, py312)"
- "check-success=test (3.13, py313,py313-trio)"
- "check-success=test (3.12, pep8)"
- "check-success=test (3.13, py313)"
- "check-success=test (3.14, py314,py314-trio)"
- "check-success=test (3.14, pep8)"
pull_request_rules:
- name: warn on no changelog
conditions:
- -files~=^releasenotes/notes/
- label!=no-changelog
- -closed
actions:
comment:
message: >
⚠️ No release notes detected. Please make sure to use
[reno](https://docs.openstack.org/reno/latest/user/usage.html) to add
a changelog entry.
- name: automatic queue
conditions: []
actions:
queue:
- name: dismiss reviews

@@ -40,0 +20,0 @@ conditions: []

@@ -640,15 +640,1 @@ Tenacity

.. _`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
Metadata-Version: 2.4
Name: tenacity
Version: 9.1.2
Version: 9.1.3
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.9
Classifier: Programming Language :: Python :: 3.10

@@ -20,4 +19,5 @@ Classifier: Programming Language :: Python :: 3.11

Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Requires-Python: >=3.10
License-File: LICENSE

@@ -24,0 +24,0 @@ Provides-Extra: doc

@@ -14,3 +14,3 @@ [build-system]

indent-width = 4
target-version = "py39"
target-version = "py310"

@@ -17,0 +17,0 @@ [tool.mypy]

@@ -640,15 +640,1 @@ Tenacity

.. _`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

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

Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10

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

Programming Language :: Python :: 3.13
Programming Language :: Python :: 3.14
Topic :: Utilities

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

install_requires =
python_requires = >=3.9
python_requires = >=3.10
packages = find:

@@ -30,0 +30,0 @@

Metadata-Version: 2.4
Name: tenacity
Version: 9.1.2
Version: 9.1.3
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.9
Classifier: Programming Language :: Python :: 3.10

@@ -20,4 +19,5 @@ Classifier: Programming Language :: Python :: 3.11

Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Requires-Python: >=3.10
License-File: LICENSE

@@ -24,0 +24,0 @@ Provides-Extra: doc

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

releasenotes/notes/annotate_code-197b93130df14042.yaml
releasenotes/notes/async-sleep-retrying-32de5866f5d041.yaml
releasenotes/notes/before_sleep_log-improvements-d8149274dfb37d7c.yaml

@@ -39,2 +40,3 @@ releasenotes/notes/clarify-reraise-option-6829667eacf4f599.yaml

releasenotes/notes/drop-deprecated-python-versions-69a05cb2e0f1034c.yaml
releasenotes/notes/drop-python-3.9-ecfa2d7db9773e96.yaml
releasenotes/notes/drop_deprecated-7ea90b212509b082.yaml

@@ -48,2 +50,3 @@ releasenotes/notes/export-convenience-symbols-981d9611c8b754f3.yaml

releasenotes/notes/fix_async-52b6594c8e75c4bc.yaml
releasenotes/notes/logging-protocol-a4cf0f786f21e4ee.yaml
releasenotes/notes/make-logger-more-compatible-5da1ddf1bab77047.yaml

@@ -57,2 +60,3 @@ releasenotes/notes/no-async-iter-6132a42e52348a75.yaml

releasenotes/notes/sphinx_define_error-642c9cd5c165d39a.yaml
releasenotes/notes/support-py3.14-14928188cab53b99.yaml
releasenotes/notes/support-timedelta-wait-unit-type-5ba1e9fc0fe45523.yaml

@@ -59,0 +63,0 @@ releasenotes/notes/timedelta-for-stop-ef6bf71b88ce9988.yaml

@@ -62,2 +62,3 @@ # Copyright 2016-2018 Julien Danjou

from .wait import wait_combine # noqa
from .wait import wait_exception # noqa
from .wait import wait_exponential # noqa

@@ -102,10 +103,7 @@ from .wait import wait_fixed # noqa

WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable[..., t.Any])
P = t.ParamSpec("P")
R = t.TypeVar("R")
dataclass_kwargs = {}
if sys.version_info >= (3, 10):
dataclass_kwargs.update({"slots": True})
@dataclasses.dataclass(**dataclass_kwargs)
@dataclasses.dataclass(slots=True)
class IterState:

@@ -312,15 +310,11 @@ actions: t.List[t.Callable[["RetryCallState"], t.Any]] = dataclasses.field(

"""
try:
return self._local.statistics # type: ignore[no-any-return]
except AttributeError:
if not hasattr(self._local, "statistics"):
self._local.statistics = t.cast(t.Dict[str, t.Any], {})
return self._local.statistics
return self._local.statistics # type: ignore[no-any-return]
@property
def iter_state(self) -> IterState:
try:
return self._local.iter_state # type: ignore[no-any-return]
except AttributeError:
if not hasattr(self._local, "iter_state"):
self._local.iter_state = IterState()
return self._local.iter_state
return self._local.iter_state # type: ignore[no-any-return]

@@ -495,9 +489,3 @@ def wraps(self, f: WrappedFn) -> WrappedFn:

if sys.version_info >= (3, 9):
FutureGenericT = futures.Future[t.Any]
else:
FutureGenericT = futures.Future
class Future(FutureGenericT):
class Future(futures.Future[t.Any]):
"""Encapsulates a (future or past) attempted call to a target function."""

@@ -610,3 +598,23 @@

def retry(
sleep: t.Callable[[t.Union[int, float]], t.Union[None, t.Awaitable[None]]] = sleep,
*,
sleep: t.Callable[[t.Union[int, float]], t.Awaitable[None]],
stop: "StopBaseT" = ...,
wait: "WaitBaseT" = ...,
retry: "t.Union[RetryBaseT, tasyncio.retry.RetryBaseT]" = ...,
before: t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] = ...,
after: t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]] = ...,
before_sleep: t.Optional[
t.Callable[["RetryCallState"], t.Union[None, t.Awaitable[None]]]
] = ...,
reraise: bool = ...,
retry_error_cls: t.Type["RetryError"] = ...,
retry_error_callback: t.Optional[
t.Callable[["RetryCallState"], t.Union[t.Any, t.Awaitable[t.Any]]]
] = ...,
) -> t.Callable[[t.Callable[P, R | t.Awaitable[R]]], t.Callable[P, t.Awaitable[R]]]: ...
@t.overload
def retry(
sleep: t.Callable[[t.Union[int, float]], None] = sleep,
stop: "StopBaseT" = stop_never,

@@ -650,3 +658,6 @@ wait: "WaitBaseT" = wait_none(),

r: "BaseRetrying"
if _utils.is_coroutine_callable(f):
sleep = dkw.get("sleep")
if _utils.is_coroutine_callable(f) or (
sleep is not None and _utils.is_coroutine_callable(sleep)
):
r = AsyncRetrying(*dargs, **dkw)

@@ -699,2 +710,3 @@ elif (

"wait_combine",
"wait_exception",
"wait_exponential",

@@ -701,0 +713,0 @@ "wait_fixed",

@@ -28,2 +28,14 @@ # Copyright 2016 Julien Danjou

class LoggerProtocol(typing.Protocol):
"""
Protocol used by utils expecting a logger (eg: before_log).
Compatible with logging, structlog, loguru, etc...
"""
def log(
self, level: int, msg: str, /, *args: typing.Any, **kwargs: typing.Any
) -> typing.Any: ...
def find_ordinal(pos_num: int) -> str:

@@ -30,0 +42,0 @@ # See: https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers

@@ -22,4 +22,2 @@ # Copyright 2016 Julien Danjou

if typing.TYPE_CHECKING:
import logging
from tenacity import RetryCallState

@@ -33,5 +31,5 @@

def after_log(
logger: "logging.Logger",
logger: _utils.LoggerProtocol,
log_level: int,
sec_format: str = "%0.3f",
sec_format: str = "%.3g",
) -> typing.Callable[["RetryCallState"], None]:

@@ -38,0 +36,0 @@ """After call strategy that logs to some logger the finished attempt."""

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

retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs)
is_async = _utils.is_coroutine_callable(fn)
while True:

@@ -115,3 +116,6 @@ do = await self.iter(retry_state=retry_state)

try:
result = await fn(*args, **kwargs)
if is_async:
result = await fn(*args, **kwargs)
else:
result = fn(*args, **kwargs)
except BaseException: # noqa: B902

@@ -118,0 +122,0 @@ retry_state.set_exception(sys.exc_info()) # type: ignore[arg-type]

@@ -22,4 +22,2 @@ # Copyright 2016 Julien Danjou

if typing.TYPE_CHECKING:
import logging
from tenacity import RetryCallState

@@ -33,5 +31,6 @@

def before_sleep_log(
logger: "logging.Logger",
logger: _utils.LoggerProtocol,
log_level: int,
exc_info: bool = False,
sec_format: str = "%.3g",
) -> typing.Callable[["RetryCallState"], None]:

@@ -70,3 +69,3 @@ """Before sleep strategy that logs to some logger the attempt."""

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

@@ -73,0 +72,0 @@ )

@@ -22,4 +22,2 @@ # Copyright 2016 Julien Danjou

if typing.TYPE_CHECKING:
import logging
from tenacity import RetryCallState

@@ -33,3 +31,3 @@

def before_log(
logger: "logging.Logger", log_level: int
logger: _utils.LoggerProtocol, log_level: int
) -> typing.Callable[["RetryCallState"], None]:

@@ -36,0 +34,0 @@ """Before call strategy that logs to some logger the attempt."""

@@ -40,3 +40,3 @@ # Copyright 2017 Elisey Zanko

@gen.coroutine # type: ignore[misc]
@gen.coroutine # type: ignore[untyped-decorator]
def __call__(

@@ -43,0 +43,0 @@ self,

@@ -101,6 +101,6 @@ # Copyright 2016–2021 Julien Danjou

[wait_fixed(2) for j in range(5)] +
[wait_fixed(5) for k in range(4)))
[wait_fixed(5) for k in range(4)]))
def wait_chained():
print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s
thereafter.")
print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s "
"thereafter.")
"""

@@ -117,2 +117,41 @@

class wait_exception(wait_base):
"""Wait strategy that waits the amount of time returned by the predicate.
The predicate is passed the exception object. Based on the exception, the
user can decide how much time to wait before retrying.
For example::
def http_error(exception: BaseException) -> float:
if (
isinstance(exception, requests.HTTPError)
and exception.response.status_code == requests.codes.too_many_requests
):
return float(exception.response.headers.get("Retry-After", "1"))
return 60.0
@retry(
stop=stop_after_attempt(3),
wait=wait_exception(http_error),
)
def http_get_request(url: str) -> None:
response = requests.get(url)
response.raise_for_status()
"""
def __init__(self, predicate: typing.Callable[[BaseException], float]) -> None:
self.predicate = predicate
def __call__(self, retry_state: "RetryCallState") -> float:
if retry_state.outcome is None:
raise RuntimeError("__call__() called before outcome was set")
exception = retry_state.outcome.exception()
if exception is None:
raise RuntimeError("outcome failed but the exception is None")
return self.predicate(exception)
class wait_incrementing(wait_base):

@@ -119,0 +158,0 @@ """Wait an incremental amount of time after each attempt.

@@ -30,3 +30,3 @@ # mypy: disable-error-code="no-untyped-def,no-untyped-call"

sec_format = "%0.3f"
sec_format = "%.3g"
delay_since_first_attempt = 0.1

@@ -33,0 +33,0 @@

@@ -37,3 +37,7 @@ # mypy: disable-error-code="no-untyped-def,no-untyped-call"

from .test_tenacity import NoIOErrorAfterCount, current_time_ms
from .test_tenacity import (
NoIOErrorAfterCount,
NoneReturnUntilAfterCount,
current_time_ms,
)

@@ -44,4 +48,3 @@

def wrapper(*a, **kw):
loop = asyncio.get_event_loop()
return loop.run_until_complete(callable_(*a, **kw))
return asyncio.run(callable_(*a, **kw))

@@ -469,3 +472,24 @@ return wrapper

class TestSyncFunctionWithAsyncSleep(unittest.TestCase):
@asynctest
async def test_sync_function_with_async_sleep(self):
"""A sync function with an async sleep callable uses AsyncRetrying."""
mock_sleep = mock.AsyncMock()
thing = NoneReturnUntilAfterCount(2)
@retry(
sleep=mock_sleep,
wait=wait_fixed(1),
retry=retry_if_result(lambda x: x is None),
)
def sync_function():
return thing.go()
result = await sync_function()
assert result is True
assert mock_sleep.await_count == 2
if __name__ == "__main__":
unittest.main()

@@ -15,4 +15,3 @@ import asyncio

def wrapper(*a: typing.Any, **kw: typing.Any) -> typing.Any:
loop = asyncio.get_event_loop()
return loop.run_until_complete(callable_(*a, **kw))
return asyncio.run(callable_(*a, **kw))

@@ -19,0 +18,0 @@ return wrapper

@@ -20,3 +20,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"

import re
import sys
import time

@@ -32,2 +31,3 @@ import typing

import pytest
from typeguard import check_type

@@ -374,2 +374,20 @@ import tenacity

def test_wait_exception(self):
def predicate(exc):
if isinstance(exc, ValueError):
return 3.5
return 10.0
r = Retrying(wait=tenacity.wait_exception(predicate))
fut1 = tenacity.Future.construct(1, ValueError(), True)
self.assertEqual(r.wait(make_retry_state(1, 0, last_result=fut1)), 3.5)
fut2 = tenacity.Future.construct(1, KeyError(), True)
self.assertEqual(r.wait(make_retry_state(1, 0, last_result=fut2)), 10.0)
fut3 = tenacity.Future.construct(1, None, False)
with self.assertRaises(RuntimeError):
r.wait(make_retry_state(1, 0, last_result=fut3))
def test_wait_double_sum(self):

@@ -1717,14 +1735,5 @@ r = Retrying(wait=tenacity.wait_random(0, 3) + tenacity.wait_fixed(5))

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

@@ -1731,0 +1740,0 @@ # type: (int) -> str

[tox]
# we only test trio on latest python version
envlist = py3{9,10,11,12,13,13-trio}, pep8, pypy3
envlist = py3{10,11,12,13,14,14-trio}, pep8, pypy3
skip_missing_interpreters = True

@@ -14,5 +14,5 @@

commands =
py3{8,9,10,11,12,13},pypy3: pytest {posargs}
py3{8,9,10,11,12,13},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build
py3{8,9,10,11,12,13},pypy3: sphinx-build -a -E -W -b html doc/source doc/build
py3{10,11,12,13,14},pypy3: pytest {posargs}
py3{10,11,12,13,14},pypy3: sphinx-build -a -E -W -b doctest doc/source doc/build
py3{10,11,12,13,14},pypy3: sphinx-build -a -E -W -b html doc/source doc/build

@@ -19,0 +19,0 @@ [testenv:pep8]