Skip to content

Commit 1fcfe24

Browse files
committed
Move urllib3 mocking into separate module
1 parent d7b0a33 commit 1fcfe24

File tree

3 files changed

+94
-77
lines changed

3 files changed

+94
-77
lines changed

wayback/tests/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pytest_plugins = "wayback.tests.urllib3_mock"

wayback/tests/test_client.py

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
from datetime import date, datetime, timezone, timedelta
2-
from io import BytesIO
32
from itertools import islice
43
from pathlib import Path
54
import time
65
import pytest
76
from unittest import mock
8-
from urllib.parse import urlparse, ParseResult, parse_qs
9-
from urllib3 import (HTTPConnectionPool,
10-
HTTPResponse,
7+
from urllib3 import (HTTPResponse,
118
Timeout as Urllib3Timeout)
129
# The Header dict lives in a different place for urllib3 v2:
1310
try:
@@ -229,79 +226,6 @@ def test_search_with_filter_tuple():
229226
assert all(('feature' in v.url for v in versions))
230227

231228

232-
class Urllib3MockManager:
233-
def __init__(self) -> None:
234-
self.responses = []
235-
236-
def get(self, url, responses) -> None:
237-
url_info = urlparse(url)
238-
if url_info.path == '':
239-
url_info = url_info._replace(path='/')
240-
for index, response in enumerate(responses):
241-
repeat = True if index == len(responses) - 1 else False
242-
self.responses.append(('GET', url_info, response, repeat))
243-
244-
def _compare_querystrings(self, actual, candidate):
245-
for k, v in candidate.items():
246-
if k not in actual or actual[k] != v:
247-
return False
248-
return True
249-
250-
def urlopen(self, pool: HTTPConnectionPool, method, url, *args, preload_content: bool = True, **kwargs):
251-
opened_url = urlparse(url)
252-
opened_path = opened_url.path or '/'
253-
opened_query = parse_qs(opened_url.query)
254-
for index, candidate in enumerate(self.responses):
255-
candidate_url: ParseResult = candidate[1]
256-
if (
257-
method == candidate[0]
258-
and (not candidate_url.scheme or candidate_url.scheme == pool.scheme)
259-
and (not candidate_url.hostname or candidate_url.hostname == pool.host)
260-
and (not candidate_url.port or candidate_url.port == pool.port)
261-
and candidate_url.path == opened_path
262-
# This is cheap, ideally we'd parse the querystrings.
263-
# and parse_qs(candidate_url.query) == opened_query
264-
and self._compare_querystrings(opened_query, parse_qs(candidate_url.query))
265-
):
266-
if not candidate[3]:
267-
self.responses.pop(index)
268-
269-
data = candidate[2]
270-
if data.get('exc'):
271-
raise data['exc']()
272-
273-
content = data.get('content')
274-
if content is None:
275-
content = data.get('text', '').encode()
276-
277-
return HTTPResponse(
278-
body=BytesIO(content),
279-
headers=HTTPHeaderDict(data.get('headers', {})),
280-
status=data.get('status_code', 200),
281-
decode_content=False,
282-
preload_content=preload_content,
283-
)
284-
285-
# No matches!
286-
raise RuntimeError(
287-
f"No HTTP mocks matched {method} {pool.scheme}://{pool.host}{url}"
288-
)
289-
290-
291-
@pytest.fixture
292-
def urllib3_mock(monkeypatch):
293-
manager = Urllib3MockManager()
294-
295-
def urlopen_mock(self, method, url, *args, preload_content: bool = True, **kwargs):
296-
return manager.urlopen(self, method, url, *args, preload_content=preload_content, **kwargs)
297-
298-
monkeypatch.setattr(
299-
"urllib3.connectionpool.HTTPConnectionPool.urlopen", urlopen_mock
300-
)
301-
302-
return manager
303-
304-
305229
def test_search_removes_malformed_entries(urllib3_mock):
306230
"""
307231
The CDX index contains many lines for things that can't actually be

wayback/tests/urllib3_mock.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""
2+
This module provides really simplistic mocking support for urllib3. It
3+
mirrors the parts of `requests-mock` that we currently use, and no more.
4+
5+
There is an existing urllib3_mock project, but it has not been maintained for
6+
many years and no longer works correctly with current versions of urllib3:
7+
https://pypi.org/project/urllib3-mock/
8+
"""
9+
10+
from io import BytesIO
11+
from urllib.parse import urlparse, ParseResult, parse_qs
12+
import pytest
13+
from urllib3 import HTTPConnectionPool, HTTPResponse
14+
# The Header dict lives in a different place for urllib3 v2:
15+
try:
16+
from urllib3 import HTTPHeaderDict
17+
# vs. urllib3 v1:
18+
except ImportError:
19+
from urllib3.response import HTTPHeaderDict
20+
21+
22+
class Urllib3MockManager:
23+
def __init__(self) -> None:
24+
self.responses = []
25+
26+
def get(self, url, responses) -> None:
27+
url_info = urlparse(url)
28+
if url_info.path == '':
29+
url_info = url_info._replace(path='/')
30+
for index, response in enumerate(responses):
31+
repeat = True if index == len(responses) - 1 else False
32+
self.responses.append(('GET', url_info, response, repeat))
33+
34+
def _compare_querystrings(self, actual, candidate):
35+
for k, v in candidate.items():
36+
if k not in actual or actual[k] != v:
37+
return False
38+
return True
39+
40+
def urlopen(self, pool: HTTPConnectionPool, method, url, *args, preload_content: bool = True, **kwargs):
41+
opened_url = urlparse(url)
42+
opened_path = opened_url.path or '/'
43+
opened_query = parse_qs(opened_url.query)
44+
for index, candidate in enumerate(self.responses):
45+
candidate_url: ParseResult = candidate[1]
46+
if (
47+
method == candidate[0]
48+
and (not candidate_url.scheme or candidate_url.scheme == pool.scheme)
49+
and (not candidate_url.hostname or candidate_url.hostname == pool.host)
50+
and (not candidate_url.port or candidate_url.port == pool.port)
51+
and candidate_url.path == opened_path
52+
# This is cheap, ideally we'd parse the querystrings.
53+
# and parse_qs(candidate_url.query) == opened_query
54+
and self._compare_querystrings(opened_query, parse_qs(candidate_url.query))
55+
):
56+
if not candidate[3]:
57+
self.responses.pop(index)
58+
59+
data = candidate[2]
60+
if data.get('exc'):
61+
raise data['exc']()
62+
63+
content = data.get('content')
64+
if content is None:
65+
content = data.get('text', '').encode()
66+
67+
return HTTPResponse(
68+
body=BytesIO(content),
69+
headers=HTTPHeaderDict(data.get('headers', {})),
70+
status=data.get('status_code', 200),
71+
decode_content=False,
72+
preload_content=preload_content,
73+
)
74+
75+
# No matches!
76+
raise RuntimeError(
77+
f"No HTTP mocks matched {method} {pool.scheme}://{pool.host}{url}"
78+
)
79+
80+
81+
@pytest.fixture
82+
def urllib3_mock(monkeypatch):
83+
manager = Urllib3MockManager()
84+
85+
def urlopen_mock(self, method, url, *args, preload_content: bool = True, **kwargs):
86+
return manager.urlopen(self, method, url, *args, preload_content=preload_content, **kwargs)
87+
88+
monkeypatch.setattr(
89+
"urllib3.connectionpool.HTTPConnectionPool.urlopen", urlopen_mock
90+
)
91+
92+
return manager

0 commit comments

Comments
 (0)