1111from dataclasses import dataclass , replace
1212from functools import wraps
1313from inspect import iscoroutinefunction
14- from typing import (
15- AsyncIterator ,
16- Iterable ,
17- Iterator ,
18- TypeVar ,
19- )
14+ from types import TracebackType
15+ from typing import AsyncIterator , Iterable , Iterator , TypeVar
2016
2117import tenacity as _t
2218
@@ -68,10 +64,59 @@ def retry_context(
6864 )
6965
7066
67+ @dataclass
68+ class Attempt :
69+ """
70+ A context manager that can be used to retry code blocks.
71+
72+ Instances are yielded by the :func:`stamina.retry_context` iterator.
73+
74+ .. versionadded:: 23.2.0
75+ """
76+
77+ _t_attempt : _t .AttemptManager
78+
79+ @property
80+ def num (self ) -> int :
81+ """
82+ The number of the current attempt.
83+ """
84+ return self ._t_attempt .retry_state .attempt_number # type: ignore[no-any-return]
85+
86+ def __enter__ (self ) -> None :
87+ return self ._t_attempt .__enter__ () # type: ignore[no-any-return]
88+
89+ def __exit__ (
90+ self ,
91+ exc_type : type [BaseException ] | None ,
92+ exc_value : BaseException | None ,
93+ traceback : TracebackType | None ,
94+ ) -> bool | None :
95+ return self ._t_attempt .__exit__ ( # type: ignore[no-any-return]
96+ exc_type , exc_value , traceback
97+ )
98+
99+
100+ _STOP_NO_RETRY = _t .stop_after_attempt (1 )
101+
102+
103+ class _LazyNoAsyncRetry :
104+ """
105+ Allows us a free null object pattern using non-retries and avoid None.
106+ """
107+
108+ def __aiter__ (self ) -> _t .AsyncRetrying :
109+ return _t .AsyncRetrying (reraise = True , stop = _STOP_NO_RETRY ).__aiter__ ()
110+
111+
112+ _LAZY_NO_ASYNC_RETRY = _LazyNoAsyncRetry ()
113+
114+
71115@dataclass
72116class _RetryContextIterator :
73- __slots__ = ("_tenacity_kw" , "_name" , "_args" , "_kw" )
74- _tenacity_kw : dict [str , object ]
117+ __slots__ = ("_t_kw" , "_t_a_retrying" , "_name" , "_args" , "_kw" )
118+ _t_kw : dict [str , object ]
119+ _t_a_retrying : _t .AsyncRetrying
75120 _name : str
76121 _args : tuple [object , ...]
77122 _kw : dict [str , object ]
@@ -94,7 +139,7 @@ def from_params(
94139 _name = name ,
95140 _args = args ,
96141 _kw = kw ,
97- _tenacity_kw = {
142+ _t_kw = {
98143 "retry" : _t .retry_if_exception_type (on ),
99144 "wait" : _t .wait_exponential_jitter (
100145 initial = wait_initial .total_seconds ()
@@ -116,45 +161,51 @@ def from_params(
116161 ),
117162 "reraise" : True ,
118163 },
164+ _t_a_retrying = _LAZY_NO_ASYNC_RETRY ,
119165 )
120166
121- _STOP_NO_RETRY = _t .stop_after_attempt (1 )
167+ def with_name (
168+ self , name : str , args : tuple [object , ...], kw : dict [str , object ]
169+ ) -> _RetryContextIterator :
170+ """
171+ Recreate ourselves with a new name and arguments.
172+ """
173+ return replace (self , _name = name , _args = args , _kw = kw )
122174
123- def __iter__ (self ) -> Iterator [_t . AttemptManager ]:
175+ def __iter__ (self ) -> Iterator [Attempt ]:
124176 if not _CONFIG .is_active :
125- yield from _t .Retrying (reraise = True , stop = self ._STOP_NO_RETRY )
177+ for r in _t .Retrying (
178+ reraise = True , stop = _STOP_NO_RETRY
179+ ): # pragma: no cover -- it's always once + GeneratorExit
180+ yield Attempt (r )
126181
127- yield from _t .Retrying (
182+ for r in _t .Retrying (
128183 before_sleep = _make_before_sleep (
129184 self ._name , _CONFIG .on_retry , self ._args , self ._kw
130185 )
131186 if _CONFIG .on_retry
132187 else None ,
133- ** self ._tenacity_kw ,
134- )
188+ ** self ._t_kw ,
189+ ):
190+ yield Attempt (r )
191+
192+ def __aiter__ (self ) -> AsyncIterator [Attempt ]:
193+ if _CONFIG .is_active :
194+ self ._t_a_retrying = _t .AsyncRetrying (
195+ before_sleep = _make_before_sleep (
196+ self ._name , _CONFIG .on_retry , self ._args , self ._kw
197+ )
198+ if _CONFIG .on_retry
199+ else None ,
200+ ** self ._t_kw ,
201+ )
135202
136- def __aiter__ (self ) -> AsyncIterator [_t .AttemptManager ]:
137- if not _CONFIG .is_active :
138- return _t .AsyncRetrying ( # type: ignore[no-any-return]
139- reraise = True , stop = self ._STOP_NO_RETRY
140- ).__aiter__ ()
203+ self ._t_a_retrying = self ._t_a_retrying .__aiter__ ()
141204
142- return _t .AsyncRetrying ( # type: ignore[no-any-return]
143- before_sleep = _make_before_sleep (
144- self ._name , _CONFIG .on_retry , self ._args , self ._kw
145- )
146- if _CONFIG .on_retry
147- else None ,
148- ** self ._tenacity_kw ,
149- ).__aiter__ ()
205+ return self
150206
151- def with_name (
152- self , name : str , args : tuple [object , ...], kw : dict [str , object ]
153- ) -> _RetryContextIterator :
154- """
155- Recreate ourselves with a new name and arguments.
156- """
157- return replace (self , _name = name , _args = args , _kw = kw )
207+ async def __anext__ (self ) -> Attempt :
208+ return Attempt (await self ._t_a_retrying .__anext__ ())
158209
159210
160211def _make_before_sleep (
0 commit comments