1
0
mirror of https://github.com/kevin1024/vcrpy.git synced 2025-12-09 01:03:24 +00:00

Add HeadersDict, and mark add_header deprecated.

HeadersDict is a subclass of CaseInsensitiveDict with two new features:

  1. Preserve the case of the header key from the first time it was set.
     This means that later munging won't modify the key case. (You can
     force picking up the new case with `del` followed by setting.)

  2. If the value is a list or tuple, unpack it and store the first
     element. This is the same as how `Request.add_header()` used to work.

For backward compatibility this commit preserves `Request.add_header()` but
marks it deprecated.
This commit is contained in:
Aron Griffis
2015-08-24 22:14:38 -04:00
parent 646d12df94
commit 7312229aef
4 changed files with 79 additions and 35 deletions

View File

@@ -73,7 +73,7 @@ def test_remove_nonexistent_post_data_parameters():
def test_remove_json_post_data_parameters(): def test_remove_json_post_data_parameters():
body = b'{"id": "secret", "foo": "bar", "baz": "qux"}' body = b'{"id": "secret", "foo": "bar", "baz": "qux"}'
request = Request('POST', 'http://google.com', body, {}) request = Request('POST', 'http://google.com', body, {})
request.add_header('Content-Type', 'application/json') request.headers['Content-Type'] = 'application/json'
remove_post_data_parameters(request, ['id']) remove_post_data_parameters(request, ['id'])
request_body_json = json.loads(request.body.decode('utf-8')) request_body_json = json.loads(request.body.decode('utf-8'))
expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}'.decode('utf-8')) expected_json = json.loads(b'{"foo": "bar", "baz": "qux"}'.decode('utf-8'))
@@ -83,7 +83,7 @@ def test_remove_json_post_data_parameters():
def test_remove_all_json_post_data_parameters(): def test_remove_all_json_post_data_parameters():
body = b'{"id": "secret", "foo": "bar"}' body = b'{"id": "secret", "foo": "bar"}'
request = Request('POST', 'http://google.com', body, {}) request = Request('POST', 'http://google.com', body, {})
request.add_header('Content-Type', 'application/json') request.headers['Content-Type'] = 'application/json'
remove_post_data_parameters(request, ['id', 'foo']) remove_post_data_parameters(request, ['id', 'foo'])
assert request.body == b'{}' assert request.body == b'{}'
@@ -91,6 +91,6 @@ def test_remove_all_json_post_data_parameters():
def test_remove_nonexistent_json_post_data_parameters(): def test_remove_nonexistent_json_post_data_parameters():
body = b'{}' body = b'{}'
request = Request('POST', 'http://google.com', body, {}) request = Request('POST', 'http://google.com', body, {})
request.add_header('Content-Type', 'application/json') request.headers['Content-Type'] = 'application/json'
remove_post_data_parameters(request, ['id']) remove_post_data_parameters(request, ['id'])
assert request.body == b'{}' assert request.body == b'{}'

View File

@@ -1,6 +1,6 @@
import pytest import pytest
from vcr.request import Request from vcr.request import Request, HeadersDict
def test_str(): def test_str():
@@ -12,11 +12,16 @@ def test_headers():
headers = {'X-Header1': ['h1'], 'X-Header2': 'h2'} headers = {'X-Header1': ['h1'], 'X-Header2': 'h2'}
req = Request('GET', 'http://go.com/', '', headers) req = Request('GET', 'http://go.com/', '', headers)
assert req.headers == {'X-Header1': 'h1', 'X-Header2': 'h2'} assert req.headers == {'X-Header1': 'h1', 'X-Header2': 'h2'}
req.headers['X-Header1'] = 'h11'
req.add_header('X-Header1', 'h11')
assert req.headers == {'X-Header1': 'h11', 'X-Header2': 'h2'} assert req.headers == {'X-Header1': 'h11', 'X-Header2': 'h2'}
def test_add_header_deprecated():
req = Request('GET', 'http://go.com/', '', {})
pytest.deprecated_call(req.add_header, 'foo', 'bar')
assert req.headers == {'foo': 'bar'}
@pytest.mark.parametrize("uri, expected_port", [ @pytest.mark.parametrize("uri, expected_port", [
('http://go.com/', 80), ('http://go.com/', 80),
('http://go.com:80/', 80), ('http://go.com:80/', 80),
@@ -36,3 +41,30 @@ def test_uri():
req = Request('GET', 'http://go.com:80/', '', {}) req = Request('GET', 'http://go.com:80/', '', {})
assert req.uri == 'http://go.com:80/' assert req.uri == 'http://go.com:80/'
def test_HeadersDict():
# Simple test of CaseInsensitiveDict
h = HeadersDict()
assert h == {}
h['Content-Type'] = 'application/json'
assert h == {'Content-Type': 'application/json'}
assert h['content-type'] == 'application/json'
assert h['CONTENT-TYPE'] == 'application/json'
# Test feature of HeadersDict: devolve list to first element
h = HeadersDict()
assert h == {}
h['x'] = ['foo', 'bar']
assert h == {'x': 'foo'}
# Test feature of HeadersDict: preserve original key case
h = HeadersDict()
assert h == {}
h['Content-Type'] = 'application/json'
assert h == {'Content-Type': 'application/json'}
h['content-type'] = 'text/plain'
assert h == {'Content-Type': 'text/plain'}
h['CONtent-tyPE'] = 'whoa'
assert h == {'Content-Type': 'whoa'}

View File

@@ -1,3 +1,4 @@
import warnings
from six import BytesIO, text_type from six import BytesIO, text_type
from six.moves.urllib.parse import urlparse, parse_qsl from six.moves.urllib.parse import urlparse, parse_qsl
from .util import CaseInsensitiveDict from .util import CaseInsensitiveDict
@@ -6,23 +7,6 @@ from .util import CaseInsensitiveDict
class Request(object): class Request(object):
""" """
VCR's representation of a request. VCR's representation of a request.
There is a weird quirk in HTTP. You can send the same header twice. For
this reason, headers are represented by a dict, with lists as the values.
However, it appears that HTTPlib is completely incapable of sending the
same header twice. This puts me in a weird position: I want to be able to
accurately represent HTTP headers in cassettes, but I don't want the extra
step of always having to do [0] in the general case, i.e.
request.headers['key'][0]
In addition, some servers sometimes send the same header more than once,
and httplib *can* deal with this situation.
Futhermore, I wanted to keep the request and response cassette format as
similar as possible.
For this reason, in cassettes I keep a dict with lists as keys, but once
deserialized into VCR, I keep them as plain, naked dicts.
""" """
def __init__(self, method, uri, body, headers): def __init__(self, method, uri, body, headers):
@@ -33,9 +17,7 @@ class Request(object):
self.body = body.read() self.body = body.read()
else: else:
self.body = body self.body = body
self.headers = CaseInsensitiveDict() self.headers = headers
for key, value in headers.items():
self.add_header(key, value)
@property @property
def headers(self): def headers(self):
@@ -43,8 +25,8 @@ class Request(object):
@headers.setter @headers.setter
def headers(self, value): def headers(self, value):
if not isinstance(value, CaseInsensitiveDict): if not isinstance(value, HeadersDict):
value = CaseInsensitiveDict(value) value = HeadersDict(value)
self._headers = value self._headers = value
@property @property
@@ -58,11 +40,10 @@ class Request(object):
self._body = value self._body = value
def add_header(self, key, value): def add_header(self, key, value):
# see class docstring for an explanation warnings.warn("Request.add_header is deprecated. "
if isinstance(value, (tuple, list)): "Please assign to request.headers instead.",
self.headers[key] = value[0] DeprecationWarning)
else: self.headers[key] = value
self.headers[key] = value
@property @property
def scheme(self): def scheme(self):
@@ -116,3 +97,35 @@ class Request(object):
@classmethod @classmethod
def _from_dict(cls, dct): def _from_dict(cls, dct):
return Request(**dct) return Request(**dct)
class HeadersDict(CaseInsensitiveDict):
"""
There is a weird quirk in HTTP. You can send the same header twice. For
this reason, headers are represented by a dict, with lists as the values.
However, it appears that HTTPlib is completely incapable of sending the
same header twice. This puts me in a weird position: I want to be able to
accurately represent HTTP headers in cassettes, but I don't want the extra
step of always having to do [0] in the general case, i.e.
request.headers['key'][0]
In addition, some servers sometimes send the same header more than once,
and httplib *can* deal with this situation.
Futhermore, I wanted to keep the request and response cassette format as
similar as possible.
For this reason, in cassettes I keep a dict with lists as keys, but once
deserialized into VCR, I keep them as plain, naked dicts.
"""
def __setitem__(self, key, value):
if isinstance(value, (tuple, list)):
value = value[0]
# Preserve the case from the first time this key was set.
old = self._store.get(key.lower())
if old:
key = old[0]
super(HeadersDict, self).__setitem__(key, value)

View File

@@ -188,8 +188,7 @@ class VCRConnection(object):
log.debug('Got {0}'.format(self._vcr_request)) log.debug('Got {0}'.format(self._vcr_request))
def putheader(self, header, *values): def putheader(self, header, *values):
for value in values: self._vcr_request.headers[header] = values
self._vcr_request.add_header(header, value)
def send(self, data): def send(self, data):
''' '''