Skip to content

Commit bdc5eff

Browse files
Add urllib3 Retry mechanism, documentation and tests (#536)
Add support for urllib3 Retry adapter. Co-authored-by: Mark Story <[email protected]>
1 parent 4f513fc commit bdc5eff

File tree

4 files changed

+171
-1
lines changed

4 files changed

+171
-1
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
------
33

44
* Add `threading.Lock()` to allow `responses` working with `threading` module.
5+
* Add `urllib3` `Retry` mechanism. See #135
56
* Removed internal `_cookies_from_headers` function
67
* Now `add`, `upsert`, `replace` methods return registered response.
78
`remove` method returns list of removed responses.

README.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,44 @@ in the execution chain and contain the history of redirects.
997997
assert rsp2.url in exc_info.value.response.history[1].url
998998
999999
1000+
Validate ``Retry`` mechanism
1001+
----------------------------
1002+
1003+
If you are using the ``Retry`` features of ``urllib3`` and want to cover scenarios that test your retry limits, you can test those scenarios with ``responses`` as well. The best approach will be to use an `Ordered Registry`_
1004+
1005+
.. code-block:: python
1006+
1007+
import requests
1008+
1009+
import responses
1010+
from responses import registries
1011+
1012+
@responses.activate(registry=registries.OrderedRegistry)
1013+
def test_max_retries():
1014+
url = 'https://example.com'
1015+
rsp1 = responses.get(url, body='Error', status=500)
1016+
rsp2 = responses.get(url, body='Error', status=500)
1017+
rsp3 = responses.get(url, body='Error', status=500)
1018+
rsp4 = responses.get(url, body='OK', status=200)
1019+
1020+
session = requests.Session()
1021+
1022+
adapter = requests.adapters.HTTPAdapter(max_retries=Retry(
1023+
total=4,
1024+
backoff_factor=0.1,
1025+
status_forcelist=[500],
1026+
method_whitelist=['GET', 'POST', 'PATCH']
1027+
))
1028+
session.mount('https://', adapter)
1029+
1030+
resp = session.get(url)
1031+
1032+
assert resp.status_code == 200
1033+
assert rsp1.call_count == 1
1034+
assert rsp2.call_count == 1
1035+
assert rsp3.call_count == 1
1036+
assert rsp4.call_count == 1
1037+
10001038
10011039
Using a callback to modify the response
10021040
---------------------------------------

responses/__init__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from warnings import warn
2020

2121
from requests.adapters import HTTPAdapter
22+
from requests.adapters import MaxRetryError
2223
from requests.exceptions import ConnectionError
2324

2425
from responses.matchers import json_params_matcher as _json_params_matcher
@@ -846,7 +847,7 @@ def _parse_request_params(self, url):
846847
params[key] = values
847848
return params
848849

849-
def _on_request(self, adapter, request, **kwargs):
850+
def _on_request(self, adapter, request, *, retries=None, **kwargs):
850851
# add attributes params and req_kwargs to 'request' object for further match comparison
851852
# original request object does not have these attributes
852853
request.params = self._parse_request_params(request.path_url)
@@ -904,6 +905,22 @@ def _on_request(self, adapter, request, **kwargs):
904905
response = resp_callback(response) if resp_callback else response
905906
match.call_count += 1
906907
self._calls.add(request, response)
908+
909+
retries = retries or adapter.max_retries
910+
# first validate that current request is eligible to be retried.
911+
# See ``requests.packages.urllib3.util.retry.Retry`` documentation.
912+
if retries.is_retry(
913+
method=response.request.method, status_code=response.status_code
914+
):
915+
try:
916+
retries = retries.increment(
917+
method=response.request.method, url=response.url, response=response
918+
)
919+
return self._on_request(adapter, request, retries=retries, **kwargs)
920+
except MaxRetryError:
921+
if retries.raise_on_status:
922+
raise
923+
return response
907924
return response
908925

909926
def start(self):

responses/tests/test_responses.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@
1313

1414
import pytest
1515
import requests
16+
from requests.adapters import MaxRetryError
1617
from requests.exceptions import ChunkedEncodingError
1718
from requests.exceptions import ConnectionError
1819
from requests.exceptions import HTTPError
20+
from urllib3.util.retry import Retry
1921

2022
import responses
2123
from responses import BaseResponse
2224
from responses import CallbackResponse
2325
from responses import PassthroughResponse
2426
from responses import Response
2527
from responses import matchers
28+
from responses import registries
2629

2730

2831
def assert_reset():
@@ -2342,3 +2345,114 @@ def run():
23422345

23432346
run()
23442347
assert_reset()
2348+
2349+
2350+
class TestMaxRetry:
2351+
def test_max_retries(self):
2352+
"""This example is present in README.rst"""
2353+
2354+
@responses.activate(registry=registries.OrderedRegistry)
2355+
def run():
2356+
url = "https://example.com"
2357+
rsp1 = responses.get(url, body="Error", status=500)
2358+
rsp2 = responses.get(url, body="Error", status=500)
2359+
rsp3 = responses.get(url, body="Error", status=500)
2360+
rsp4 = responses.get(url, body="OK", status=200)
2361+
2362+
session = requests.Session()
2363+
2364+
adapter = requests.adapters.HTTPAdapter(
2365+
max_retries=Retry(
2366+
total=4,
2367+
backoff_factor=0.1,
2368+
status_forcelist=[500],
2369+
method_whitelist=["GET", "POST", "PATCH"],
2370+
)
2371+
)
2372+
session.mount("https://", adapter)
2373+
2374+
resp = session.get(url)
2375+
2376+
assert resp.status_code == 200
2377+
assert rsp1.call_count == 1
2378+
assert rsp2.call_count == 1
2379+
assert rsp3.call_count == 1
2380+
assert rsp4.call_count == 1
2381+
2382+
run()
2383+
assert_reset()
2384+
2385+
@pytest.mark.parametrize("raise_on_status", (True, False))
2386+
def test_max_retries_exceed(self, raise_on_status):
2387+
@responses.activate(registry=registries.OrderedRegistry)
2388+
def run():
2389+
url = "https://example.com"
2390+
rsp1 = responses.get(url, body="Error", status=500)
2391+
rsp2 = responses.get(url, body="Error", status=500)
2392+
rsp3 = responses.get(url, body="Error", status=500)
2393+
2394+
session = requests.Session()
2395+
2396+
adapter = requests.adapters.HTTPAdapter(
2397+
max_retries=Retry(
2398+
total=2,
2399+
backoff_factor=0.1,
2400+
status_forcelist=[500],
2401+
method_whitelist=["GET", "POST", "PATCH"],
2402+
raise_on_status=raise_on_status,
2403+
)
2404+
)
2405+
session.mount("https://", adapter)
2406+
2407+
if raise_on_status:
2408+
with pytest.raises(MaxRetryError):
2409+
session.get(url)
2410+
else:
2411+
resp = session.get(url)
2412+
assert resp.status_code == 500
2413+
2414+
assert rsp1.call_count == 1
2415+
assert rsp2.call_count == 1
2416+
assert rsp3.call_count == 1
2417+
2418+
run()
2419+
assert_reset()
2420+
2421+
def test_adapter_retry_untouched(self):
2422+
"""Validate that every new request uses brand-new Retry object"""
2423+
2424+
@responses.activate(registry=registries.OrderedRegistry)
2425+
def run():
2426+
url = "https://example.com"
2427+
error_rsp = responses.get(url, body="Error", status=500)
2428+
responses.add(error_rsp)
2429+
responses.add(error_rsp)
2430+
ok_rsp = responses.get(url, body="OK", status=200)
2431+
2432+
responses.add(error_rsp)
2433+
responses.add(error_rsp)
2434+
responses.add(error_rsp)
2435+
responses.add(ok_rsp)
2436+
2437+
session = requests.Session()
2438+
2439+
adapter = requests.adapters.HTTPAdapter(
2440+
max_retries=Retry(
2441+
total=4,
2442+
backoff_factor=0.1,
2443+
status_forcelist=[500],
2444+
method_whitelist=["GET", "POST", "PATCH"],
2445+
)
2446+
)
2447+
session.mount("https://", adapter)
2448+
2449+
resp = session.get(url)
2450+
assert resp.status_code == 200
2451+
2452+
resp = session.get(url)
2453+
assert resp.status_code == 200
2454+
2455+
assert len(responses.calls) == 8
2456+
2457+
run()
2458+
assert_reset()

0 commit comments

Comments
 (0)