diff --git a/tests/unit/test_filters.py b/tests/unit/test_filters.py index 2bab38c..30bb203 100644 --- a/tests/unit/test_filters.py +++ b/tests/unit/test_filters.py @@ -73,7 +73,7 @@ def test_remove_nonexistent_post_data_parameters(): def test_remove_json_post_data_parameters(): body = b'{"id": "secret", "foo": "bar", "baz": "qux"}' 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']) request_body_json = json.loads(request.body.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(): body = b'{"id": "secret", "foo": "bar"}' 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']) assert request.body == b'{}' @@ -91,6 +91,6 @@ def test_remove_all_json_post_data_parameters(): def test_remove_nonexistent_json_post_data_parameters(): body = b'{}' 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']) assert request.body == b'{}' diff --git a/tests/unit/test_request.py b/tests/unit/test_request.py index 9a26acc..a89e15a 100644 --- a/tests/unit/test_request.py +++ b/tests/unit/test_request.py @@ -1,6 +1,6 @@ import pytest -from vcr.request import Request +from vcr.request import Request, HeadersDict def test_str(): @@ -12,11 +12,16 @@ def test_headers(): headers = {'X-Header1': ['h1'], 'X-Header2': 'h2'} req = Request('GET', 'http://go.com/', '', headers) assert req.headers == {'X-Header1': 'h1', 'X-Header2': 'h2'} - - req.add_header('X-Header1', 'h11') + req.headers['X-Header1'] = 'h11' 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", [ ('http://go.com/', 80), ('http://go.com:80/', 80), @@ -36,3 +41,30 @@ def test_uri(): req = Request('GET', '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'} diff --git a/vcr/request.py b/vcr/request.py index bc15e6f..63e451e 100644 --- a/vcr/request.py +++ b/vcr/request.py @@ -1,3 +1,4 @@ +import warnings from six import BytesIO, text_type from six.moves.urllib.parse import urlparse, parse_qsl from .util import CaseInsensitiveDict @@ -6,23 +7,6 @@ from .util import CaseInsensitiveDict class Request(object): """ 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): @@ -33,9 +17,7 @@ class Request(object): self.body = body.read() else: self.body = body - self.headers = CaseInsensitiveDict() - for key, value in headers.items(): - self.add_header(key, value) + self.headers = headers @property def headers(self): @@ -43,8 +25,8 @@ class Request(object): @headers.setter def headers(self, value): - if not isinstance(value, CaseInsensitiveDict): - value = CaseInsensitiveDict(value) + if not isinstance(value, HeadersDict): + value = HeadersDict(value) self._headers = value @property @@ -58,11 +40,10 @@ class Request(object): self._body = value def add_header(self, key, value): - # see class docstring for an explanation - if isinstance(value, (tuple, list)): - self.headers[key] = value[0] - else: - self.headers[key] = value + warnings.warn("Request.add_header is deprecated. " + "Please assign to request.headers instead.", + DeprecationWarning) + self.headers[key] = value @property def scheme(self): @@ -116,3 +97,35 @@ class Request(object): @classmethod def _from_dict(cls, 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) diff --git a/vcr/stubs/__init__.py b/vcr/stubs/__init__.py index a5ec50d..19d57f1 100644 --- a/vcr/stubs/__init__.py +++ b/vcr/stubs/__init__.py @@ -188,8 +188,7 @@ class VCRConnection(object): log.debug('Got {0}'.format(self._vcr_request)) def putheader(self, header, *values): - for value in values: - self._vcr_request.add_header(header, value) + self._vcr_request.headers[header] = values def send(self, data): '''