Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2afbd5a

Browse files
committedMar 14, 2025··
Replace urllib3 with httpx
1 parent 7377408 commit 2afbd5a

File tree

3 files changed

+90
-82
lines changed

3 files changed

+90
-82
lines changed
 

‎gabbi/case.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
1515
The test case encapsulates the request headers and body and expected
1616
response headers and body. When the test is run an HTTP request is
17-
made using urllib3. Assertions are made against the response.
17+
made using httpx. Assertions are made against the response.
1818
"""
1919

2020
from collections import OrderedDict
@@ -500,7 +500,7 @@ def _run_request(
500500
"""
501501

502502
if 'user-agent' not in (key.lower() for key in headers):
503-
headers['user-agent'] = "gabbi/%s (Python urllib3)" % __version__
503+
headers['user-agent'] = "gabbi/%s (Python httpx)" % __version__
504504

505505
try:
506506
response, content = self.http.request(

‎gabbi/httpclient.py

+87-79
Original file line numberDiff line numberDiff line change
@@ -11,53 +11,56 @@
1111
# License for the specific language governing permissions and limitations
1212
# under the License.
1313

14+
from gabbi import utils
15+
from gabbi.handlers import jsonhandler
16+
import httpx
17+
import logging
1418
import os
1519
import sys
1620

17-
import certifi
18-
import urllib3
19-
20-
from gabbi.handlers import jsonhandler
21-
from gabbi import utils
22-
23-
# Disable SSL warnings otherwise tests which process stderr will get
24-
# extra information.
25-
urllib3.disable_warnings()
21+
logging.getLogger("httpx").setLevel(logging.WARNING)
2622

2723

28-
class Http(urllib3.PoolManager):
29-
"""A subclass of the ``urllib3.PoolManager`` to munge the data.
24+
class Http:
25+
"""A class to munge the HTTP response.
3026
3127
This transforms the response to look more like what httplib2
3228
provided when it was used as the HTTP client.
3329
"""
3430

31+
def __init__(self, **kwargs):
32+
self.extensions = {}
33+
if "server_hostname" in kwargs:
34+
self.extensions["sni_hostname"] = kwargs["server_hostname"]
35+
self.client = httpx.Client(verify=kwargs.get("cert_validate", True))
36+
3537
def request(self, absolute_uri, method, body, headers, redirect, timeout):
36-
if redirect:
37-
retry = urllib3.util.Retry(raise_on_redirect=False, redirect=5)
38-
else:
39-
retry = urllib3.util.Retry(total=False, redirect=False)
40-
response = super(Http, self).request(
41-
method,
42-
absolute_uri,
43-
body=body,
44-
headers=headers,
45-
retries=retry,
46-
timeout=timeout,
47-
)
38+
try:
39+
response = self.client.request(
40+
method=method,
41+
url=absolute_uri,
42+
headers=headers,
43+
content=body,
44+
timeout=timeout,
45+
follow_redirects=redirect,
46+
extensions=self.extensions,
47+
)
48+
except httpx.ConnectError as error:
49+
raise RuntimeError(
50+
f"{error}.\n\nMethod: {method}\nURL: {absolute_uri}"
51+
) from error
4852

4953
# Transform response into something akin to httplib2
5054
# response object.
51-
content = response.data
52-
status = response.status
53-
reason = response.reason
55+
content = response.content
56+
status = response.status_code
57+
reason = response.reason_phrase
58+
http_version = response.http_version
5459
headers = response.headers
55-
headers['status'] = str(status)
56-
headers['reason'] = reason
60+
headers["status"] = str(status)
61+
headers["reason"] = str(reason)
62+
headers["http_protocol_version"] = str(http_version)
5763

58-
# Shut down open PoolManagers whose connections have completed to
59-
# save on socket file descriptors.
60-
self.clear()
6164
return headers, content
6265

6366

@@ -85,59 +88,71 @@ class VerboseHttp(Http):
8588
# Can include response object attributes that are not
8689
# technically headers.
8790
HEADER_BLACKLIST = [
88-
'status',
89-
'reason',
91+
"status",
92+
"reason",
93+
"http_protocol_version",
9094
]
9195

92-
REQUEST_PREFIX = '>'
93-
RESPONSE_PREFIX = '<'
96+
REQUEST_PREFIX = ">"
97+
RESPONSE_PREFIX = "<"
9498
COLORMAP = {
95-
'caption': os.environ.get('GABBI_CAPTION_COLOR', 'BLUE').upper(),
96-
'header': os.environ.get('GABBI_HEADER_COLOR', 'YELLOW').upper(),
97-
'request': os.environ.get('GABBI_REQUEST_COLOR', 'CYAN').upper(),
98-
'status': os.environ.get('GABBI_STATUS_COLOR', 'CYAN').upper(),
99+
"caption": os.environ.get("GABBI_CAPTION_COLOR", "BLUE").upper(),
100+
"header": os.environ.get("GABBI_HEADER_COLOR", "YELLOW").upper(),
101+
"request": os.environ.get("GABBI_REQUEST_COLOR", "CYAN").upper(),
102+
"status": os.environ.get("GABBI_STATUS_COLOR", "CYAN").upper(),
99103
}
100104

101105
def __init__(self, **kwargs):
102-
self.caption = kwargs.pop('caption')
103-
self._show_body = kwargs.pop('body')
104-
self._show_headers = kwargs.pop('headers')
105-
self._use_color = kwargs.pop('colorize')
106-
self._stream = kwargs.pop('stream')
106+
self.caption = kwargs.pop("caption")
107+
self._show_body = kwargs.pop("body")
108+
self._show_headers = kwargs.pop("headers")
109+
self._use_color = kwargs.pop("colorize")
110+
self._stream = kwargs.pop("stream")
107111
if self._use_color:
108112
self.colorize = utils.get_colorizer(self._stream)
109-
super(VerboseHttp, self).__init__(**kwargs)
113+
super().__init__(**kwargs)
110114

111115
def request(self, absolute_uri, method, body, headers, redirect, timeout):
112116
"""Display request parameters before requesting."""
113117

114-
self._verbose_output('#### %s ####' % self.caption,
115-
color=self.COLORMAP['caption'])
116-
self._verbose_output('%s %s' % (method, absolute_uri),
117-
prefix=self.REQUEST_PREFIX,
118-
color=self.COLORMAP['request'])
118+
self._verbose_output(
119+
f"#### {self.caption} ####",
120+
color=self.COLORMAP["caption"],
121+
)
122+
self._verbose_output(
123+
f"{method} {absolute_uri}",
124+
prefix=self.REQUEST_PREFIX,
125+
color=self.COLORMAP["request"],
126+
)
119127

120128
self._print_headers(headers, prefix=self.REQUEST_PREFIX)
121129
self._print_body(headers, body)
122130

123-
response, content = super(VerboseHttp, self).request(
124-
absolute_uri, method, body, headers, redirect, timeout)
131+
response, content = super().request(
132+
absolute_uri,
133+
method,
134+
body,
135+
headers,
136+
redirect,
137+
timeout,
138+
)
125139

126140
# Blank line for division
127-
self._verbose_output('')
128-
self._verbose_output('%s %s' % (response['status'],
129-
response['reason']),
130-
prefix=self.RESPONSE_PREFIX,
131-
color=self.COLORMAP['status'])
141+
self._verbose_output("")
142+
self._verbose_output(
143+
f'{response["status"]} {response["reason"]}',
144+
prefix=self.RESPONSE_PREFIX,
145+
color=self.COLORMAP["status"],
146+
)
132147
self._print_headers(response, prefix=self.RESPONSE_PREFIX)
133148

134149
# response body
135150
self._print_body(response, content)
136-
self._verbose_output('')
151+
self._verbose_output("")
137152

138153
return (response, content)
139154

140-
def _print_headers(self, headers, prefix=''):
155+
def _print_headers(self, headers, prefix=""):
141156
"""Output request or response headers."""
142157
if self._show_headers:
143158
for key in headers:
@@ -148,11 +163,11 @@ def _print_body(self, headers, content):
148163
"""Output body if not binary."""
149164
# Use text/plain as the default so that when there is not content-type
150165
# we can still see the output.
151-
content_type = utils.extract_content_type(headers, 'text/plain')[0]
166+
content_type = utils.extract_content_type(headers, "text/plain")[0]
152167
if self._show_body and utils.not_binary(content_type):
153168
content = utils.decode_response_content(headers, content)
154169
if isinstance(content, bytes):
155-
content = content.decode('utf-8')
170+
content = content.decode("utf-8")
156171
# TODO(cdent): Using the JSONHandler here instead of
157172
# just the json module to make it clear that eventually
158173
# we could pretty print any printable output by using a
@@ -166,57 +181,50 @@ def _print_body(self, headers, content):
166181
except ValueError:
167182
# It it didn't decode for some reason treat it as a string.
168183
pass
169-
self._verbose_output('')
184+
self._verbose_output("")
170185
if content:
171186
self._verbose_output(content)
172187

173-
def _print_header(self, name, value, prefix='', stream=None):
188+
def _print_header(self, name, value, prefix="", stream=None):
174189
"""Output one single header."""
175-
header = self.colorize(self.COLORMAP['header'], "%s:" % name)
176-
self._verbose_output("%s %s" % (header, value), prefix=prefix,
177-
stream=stream)
190+
header = self.colorize(self.COLORMAP["header"], f"{name}:")
191+
self._verbose_output(f"{header} {value}", prefix=prefix, stream=stream)
178192

179-
def _verbose_output(self, message, prefix='', color=None, stream=None):
193+
def _verbose_output(self, message, prefix="", color=None, stream=None):
180194
"""Output a message."""
181195
stream = stream or self._stream
182196
if prefix and message:
183-
print(prefix, end=' ', file=stream)
197+
print(prefix, end=" ", file=stream)
184198
if color:
185199
message = self.colorize(color, message)
186200
print(message, file=stream)
187201

188202

189203
def get_http(
190204
verbose=False,
191-
caption='',
205+
caption="",
192206
cert_validate=True,
193207
hostname=None,
194208
timeout=30,
195209
):
196210
"""Return an ``Http`` class for making requests."""
197-
cert_validation = {'cert_reqs': 'CERT_NONE'} if not cert_validate else {}
198-
199211
if not verbose:
200212
return Http(
201-
strict=True,
202-
ca_certs=certifi.where(),
203213
server_hostname=hostname,
204214
timeout=timeout,
205-
**cert_validation
215+
cert_validate=cert_validate,
206216
)
207217

208-
headers = False if verbose == 'body' else True
209-
body = False if verbose == 'headers' else True
218+
headers = verbose != "body"
219+
body = verbose != "headers"
210220

211221
return VerboseHttp(
212222
headers=headers,
213223
body=body,
214224
stream=sys.stdout,
215225
caption=caption,
216226
colorize=True,
217-
strict=True,
218-
ca_certs=certifi.where(),
219227
server_hostname=hostname,
220228
timeout=timeout,
221-
**cert_validation
229+
cert_validate=cert_validate,
222230
)

‎requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
pbr
22
pytest
33
PyYAML
4-
urllib3>=1.26.9,<2.0.0
4+
httpx
55
certifi
66
jsonpath-rw-ext>=1.0.0
77
wsgi-intercept>=1.13.0

0 commit comments

Comments
 (0)
Please sign in to comment.