77import sys
88
99from collections .abc import Callable
10+ from dataclasses import dataclass , replace
1011from functools import wraps
1112from inspect import iscoroutinefunction
1213from typing import Iterable , TypeVar
3233P = ParamSpec ("P" )
3334
3435
36+ def retry_context (
37+ on : type [Exception ] | tuple [type [Exception ], ...],
38+ attempts : int | None = 10 ,
39+ timeout : float | int | None = 45.0 ,
40+ wait_initial : float | int = 0.1 ,
41+ wait_max : float | int = 5.0 ,
42+ wait_jitter : float | int = 1.0 ,
43+ wait_exp_base : float | int = 2.0 ,
44+ ) -> _RetryContextIterator :
45+ """
46+ Iterator that yields context managers that can be used to retry code
47+ blocks.
48+
49+ Arguments have the same meaning as for :func:`retry`.
50+
51+ .. versionadded:: 23.1.0
52+ """
53+
54+ return _RetryContextIterator .from_params (
55+ on = on ,
56+ attempts = attempts ,
57+ timeout = timeout ,
58+ wait_initial = wait_initial ,
59+ wait_max = wait_max ,
60+ wait_jitter = wait_jitter ,
61+ wait_exp_base = wait_exp_base ,
62+ name = "<context block>" ,
63+ args = (),
64+ kw = {},
65+ )
66+
67+
68+ @dataclass
69+ class _RetryContextIterator :
70+ __slots__ = ("_tenacity_kw" , "_name" , "_args" , "_kw" )
71+ _tenacity_kw : dict [str , object ]
72+ _name : str
73+ _args : tuple [object , ...]
74+ _kw : dict [str , object ]
75+
76+ @classmethod
77+ def from_params (
78+ cls ,
79+ on : type [Exception ] | tuple [type [Exception ], ...],
80+ attempts : int | None ,
81+ timeout : float | int | None ,
82+ wait_initial : float | int ,
83+ wait_max : float | int ,
84+ wait_jitter : float | int ,
85+ wait_exp_base : float | int ,
86+ name : str ,
87+ args : tuple [object , ...],
88+ kw : dict [str , object ],
89+ ) -> _RetryContextIterator :
90+ return cls (
91+ _name = name ,
92+ _args = args ,
93+ _kw = kw ,
94+ _tenacity_kw = {
95+ "retry" : _t .retry_if_exception_type (on ),
96+ "wait" : _t .wait_exponential_jitter (
97+ initial = wait_initial ,
98+ max = wait_max ,
99+ exp_base = wait_exp_base ,
100+ jitter = wait_jitter ,
101+ ),
102+ "stop" : _make_stop (attempts = attempts , timeout = timeout ),
103+ "reraise" : True ,
104+ },
105+ )
106+
107+ _STOP_NO_RETRY = _t .stop_after_attempt (1 )
108+
109+ def __iter__ (self ) -> _t .Retrying :
110+ if not _CONFIG .is_active :
111+ return _t .Retrying (
112+ reraise = True , stop = self ._STOP_NO_RETRY
113+ ).__iter__ ()
114+
115+ return _t .Retrying (
116+ before_sleep = _make_before_sleep (
117+ self ._name , _CONFIG .on_retry , self ._args , self ._kw
118+ )
119+ if _CONFIG .on_retry
120+ else None ,
121+ ** self ._tenacity_kw ,
122+ ).__iter__ ()
123+
124+ def __aiter__ (self ) -> _t .AsyncRetrying :
125+ if not _CONFIG .is_active :
126+ return _t .AsyncRetrying (
127+ reraise = True , stop = self ._STOP_NO_RETRY
128+ ).__aiter__ ()
129+
130+ return _t .AsyncRetrying (
131+ before_sleep = _make_before_sleep (
132+ self ._name , _CONFIG .on_retry , self ._args , self ._kw
133+ )
134+ if _CONFIG .on_retry
135+ else None ,
136+ ** self ._tenacity_kw ,
137+ ).__aiter__ ()
138+
139+ def with_name (
140+ self , name : str , args : tuple [object , ...], kw : dict [str , object ]
141+ ) -> _RetryContextIterator :
142+ """
143+ Recreate ourselves with a new name and arguments.
144+ """
145+ return replace (self , _name = name , _args = args , _kw = kw )
146+
147+
148+ def _make_before_sleep (
149+ name : str , on_retry : Iterable [RetryHook ], args : object , kw : object
150+ ) -> Callable [[_t .RetryCallState ], None ]:
151+ """
152+ Create a `before_sleep` callback function that runs our `RetryHook`s with
153+ the necessary arguments.
154+ """
155+
156+ def before_sleep (rcs : _t .RetryCallState ) -> None :
157+ attempt = rcs .attempt_number
158+ exc = rcs .outcome .exception ()
159+ backoff = rcs .idle_for
160+
161+ for hook in on_retry :
162+ hook (
163+ attempt = attempt ,
164+ backoff = backoff ,
165+ exc = exc ,
166+ name = name ,
167+ args = args ,
168+ kwargs = kw ,
169+ )
170+
171+ return before_sleep
172+
173+
174+ def _make_stop (* , attempts : int | None , timeout : float | None ) -> _t .stop_base :
175+ """
176+ Combine *attempts* and *timeout* into one stop condition.
177+ """
178+ stops = []
179+
180+ if attempts :
181+ stops .append (_t .stop_after_attempt (attempts ))
182+ if timeout :
183+ stops .append (_t .stop_after_delay (timeout ))
184+
185+ if len (stops ) > 1 :
186+ return _t .stop_any (* stops )
187+
188+ if not stops :
189+ return _t .stop_never
190+
191+ return stops [0 ]
192+
193+
35194def retry (
36195 * ,
37196 on : type [Exception ] | tuple [type [Exception ], ...],
@@ -62,14 +221,18 @@ def do_it(code: int) -> httpx.Response:
62221
63222 return resp
64223 """
65- retry_ = _t .retry_if_exception_type (on )
66- wait = _t .wait_exponential_jitter (
67- initial = wait_initial ,
68- max = wait_max ,
69- exp_base = wait_exp_base ,
70- jitter = wait_jitter ,
224+ retry_ctx = _RetryContextIterator .from_params (
225+ on = on ,
226+ attempts = attempts ,
227+ timeout = timeout ,
228+ wait_initial = wait_initial ,
229+ wait_max = wait_max ,
230+ wait_jitter = wait_jitter ,
231+ wait_exp_base = wait_exp_base ,
232+ name = "<unknown>" ,
233+ args = (),
234+ kw = {},
71235 )
72- stop = _make_stop (attempts = attempts , timeout = timeout )
73236
74237 def retry_decorator (wrapped : Callable [P , T ]) -> Callable [P , T ]:
75238 name = guess_name (wrapped )
@@ -78,19 +241,8 @@ def retry_decorator(wrapped: Callable[P, T]) -> Callable[P, T]:
78241
79242 @wraps (wrapped )
80243 def sync_inner (* args : P .args , ** kw : P .kwargs ) -> T : # type: ignore[return]
81- if not _CONFIG .is_active :
82- return wrapped (* args , ** kw )
83-
84- for attempt in _t .Retrying ( # noqa: RET503
85- retry = retry_ ,
86- wait = wait ,
87- stop = stop ,
88- reraise = True ,
89- before_sleep = _make_before_sleep (
90- name , _CONFIG .on_retry , args , kw
91- )
92- if _CONFIG .on_retry
93- else None ,
244+ for attempt in retry_ctx .with_name ( # noqa: RET503
245+ name , args , kw
94246 ):
95247 with attempt :
96248 return wrapped (* args , ** kw )
@@ -99,69 +251,12 @@ def sync_inner(*args: P.args, **kw: P.kwargs) -> T: # type: ignore[return]
99251
100252 @wraps (wrapped )
101253 async def async_inner (* args : P .args , ** kw : P .kwargs ) -> T : # type: ignore[return]
102- if not _CONFIG .is_active :
103- return await wrapped (* args , ** kw ) # type: ignore[no-any-return,misc]
104-
105- async for attempt in _t .AsyncRetrying ( # noqa: RET503
106- retry = retry_ ,
107- wait = wait ,
108- stop = stop ,
109- reraise = True ,
110- before_sleep = _make_before_sleep (
111- name , _CONFIG .on_retry , args , kw
112- )
113- if _CONFIG .on_retry
114- else None ,
254+ async for attempt in retry_ctx .with_name ( # noqa: RET503
255+ name , args , kw
115256 ):
116257 with attempt :
117258 return await wrapped (* args , ** kw ) # type: ignore[misc,no-any-return]
118259
119260 return async_inner # type: ignore[return-value]
120261
121262 return retry_decorator
122-
123-
124- def _make_stop (* , attempts : int | None , timeout : float | None ) -> _t .stop_base :
125- """
126- Combine *attempts* and *timeout* into one stop condition.
127- """
128- stops = []
129-
130- if attempts :
131- stops .append (_t .stop_after_attempt (attempts ))
132- if timeout :
133- stops .append (_t .stop_after_delay (timeout ))
134-
135- if len (stops ) > 1 :
136- return _t .stop_any (* stops )
137-
138- if not stops :
139- return _t .stop_never
140-
141- return stops [0 ]
142-
143-
144- def _make_before_sleep (
145- name : str , on_retry : Iterable [RetryHook ], args : object , kw : object
146- ) -> Callable [[_t .RetryCallState ], None ]:
147- """
148- Create a `before_sleep` callback function that runs our `RetryHook`s with
149- the necessary arguments.
150- """
151-
152- def before_sleep (rcs : _t .RetryCallState ) -> None :
153- attempt = rcs .attempt_number
154- exc = rcs .outcome .exception ()
155- backoff = rcs .idle_for
156-
157- for hook in on_retry :
158- hook (
159- attempt = attempt ,
160- backoff = backoff ,
161- exc = exc ,
162- name = name ,
163- args = args ,
164- kwargs = kw ,
165- )
166-
167- return before_sleep
0 commit comments