Don't patch Requests globally on import

This also upgrades the Requests dependency to 2.4+ (released in 2014)
to avoid having to do version comparisons altogether.

Refs https://bugs.launchpad.net/python-swiftclient/+bug/1904551

Signed-off-by: Aarni Koskela <akx@iki.fi>
Change-Id: I58399f6c526b0b78462f31739c43076314ba9e76
This commit is contained in:
Aarni Koskela 2022-02-10 18:37:09 +02:00 committed by Tim Burke
parent 2636965f38
commit c09621eb42
4 changed files with 62 additions and 41 deletions

View File

@ -1,3 +1,3 @@
requests>=1.1.0 requests>=2.4.0
six>=1.9.0 six>=1.9.0

View File

@ -18,11 +18,9 @@ OpenStack Swift client library used internally
""" """
import socket import socket
import re import re
import requests
import logging import logging
import warnings import warnings
from distutils.version import StrictVersion
from requests.exceptions import RequestException, SSLError from requests.exceptions import RequestException, SSLError
from six.moves import http_client from six.moves import http_client
from six.moves.urllib.parse import quote as _quote, unquote from six.moves.urllib.parse import quote as _quote, unquote
@ -32,6 +30,7 @@ import six
from swiftclient import version as swiftclient_version from swiftclient import version as swiftclient_version
from swiftclient.exceptions import ClientException from swiftclient.exceptions import ClientException
from swiftclient.requests_compat import SwiftClientRequestsSession
from swiftclient.utils import ( from swiftclient.utils import (
iter_wrapper, LengthWrapper, ReadableToIterable, parse_api_response, iter_wrapper, LengthWrapper, ReadableToIterable, parse_api_response,
get_body) get_body)
@ -64,20 +63,6 @@ try:
except ImportError: except ImportError:
pass pass
# requests version 1.2.3 try to encode headers in ascii, preventing
# utf-8 encoded header to be 'prepared'. This also affects all
# (or at least most) versions of requests on py3
if StrictVersion(requests.__version__) < StrictVersion('2.0.0') \
or not six.PY2:
from requests.structures import CaseInsensitiveDict
def prepare_unicode_headers(self, headers):
if headers:
self.headers = CaseInsensitiveDict(headers)
else:
self.headers = CaseInsensitiveDict()
requests.models.PreparedRequest.prepare_headers = prepare_unicode_headers
logger = logging.getLogger("swiftclient") logger = logging.getLogger("swiftclient")
logger.addHandler(logging.NullHandler()) logger.addHandler(logging.NullHandler())
@ -398,7 +383,7 @@ class HTTPConnection(object):
self.host = self.parsed_url.netloc self.host = self.parsed_url.netloc
self.port = self.parsed_url.port self.port = self.parsed_url.port
self.requests_args = {} self.requests_args = {}
self.request_session = requests.Session() self.request_session = SwiftClientRequestsSession()
# Don't use requests's default headers # Don't use requests's default headers
self.request_session.headers = None self.request_session.headers = None
self.resp = None self.resp = None
@ -1434,11 +1419,6 @@ def put_object(url, token=None, container=None, name=None, contents=None,
content_length = int(v) content_length = int(v)
if content_type is not None: if content_type is not None:
headers['Content-Type'] = content_type headers['Content-Type'] = content_type
elif 'Content-Type' not in headers:
if StrictVersion(requests.__version__) < StrictVersion('2.4.0'):
# python-requests sets application/x-www-form-urlencoded otherwise
# if using python3.
headers['Content-Type'] = ''
if not contents: if not contents:
headers['Content-Length'] = '0' headers['Content-Length'] = '0'

View File

@ -0,0 +1,57 @@
# Copyright (c) 2010-2022 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import requests
from requests.sessions import merge_setting, merge_hooks
from requests.structures import CaseInsensitiveDict
class SwiftClientPreparedRequest(requests.PreparedRequest):
def prepare_headers(self, headers):
try:
return super().prepare_headers(headers)
except UnicodeError:
# If we got an unicode error from the superclass's prepare_headers,
# we had a non-spec-compliant non-ASCII header
# (e.g. an UTF-8 encoded Swift object metadata header).
# In that case, we just pass it through and hope nothing
# bad will happen from not following the HTTP spec.
self.headers = CaseInsensitiveDict(headers or {})
class SwiftClientRequestsSession(requests.Session):
def prepare_request(self, request):
# Close to the superclass's implementation,
# but no cookies or .netrc authentication overrides here.
p = SwiftClientPreparedRequest()
headers = merge_setting(
request.headers,
self.headers,
dict_class=CaseInsensitiveDict,
)
p.prepare(
method=request.method.upper(),
url=request.url,
files=request.files,
data=request.data,
json=request.json,
headers=headers,
params=merge_setting(request.params, self.params),
auth=merge_setting(request.auth, self.auth),
cookies=None,
hooks=merge_hooks(request.hooks, self.hooks),
)
return p

View File

@ -1325,7 +1325,6 @@ class TestHeadObject(MockHttpTest):
class TestPutObject(MockHttpTest): class TestPutObject(MockHttpTest):
@mock.patch('swiftclient.requests.__version__', '2.2.0')
def test_ok(self): def test_ok(self):
c.http_connection = self.fake_http_connection(200) c.http_connection = self.fake_http_connection(200)
args = ('http://www.test.com', 'TOKEN', 'container', 'obj', 'body', 4) args = ('http://www.test.com', 'TOKEN', 'container', 'obj', 'body', 4)
@ -1336,7 +1335,6 @@ class TestPutObject(MockHttpTest):
('PUT', '/container/obj', 'body', { ('PUT', '/container/obj', 'body', {
'x-auth-token': 'TOKEN', 'x-auth-token': 'TOKEN',
'content-length': '4', 'content-length': '4',
'content-type': ''
}), }),
]) ])
@ -1383,7 +1381,6 @@ class TestPutObject(MockHttpTest):
self.assertEqual(len(w), 1) self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, UserWarning)) self.assertTrue(issubclass(w[-1].category, UserWarning))
@mock.patch('swiftclient.requests.__version__', '2.2.0')
def test_server_error(self): def test_server_error(self):
body = 'c' * 60 body = 'c' * 60
headers = {'foo': 'bar'} headers = {'foo': 'bar'}
@ -1398,8 +1395,7 @@ class TestPutObject(MockHttpTest):
self.assertEqual(e.http_status, 500) self.assertEqual(e.http_status, 500)
self.assertRequests([ self.assertRequests([
('PUT', '/asdf/asdf', 'asdf', { ('PUT', '/asdf/asdf', 'asdf', {
'x-auth-token': 'asdf', 'x-auth-token': 'asdf'}),
'content-type': ''}),
]) ])
def test_query_string(self): def test_query_string(self):
@ -1540,19 +1536,7 @@ class TestPutObject(MockHttpTest):
self.assertEqual(request_header['etag'], b'1234-5678') self.assertEqual(request_header['etag'], b'1234-5678')
self.assertEqual(request_header['content-type'], b'text/plain') self.assertEqual(request_header['content-type'], b'text/plain')
@mock.patch('swiftclient.requests.__version__', '2.2.0') def test_no_content_type_requests(self):
def test_no_content_type_old_requests(self):
conn = c.http_connection(u'http://www.test.com/')
resp = MockHttpResponse(status=200)
conn[1].getresponse = resp.fake_response
conn[1]._request = resp._fake_request
c.put_object(url='http://www.test.com', http_conn=conn)
request_header = resp.requests_params['headers']
self.assertEqual(request_header['content-type'], b'')
@mock.patch('swiftclient.requests.__version__', '2.4.0')
def test_no_content_type_new_requests(self):
conn = c.http_connection(u'http://www.test.com/') conn = c.http_connection(u'http://www.test.com/')
resp = MockHttpResponse(status=200) resp = MockHttpResponse(status=200)
conn[1].getresponse = resp.fake_response conn[1].getresponse = resp.fake_response