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.4.1
to
8.4.2
+4
releasenotes/notes...context-overwrite-94190ba06a481631.yaml
---
fixes:
- |
Avoid overwriting local contexts when applying the retry decorator.
import asyncio
import typing
import unittest
from functools import wraps
from tenacity import RetryCallState, retry
def asynctest(
callable_: typing.Callable[..., typing.Any],
) -> typing.Callable[..., typing.Any]:
@wraps(callable_)
def wrapper(*a: typing.Any, **kw: typing.Any) -> typing.Any:
loop = asyncio.get_event_loop()
return loop.run_until_complete(callable_(*a, **kw))
return wrapper
MAX_RETRY_FIX_ATTEMPTS = 2
class TestIssue478(unittest.TestCase):
def test_issue(self) -> None:
results = []
def do_retry(retry_state: RetryCallState) -> bool:
outcome = retry_state.outcome
assert outcome
ex = outcome.exception()
_subject_: str = retry_state.args[0]
if _subject_ == "Fix": # no retry on fix failure
return False
if retry_state.attempt_number >= MAX_RETRY_FIX_ATTEMPTS:
return False
if ex:
do_fix_work()
return True
return False
@retry(reraise=True, retry=do_retry)
def _do_work(subject: str) -> None:
if subject == "Error":
results.append(f"{subject} is not working")
raise Exception(f"{subject} is not working")
results.append(f"{subject} is working")
def do_any_work(subject: str) -> None:
_do_work(subject)
def do_fix_work() -> None:
_do_work("Fix")
try:
do_any_work("Error")
except Exception as exc:
assert str(exc) == "Error is not working"
else:
assert False, "No exception caught"
assert results == [
"Error is not working",
"Fix is working",
"Error is not working",
]
@asynctest
async def test_async(self) -> None:
results = []
async def do_retry(retry_state: RetryCallState) -> bool:
outcome = retry_state.outcome
assert outcome
ex = outcome.exception()
_subject_: str = retry_state.args[0]
if _subject_ == "Fix": # no retry on fix failure
return False
if retry_state.attempt_number >= MAX_RETRY_FIX_ATTEMPTS:
return False
if ex:
await do_fix_work()
return True
return False
@retry(reraise=True, retry=do_retry)
async def _do_work(subject: str) -> None:
if subject == "Error":
results.append(f"{subject} is not working")
raise Exception(f"{subject} is not working")
results.append(f"{subject} is working")
async def do_any_work(subject: str) -> None:
await _do_work(subject)
async def do_fix_work() -> None:
await _do_work("Fix")
try:
await do_any_work("Error")
except Exception as exc:
assert str(exc) == "Error is not working"
else:
assert False, "No exception caught"
assert results == [
"Error is not working",
"Fix is working",
"Error is not working",
]
+1
-1
Metadata-Version: 2.1
Name: tenacity
Version: 8.4.1
Version: 8.4.2
Summary: Retry code until it succeeds

@@ -5,0 +5,0 @@ Home-page: https://github.com/jd/tenacity

Metadata-Version: 2.1
Name: tenacity
Version: 8.4.1
Version: 8.4.2
Summary: Retry code until it succeeds

@@ -5,0 +5,0 @@ Home-page: https://github.com/jd/tenacity

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

releasenotes/notes/fix-async-loop-with-result-f68e913ccb425aca.yaml
releasenotes/notes/fix-local-context-overwrite-94190ba06a481631.yaml
releasenotes/notes/fix-setuptools-config-3af71aa3592b6948.yaml

@@ -77,4 +78,5 @@ releasenotes/notes/fix-wait-typing-b26eecdb6cc0a1de.yaml

tests/test_asyncio.py
tests/test_issue_478.py
tests/test_tenacity.py
tests/test_tornado.py
tests/test_utils.py

@@ -332,3 +332,7 @@ # Copyright 2016-2018 Julien Danjou

def wrapped_f(*args: t.Any, **kw: t.Any) -> t.Any:
return self(f, *args, **kw)
# Always create a copy to prevent overwriting the local contexts when
# calling the same wrapped functions multiple times in the same stack
copy = self.copy()
wrapped_f.statistics = copy.statistics # type: ignore[attr-defined]
return copy(f, *args, **kw)

@@ -338,4 +342,6 @@ def retry_with(*args: t.Any, **kwargs: t.Any) -> WrappedFn:

wrapped_f.retry = self # type: ignore[attr-defined]
# Preserve attributes
wrapped_f.retry = wrapped_f # type: ignore[attr-defined]
wrapped_f.retry_with = retry_with # type: ignore[attr-defined]
wrapped_f.statistics = {} # type: ignore[attr-defined]

@@ -342,0 +348,0 @@ return wrapped_f # type: ignore[return-value]

@@ -178,3 +178,3 @@ # Copyright 2016 Étienne Bersac

def wraps(self, fn: WrappedFn) -> WrappedFn:
fn = super().wraps(fn)
wrapped = super().wraps(fn)
# Ensure wrapper is recognized as a coroutine function.

@@ -186,7 +186,12 @@

async def async_wrapped(*args: t.Any, **kwargs: t.Any) -> t.Any:
return await fn(*args, **kwargs)
# Always create a copy to prevent overwriting the local contexts when
# calling the same wrapped functions multiple times in the same stack
copy = self.copy()
async_wrapped.statistics = copy.statistics # type: ignore[attr-defined]
return await copy(fn, *args, **kwargs)
# Preserve attributes
async_wrapped.retry = fn.retry # type: ignore[attr-defined]
async_wrapped.retry_with = fn.retry_with # type: ignore[attr-defined]
async_wrapped.retry = async_wrapped # type: ignore[attr-defined]
async_wrapped.retry_with = wrapped.retry_with # type: ignore[attr-defined]
async_wrapped.statistics = {} # type: ignore[attr-defined]

@@ -193,0 +198,0 @@ return async_wrapped # type: ignore[return-value]