summaryrefslogtreecommitdiff
path: root/requests/structures.py
diff options
context:
space:
mode:
authorColin Dunklau <colin.dunklau@gmail.com>2013-04-30 14:52:27 -0500
committerColin Dunklau <colin.dunklau@gmail.com>2013-04-30 14:52:27 -0500
commitf7596c75dce4e87ab83bdf74e8f120a4b1a5ff03 (patch)
treecfb256c7700542f9dd723769f66e6e87033b2c5a /requests/structures.py
parentab36f3cc6f91f1e4903a3f4b31501cb7fefe4555 (diff)
downloadpython-requests-f7596c75dce4e87ab83bdf74e8f120a4b1a5ff03.tar.gz
Rewrite CaseInsensitiveDict to work correctly/sanely
Fixes #649 and #1329 by making Session.headers a CaseInsensitiveDict, and fixing the implementation of CID. Credit for the brilliant idea to map `lowercased_key -> (cased_key, mapped_value)` goes to @gazpachoking, thanks a bunch. Changes from original implementation of CaseInsensitiveDict: 1. CID is rewritten as a subclass of `collections.MutableMapping`. 2. CID remembers the case of the last-set key, but `__setitem__` and `__delitem__` will handle keys without respect to case. 3. CID returns the key case as remembered for the `keys`, `items`, and `__iter__` methods. 4. Query operations (`__getitem__` and `__contains__`) are done in a case-insensitive manner: `cid['foo']` and `cid['FOO']` will return the same value. 5. The constructor as well as `update` and `__eq__` have undefined behavior when given multiple keys that have the same `lower()`. 6. The new method `lower_items` is like `iteritems`, but keys are all lowercased. 7. CID raises `KeyError` for `__getitem__` as normal dicts do. The old implementation returned 6. The `__repr__` now makes it obvious that it's not a normal dict. See PR #1333 for the discussions that lead up to this implementation
Diffstat (limited to 'requests/structures.py')
-rw-r--r--requests/structures.py89
1 files changed, 63 insertions, 26 deletions
diff --git a/requests/structures.py b/requests/structures.py
index 05f5ac15..8d02ea67 100644
--- a/requests/structures.py
+++ b/requests/structures.py
@@ -9,6 +9,7 @@ Data structures that power Requests.
"""
import os
+import collections
from itertools import islice
@@ -33,43 +34,79 @@ class IteratorProxy(object):
return "".join(islice(self.i, None, n))
-class CaseInsensitiveDict(dict):
- """Case-insensitive Dictionary
+class CaseInsensitiveDict(collections.MutableMapping):
+ """
+ A case-insensitive ``dict``-like object.
+
+ Implements all methods and operations of
+ ``collections.MutableMapping`` as well as dict's ``copy``. Also
+ provides ``lower_items``.
+
+ All keys are expected to be strings. The structure remembers the
+ case of the last key to be set, and ``iter(instance)``,
+ ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
+ will contain case-sensitive keys. However, querying and contains
+ testing is case insensitive:
+
+ cid = CaseInsensitiveDict()
+ cid['Accept'] = 'application/json'
+ cid['aCCEPT'] == 'application/json' # True
+ list(cid) == ['Accept'] # True
For example, ``headers['content-encoding']`` will return the
- value of a ``'Content-Encoding'`` response header."""
+ value of a ``'Content-Encoding'`` response header, regardless
+ of how the header name was originally stored.
- @property
- def lower_keys(self):
- if not hasattr(self, '_lower_keys') or not self._lower_keys:
- self._lower_keys = dict((k.lower(), k) for k in list(self.keys()))
- return self._lower_keys
+ If the constructor, ``.update``, or equality comparison
+ operations are given keys that have equal ``.lower()``s, the
+ behavior is undefined.
- def _clear_lower_keys(self):
- if hasattr(self, '_lower_keys'):
- self._lower_keys.clear()
+ """
+ def __init__(self, data=None, **kwargs):
+ self._store = dict()
+ if data is None:
+ data = {}
+ self.update(data, **kwargs)
def __setitem__(self, key, value):
- dict.__setitem__(self, key, value)
- self._clear_lower_keys()
+ # Use the lowercased key for lookups, but store the actual
+ # key alongside the value.
+ self._store[key.lower()] = (key, value)
- def __delitem__(self, key):
- dict.__delitem__(self, self.lower_keys.get(key.lower(), key))
- self._lower_keys.clear()
+ def __getitem__(self, key):
+ return self._store[key.lower()][1]
- def __contains__(self, key):
- return key.lower() in self.lower_keys
+ def __delitem__(self, key):
+ del self._store[key.lower()]
- def __getitem__(self, key):
- # We allow fall-through here, so values default to None
- if key in self:
- return dict.__getitem__(self, self.lower_keys[key.lower()])
+ def __iter__(self):
+ return (casedkey for casedkey, mappedvalue in self._store.values())
- def get(self, key, default=None):
- if key in self:
- return self[key]
+ def __len__(self):
+ return len(self._store)
+
+ def lower_items(self):
+ """Like iteritems(), but with all lowercase keys."""
+ return (
+ (lowerkey, keyval[1])
+ for (lowerkey, keyval)
+ in self._store.items()
+ )
+
+ def __eq__(self, other):
+ if isinstance(other, collections.Mapping):
+ other = CaseInsensitiveDict(other)
else:
- return default
+ return NotImplemented
+ # Compare insensitively
+ return dict(self.lower_items()) == dict(other.lower_items())
+
+ # Copy is required
+ def copy(self):
+ return CaseInsensitiveDict(self._store.values())
+
+ def __repr__(self):
+ return '%s(%r)' % (self.__class__.__name__, dict(self.items()))
class LookupDict(dict):