Permalink
Browse files

document and change HTTPHeaderDict, update CHANGES for bugfix

  • Loading branch information...
1 parent 49b0279 commit 332a901c17bcc32bdd3db0009dac737f3c34a928 @jschneier jschneier committed Feb 7, 2014
Showing with 72 additions and 16 deletions.
  1. +1 −1 CHANGES.rst
  2. +3 −0 docs/collections.rst
  3. +8 −3 test/test_collections.py
  4. +58 −9 urllib3/_collections.py
  5. +2 −3 urllib3/response.py
View
@@ -40,7 +40,7 @@ dev (master)
* Headers are now passed and stored as a custom
``urllib3.collections_.HTTPHeaderDict`` object rather than a plain ``dict``.
- (Issue #329)
+ (Issue #329, #333). Headers no longer lose their case on Python3 (Issue #236)
* ``urllib3.contrib.pyopenssl`` now uses the operating system's default CA
certificates on inject. (Issue #332)
View
@@ -8,3 +8,6 @@ components in a decoupled and application-agnostic design.
.. autoclass:: RecentlyUsedContainer
:members:
+
+ .. autoclass:: HTTPHeaderDict
+ :members:
View
@@ -128,7 +128,7 @@ def test_iter(self):
class TestHTTPHeaderDict(unittest.TestCase):
def setUp(self):
self.d = HTTPHeaderDict(A='foo')
- self.d.append('a', 'bar')
+ self.d.add('a', 'bar')
def test_overwriting_with_setitem_replaces(self):
d = HTTPHeaderDict()
@@ -144,15 +144,20 @@ def test_copy(self):
self.assertTrue(self.d is not h)
self.assertEqual(self.d, h)
- def test_append(self):
+ def test_add(self):
d = HTTPHeaderDict()
d['A'] = 'foo'
- d.append('a', 'bar')
+ d.add('a', 'bar')
self.assertEqual(d['a'], 'foo, bar')
self.assertEqual(d['A'], 'foo, bar')
+ def test_getlist(self):
+ self.assertEqual(self.d.getlist('a'), ['foo', 'bar'])
+ self.assertEqual(self.d.getlist('A'), ['foo', 'bar'])
+ self.assertEqual(self.d.getlist('b'), [])
+
def test_delitem(self):
del self.d['a']
self.assertFalse('a' in self.d)
View
@@ -105,27 +105,76 @@ def keys(self):
class HTTPHeaderDict(MutableMapping):
+ """
+ :param headers:
+ An iterable of field-value pairs. Must not contain multiple field names
+ when compared case-insensitively.
+
+ :param kwargs:
+ Additional field-value pairs to pass in to ``dict.update``.
+
+ A ``dict`` like container for storing HTTP Headers.
+
+ Field names are stored and compared case-insensitively in compliance with
+ RFC 2616. Iteration provides the first case-sensitive key seen for each
+ case-insensitive pair.
+
+ Using ``__setitem__`` syntax overwrites fields that compare equal
+ case-insensitively in order to maintain ``dict``'s api. For fields that
+ compare equal, instead create a new ``HTTPHeaderDict`` and use ``.add``
+ in a loop.
+
+ If multiple fields that are equal case-insensitively are passed to the
+ constructor or ``.update``, the behavior is undefined and some will be
+ lost.
+
+ >>> headers = HTTPHeaderDict()
+ >>> headers.add('Set-Cookie', 'foo=bar')
+ >>> headers.add('set-cookie', 'baz=quxx')
+ >>> headers['content-length'] = '7'
+ >>> headers['SET-cookie']
+ 'foo=bar, baz=quxx'
+ >>> headers['Content-Length']
+ '7'
+
+ If you want to access the raw headers with their original casing
+ for debugging purposes you can access the private ``._data`` attribute
+ which is a normal python ``dict`` that maps the case-insensitive key to a
+ list of tuples stored as (case-sensitive-original-name, value). Using the
+ structure from above as our example:
+
+ >>> headers._data
+ {'set-cookie': [('Set-Cookie', 'foo=bar'), ('set-cookie', 'baz=quxx')],
+ 'content-length': [('content-length', '7')]}
+ """
def __init__(self, headers=None, **kwargs):
self._data = {}
if headers is None:
headers = {}
self.update(headers, **kwargs)
- def raw_header(self, key):
- return self._data[key.lower()]
+ def add(self, key, value):
+ """Adds a (name, value) pair, doesn't overwrite the value if it already
+ exists.
- def append(self, key, value):
+ >>> headers = HTTPHeaderDict(foo='bar')
+ >>> headers.add('Foo', 'baz')
+ >>> headers['foo']
+ 'bar, baz'
+ """
self._data.setdefault(key.lower(), []).append((key, value))
- def get_all(self):
- return dict(self.items())
+ def getlist(self, key):
+ """Returns a list of all the values for the named field. Returns an
+ empty list if the key doesn't exist."""
+ return self[key].split(', ') if key in self else []
def copy(self):
h = HTTPHeaderDict()
- for key in self:
- for rawkey, value in self.raw_header(key):
- h.append(rawkey, value)
+ for key in self._data:
+ for rawkey, value in self._data[key]:
+ h.add(rawkey, value)
return h
def __eq__(self, other):
@@ -153,4 +202,4 @@ def __iter__(self):
yield headers[0][0]
def __repr__(self):
- return '%s(%r)' % (self.__class__.__name__, self.get_all())
+ return '%s(%r)' % (self.__class__.__name__, dict(self.items()))
View
@@ -253,10 +253,9 @@ def from_httplib(ResponseCls, r, **response_kw):
with ``original_response=r``.
"""
- _headers = HTTPHeaderDict()
+ headers = HTTPHeaderDict()
for k, v in r.getheaders():
- _headers.append(k, v)
- headers = _headers.get_all()
+ headers.add(k, v)
# HTTPResponse objects in Python 3 don't have a .strict attribute
strict = getattr(r, 'strict', 0)

0 comments on commit 332a901

Please sign in to comment.