# -*- coding: utf-8 -*- """ requests.structures ~~~~~~~~~~~~~~~~~~~ Data structures that power Requests. """ import collections from .basics import basestring, OrderedDict 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, regardless of how the header name was originally stored. If the constructor, ``.update``, or equality comparison operations are given keys that have equal ``.lower()``s, the behavior is undefined. """ __slots__ = ('_store') def __init__(self, data=None, **kwargs): self._store = collections.OrderedDict() if data is None: data = {} self.update(data, **kwargs) def __setitem__(self, key, value): # Use the lowercased key for lookups, but store the actual # key alongside the value. self._store[key.lower()] = (key, value) def __getitem__(self, key): return self._store[key.lower()][1] def __delitem__(self, key): del self._store[key.lower()] def __iter__(self): return (casedkey for casedkey, mappedvalue in self._store.values()) 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 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 str(dict(self.items())) class HTTPHeaderDict(CaseInsensitiveDict): """A case-insensitive ``dict``-like object suitable for HTTP headers that supports multiple values with the same key, via the ``add``, ``extend``, ``multiget`` and ``multiset`` methods. """ def __init__(self, data=None, **kwargs): super(HTTPHeaderDict, self).__init__() self.extend({} if data is None else data, **kwargs) # We'll store tuples in the internal dictionary, but present them as a # concatenated string when we use item access methods. # def __setitem__(self, key, val): # Special–case null values. if (not isinstance(val, basestring)) and (val is not None): raise ValueError('only string-type values (or None) are allowed') super(HTTPHeaderDict, self).__setitem__(key, (val,)) def __getitem__(self, key): val = super(HTTPHeaderDict, self).__getitem__(key) # Special–case null values. if len(val) == 1 and val[0] is None: return val[0] return ', '.join(val) def lower_items(self): return ( (lk, ', '.join(vals)) for (lk, (k, vals)) in self._store.items() ) def copy(self): return type(self)(self) def getlist(self, key): """Returns a list of all the values for the named field. Returns an empty list if the key isn't present in the dictionary.""" return list(self._store.get(key.lower(), (None, []))[1]) def setlist(self, key, values): """Set a sequence of strings to the associated key - this will overwrite any previously stored value.""" if not isinstance(values, (list, tuple)): raise ValueError('argument is not sequence') if any(not isinstance(v, basestring) for v in values): raise ValueError('non-string items in sequence') if not values: self.pop(key, None) return super(HTTPHeaderDict, self).__setitem__(key, tuple(values)) def _extend(self, key, values): new_value_tpl = key, values # Inspired by urllib3's implementation - use one call which should be # suitable for the common case. old_value_tpl = self._store.setdefault(key.lower(), new_value_tpl) if old_value_tpl is not new_value_tpl: old_key, old_values = old_value_tpl self._store[key.lower()] = (old_key, old_values + values) def add(self, key, val): """Adds a key, value pair to this dictionary - if there is already a value for this key, then the value will be appended to those values. """ if not isinstance(val, basestring): raise ValueError('value must be a string-type object') self._extend(key, (val,)) def extend(self, *args, **kwargs): """Like update, but will add values to existing sequences rather than replacing them. You can pass a mapping object or a sequence of two tuples - values in these objects can be strings or sequence of strings. """ if len(args) > 1: raise TypeError( f"extend() takes at most 1 positional " "arguments ({len(args)} given)" ) for other in args + (kwargs,): if isinstance(other, collections.Mapping): # See if looks like a HTTPHeaderDict (either urllib3's # implementation or ours). If so, then we have to add values # in one go for each key. multiget = getattr(other, 'getlist', None) if multiget: for key in other: self._extend(key, tuple(multiget(key))) continue # Otherwise, just walk over items to get them. item_seq = other.items() else: item_seq = other for ik, iv in item_seq: if isinstance(iv, basestring): self._extend(ik, (iv,)) elif any(not isinstance(v, basestring) for v in iv): raise ValueError('non-string items in sequence') else: self._extend(ik, tuple(iv)) @property def _as_dict(self): """A dictionary representation of the HTTPHeaderDict.""" d = {} for k, vals in self._store.values(): d[k] = vals[0] if len(vals) == 1 else vals return d def __repr__(self): return repr(self._as_dict) class LookupDict(dict): """Dictionary lookup object.""" def __init__(self, name=None): self.name = name super(LookupDict, self).__init__() def __repr__(self): return f'' def __getitem__(self, key): # We allow fall-through here, so values default to None return self.__dict__.get(key, None) def __iter__(self): return super(LookupDict, self).__dir__() def get(self, key, default=None): return self.__dict__.get(key, default)