11import asyncio
2- import logging
32import time
43from typing import Optional
54
@@ -19,160 +18,138 @@ def __init__(self, headers: Optional[dict[str, str]] = None, status_code: Option
1918 super ().__init__ ("rate limit" )
2019
2120
22- @pytest .mark .asyncio
23- async def test_async_success_first_try ():
24- calls = {"n" : 0 }
21+ @pytest .fixture
22+ def counter ():
23+ class C :
24+ def __init__ (self ) -> None :
25+ self .n = 0
2526
26- @retry_with_backoff (settings = RetryDecoratorSettings (max_retries = 2 ))
27- async def fn ():
28- calls ["n" ] += 1
29- return 42
27+ def inc (self ) -> int :
28+ self .n += 1
29+ return self .n
3030
31- assert await fn () == 42
32- assert calls ["n" ] == 1
31+ return C ()
3332
3433
35- @pytest .mark .asyncio
36- async def test_async_retries_then_success (monkeypatch ):
37- calls = {"n" : 0 }
38- slept = []
34+ @pytest .fixture
35+ def async_sleeps (monkeypatch ):
36+ sleeps : list [float ] = []
3937
4038 async def fake_sleep (x ):
41- slept .append (x )
39+ sleeps .append (x )
4240
4341 monkeypatch .setattr (asyncio , "sleep" , fake_sleep )
42+ return sleeps
43+
44+
45+ @pytest .fixture
46+ def sync_sleeps (monkeypatch ):
47+ sleeps : list [float ] = []
48+
49+ def fake_sleep (x ):
50+ sleeps .append (x )
51+
52+ monkeypatch .setattr (time , "sleep" , fake_sleep )
53+ return sleeps
54+
4455
56+ @pytest .mark .asyncio
57+ async def test_async_success_first_try (counter ):
58+ @retry_with_backoff (settings = RetryDecoratorSettings (max_retries = 2 ))
59+ async def fn ():
60+ counter .inc ()
61+ return 42
62+
63+ assert await fn () == 42
64+ assert counter .n == 1
65+
66+
67+ @pytest .mark .asyncio
68+ async def test_async_retries_then_success (counter , async_sleeps ):
4569 @retry_with_backoff (settings = RetryDecoratorSettings (max_retries = 3 , retry_base_delay = 0.01 ))
4670 async def fn ():
47- calls ["n" ] += 1
48- if calls ["n" ] < 3 :
71+ if counter .inc () < 3 :
4972 raise DummyError ("boom" )
5073 return "ok"
5174
5275 assert await fn () == "ok"
53- assert calls ["n" ] == 3
54- # Expect at least two sleeps due to two failures
55- assert len (slept ) >= 2
76+ assert counter .n == 3
77+ assert len (async_sleeps ) >= 2 # two failures -> two sleeps
5678
5779
58- def test_sync_success_first_try ():
59- calls = {"n" : 0 }
60-
80+ def test_sync_success_first_try (counter ):
6181 @retry_with_backoff (settings = RetryDecoratorSettings (max_retries = 2 ))
6282 def fn ():
63- calls [ "n" ] += 1
83+ counter . inc ()
6484 return 7
6585
6686 assert fn () == 7
67- assert calls ["n" ] == 1
68-
69-
70- def test_sync_retries_then_fail (monkeypatch ):
71- calls = {"n" : 0 }
72- slept = []
87+ assert counter .n == 1
7388
74- def fake_sleep (x ):
75- slept .append (x )
76-
77- monkeypatch .setattr (time , "sleep" , fake_sleep )
7889
90+ def test_sync_retries_then_fail (counter , sync_sleeps ):
7991 @retry_with_backoff (settings = RetryDecoratorSettings (max_retries = 2 , retry_base_delay = 0.01 ))
8092 def fn ():
81- calls [ "n" ] += 1
93+ counter . inc ()
8294 raise DummyError ("always" )
8395
8496 with pytest .raises (DummyError ):
8597 fn ()
86- # 1 initial + 2 retries = 3 calls total
87- assert calls ["n" ] == 3
88- # Two sleeps (after two failures)
89- assert len (slept ) == 2
98+ assert counter .n == 3 # 1 initial + 2 retries
99+ assert len (sync_sleeps ) == 2
90100
91101
92102@pytest .mark .asyncio
93- async def test_async_rate_limit_uses_header_wait (monkeypatch ):
94- calls = {"n" : 0 }
95- slept = []
96-
97- async def fake_sleep (x ):
98- slept .append (x )
99-
100- monkeypatch .setattr (asyncio , "sleep" , fake_sleep )
101-
102- headers = {
103- "x-ratelimit-reset-requests" : "1.5s" ,
104- "x-ratelimit-remaining-requests" : "0" ,
105- }
103+ async def test_async_rate_limit_uses_header_wait (counter , async_sleeps ):
104+ headers = {"x-ratelimit-reset-requests" : "1.5s" , "x-ratelimit-remaining-requests" : "0" }
106105
107106 @retry_with_backoff (
108107 settings = RetryDecoratorSettings (max_retries = 1 , retry_base_delay = 0.01 ),
109108 rate_limit_exceptions = (RateLimitError ,),
110109 )
111110 async def fn ():
112- calls ["n" ] += 1
113- if calls ["n" ] == 1 :
111+ if counter .inc () == 1 :
114112 raise RateLimitError (headers = headers , status_code = 429 )
115113 return "ok"
116114
117115 out = await fn ()
118116 assert out == "ok"
119- assert calls ["n" ] == 2
120- # Should sleep roughly ~1.5s (+jitter). Verify >= 1.5
121- assert any (x >= 1.5 for x in slept )
117+ assert counter .n == 2
118+ assert any (x >= 1.5 for x in async_sleeps )
122119
123120
124121@pytest .mark .asyncio
125- async def test_is_rate_limited_callback (monkeypatch ):
126- calls = {"n" : 0 }
127- slept = []
128-
129- async def fake_sleep (x ):
130- slept .append (x )
131-
132- monkeypatch .setattr (asyncio , "sleep" , fake_sleep )
133-
134- def mark_rate_limited (exc : BaseException ) -> bool :
122+ async def test_is_rate_limited_callback (counter , async_sleeps ):
123+ def mark_rate_limited (exc : BaseException ) -> bool : # noqa: ANN001 - explicit for clarity
135124 return isinstance (exc , DummyError )
136125
137126 @retry_with_backoff (
138127 settings = RetryDecoratorSettings (max_retries = 1 , retry_base_delay = 0.01 ),
139128 is_rate_limited = mark_rate_limited ,
140129 )
141130 async def fn ():
142- calls ["n" ] += 1
143- if calls ["n" ] == 1 :
131+ if counter .inc () == 1 :
144132 raise DummyError ("treated as rate limited" )
145133 return "ok"
146134
147135 out = await fn ()
148136 assert out == "ok"
149- assert calls [ "n" ] == 2
150- assert len (slept ) == 1
137+ assert counter . n == 2
138+ assert len (async_sleeps ) == 1
151139
152140
153- def test_sync_rate_limit_headers (monkeypatch ):
154- calls = {"n" : 0 }
155- slept = []
156-
157- def fake_sleep (x ):
158- slept .append (x )
159-
160- monkeypatch .setattr (time , "sleep" , fake_sleep )
161-
162- headers = {
163- "x-ratelimit-reset-requests" : "2s" ,
164- }
141+ def test_sync_rate_limit_headers (counter , sync_sleeps ):
142+ headers = {"x-ratelimit-reset-requests" : "2s" }
165143
166144 @retry_with_backoff (
167145 settings = RetryDecoratorSettings (max_retries = 1 , retry_base_delay = 0.01 ),
168146 rate_limit_exceptions = (RateLimitError ,),
169147 )
170148 def fn ():
171- calls ["n" ] += 1
172- if calls ["n" ] == 1 :
149+ if counter .inc () == 1 :
173150 raise RateLimitError (headers = headers , status_code = 429 )
174151 return "ok"
175152
176153 assert fn () == "ok"
177- assert calls [ "n" ] == 2
178- assert any (x >= 2.0 for x in slept )
154+ assert counter . n == 2
155+ assert any (x >= 2.0 for x in sync_sleeps )
0 commit comments