MultiDict
authorKenneth Reitz <me@kennethreitz.com>
Mon, 20 Aug 2012 14:43:16 +0000 (00:43 +1000)
committerKenneth Reitz <me@kennethreitz.com>
Mon, 20 Aug 2012 14:43:16 +0000 (00:43 +1000)
requests/structures.py

index 3fda984..ef7e13f 100644 (file)
@@ -8,6 +8,7 @@ Data structures that power Requests.
 
 """
 
+_missing = KeyError
 
 class CaseInsensitiveDict(dict):
     """Case-insensitive Dictionary
@@ -65,3 +66,359 @@ class LookupDict(dict):
 
     def get(self, key, default=None):
         return self.__dict__.get(key, default)
+
+
+class TypeConversionDict(dict):
+    """Works like a regular dict but the :meth:`get` method can perform
+    type conversions.  :class:`MultiDict` and :class:`CombinedMultiDict`
+    are subclasses of this class and provide the same feature.
+
+    .. versionadded:: 0.5
+    """
+
+    def get(self, key, default=None, type=None):
+        """Return the default value if the requested data doesn't exist.
+        If `type` is provided and is a callable it should convert the value,
+        return it or raise a :exc:`ValueError` if that is not possible.  In
+        this case the function will return the default as if the value was not
+        found:
+
+        >>> d = TypeConversionDict(foo='42', bar='blub')
+        >>> d.get('foo', type=int)
+        42
+        >>> d.get('bar', -1, type=int)
+        -1
+
+        :param key: The key to be looked up.
+        :param default: The default value to be returned if the key can't
+                        be looked up.  If not further specified `None` is
+                        returned.
+        :param type: A callable that is used to cast the value in the
+                     :class:`MultiDict`.  If a :exc:`ValueError` is raised
+                     by this callable the default value is returned.
+        """
+        try:
+            rv = self[key]
+            if type is not None:
+                rv = type(rv)
+        except (KeyError, ValueError):
+            rv = default
+        return rv
+
+
+class MultiDict(TypeConversionDict):
+    """A :class:`MultiDict` is a dictionary subclass customized to deal with
+    multiple values for the same key which is for example used by the parsing
+    functions in the wrappers.  This is necessary because some HTML form
+    elements pass multiple values for the same key.
+
+    :class:`MultiDict` implements all standard dictionary methods.
+    Internally, it saves all values for a key as a list, but the standard dict
+    access methods will only return the first value for a key. If you want to
+    gain access to the other values, too, you have to use the `list` methods as
+    explained below.
+
+    Basic Usage:
+
+    >>> d = MultiDict([('a', 'b'), ('a', 'c')])
+    >>> d
+    MultiDict([('a', 'b'), ('a', 'c')])
+    >>> d['a']
+    'b'
+    >>> d.getlist('a')
+    ['b', 'c']
+    >>> 'a' in d
+    True
+
+    It behaves like a normal dict thus all dict functions will only return the
+    first value when multiple values for one key are found.
+
+    From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a
+    subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will
+    render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP
+    exceptions.
+
+    A :class:`MultiDict` can be constructed from an iterable of
+    ``(key, value)`` tuples, a dict, a :class:`MultiDict` or from Werkzeug 0.2
+    onwards some keyword parameters.
+
+    :param mapping: the initial value for the :class:`MultiDict`.  Either a
+                    regular dict, an iterable of ``(key, value)`` tuples
+                    or `None`.
+    """
+
+    def __init__(self, mapping=None):
+        if isinstance(mapping, MultiDict):
+            dict.__init__(self, ((k, l[:]) for k, l in mapping.iterlists()))
+        elif isinstance(mapping, dict):
+            tmp = {}
+            for key, value in mapping.iteritems():
+                if isinstance(value, (tuple, list)):
+                    value = list(value)
+                else:
+                    value = [value]
+                tmp[key] = value
+            dict.__init__(self, tmp)
+        else:
+            tmp = {}
+            for key, value in mapping or ():
+                tmp.setdefault(key, []).append(value)
+            dict.__init__(self, tmp)
+
+    def __getstate__(self):
+        return dict(self.lists())
+
+    def __setstate__(self, value):
+        dict.clear(self)
+        dict.update(self, value)
+
+    def __iter__(self):
+        return self.iterkeys()
+
+    def __getitem__(self, key):
+        """Return the first data value for this key;
+        raises KeyError if not found.
+
+        :param key: The key to be looked up.
+        :raise KeyError: if the key does not exist.
+        """
+        if key in self:
+            return dict.__getitem__(self, key)[0]
+        raise BadRequestKeyError(key)
+
+    def __setitem__(self, key, value):
+        """Like :meth:`add` but removes an existing key first.
+
+        :param key: the key for the value.
+        :param value: the value to set.
+        """
+        dict.__setitem__(self, key, [value])
+
+    def add(self, key, value):
+        """Adds a new value for the key.
+
+        .. versionadded:: 0.6
+
+        :param key: the key for the value.
+        :param value: the value to add.
+        """
+        dict.setdefault(self, key, []).append(value)
+
+    def getlist(self, key, type=None):
+        """Return the list of items for a given key. If that key is not in the
+        `MultiDict`, the return value will be an empty list.  Just as `get`
+        `getlist` accepts a `type` parameter.  All items will be converted
+        with the callable defined there.
+
+        :param key: The key to be looked up.
+        :param type: A callable that is used to cast the value in the
+                     :class:`MultiDict`.  If a :exc:`ValueError` is raised
+                     by this callable the value will be removed from the list.
+        :return: a :class:`list` of all the values for the key.
+        """
+        try:
+            rv = dict.__getitem__(self, key)
+        except KeyError:
+            return []
+        if type is None:
+            return list(rv)
+        result = []
+        for item in rv:
+            try:
+                result.append(type(item))
+            except ValueError:
+                pass
+        return result
+
+    def setlist(self, key, new_list):
+        """Remove the old values for a key and add new ones.  Note that the list
+        you pass the values in will be shallow-copied before it is inserted in
+        the dictionary.
+
+        >>> d = MultiDict()
+        >>> d.setlist('foo', ['1', '2'])
+        >>> d['foo']
+        '1'
+        >>> d.getlist('foo')
+        ['1', '2']
+
+        :param key: The key for which the values are set.
+        :param new_list: An iterable with the new values for the key.  Old values
+                         are removed first.
+        """
+        dict.__setitem__(self, key, list(new_list))
+
+    def setdefault(self, key, default=None):
+        """Returns the value for the key if it is in the dict, otherwise it
+        returns `default` and sets that value for `key`.
+
+        :param key: The key to be looked up.
+        :param default: The default value to be returned if the key is not
+                        in the dict.  If not further specified it's `None`.
+        """
+        if key not in self:
+            self[key] = default
+        else:
+            default = self[key]
+        return default
+
+    def setlistdefault(self, key, default_list=None):
+        """Like `setdefault` but sets multiple values.  The list returned
+        is not a copy, but the list that is actually used internally.  This
+        means that you can put new values into the dict by appending items
+        to the list:
+
+        >>> d = MultiDict({"foo": 1})
+        >>> d.setlistdefault("foo").extend([2, 3])
+        >>> d.getlist("foo")
+        [1, 2, 3]
+
+        :param key: The key to be looked up.
+        :param default: An iterable of default values.  It is either copied
+                        (in case it was a list) or converted into a list
+                        before returned.
+        :return: a :class:`list`
+        """
+        if key not in self:
+            default_list = list(default_list or ())
+            dict.__setitem__(self, key, default_list)
+        else:
+            default_list = dict.__getitem__(self, key)
+        return default_list
+
+    def items(self, multi=False):
+        """Return a list of ``(key, value)`` pairs.
+
+        :param multi: If set to `True` the list returned will have a
+                      pair for each value of each key.  Otherwise it
+                      will only contain pairs for the first value of
+                      each key.
+
+        :return: a :class:`list`
+        """
+        return list(self.iteritems(multi))
+
+    def lists(self):
+        """Return a list of ``(key, values)`` pairs, where values is the list of
+        all values associated with the key.
+
+        :return: a :class:`list`
+        """
+        return list(self.iterlists())
+
+    def values(self):
+        """Returns a list of the first value on every key's value list.
+
+        :return: a :class:`list`.
+        """
+        return [self[key] for key in self.iterkeys()]
+
+    def listvalues(self):
+        """Return a list of all values associated with a key.  Zipping
+        :meth:`keys` and this is the same as calling :meth:`lists`:
+
+        >>> d = MultiDict({"foo": [1, 2, 3]})
+        >>> zip(d.keys(), d.listvalues()) == d.lists()
+        True
+
+        :return: a :class:`list`
+        """
+        return list(self.iterlistvalues())
+
+    def iteritems(self, multi=False):
+        """Like :meth:`items` but returns an iterator."""
+        for key, values in dict.iteritems(self):
+            if multi:
+                for value in values:
+                    yield key, value
+            else:
+                yield key, values[0]
+
+    def iterlists(self):
+        """Like :meth:`items` but returns an iterator."""
+        for key, values in dict.iteritems(self):
+            yield key, list(values)
+
+    def itervalues(self):
+        """Like :meth:`values` but returns an iterator."""
+        for values in dict.itervalues(self):
+            yield values[0]
+
+    def iterlistvalues(self):
+        """Like :meth:`listvalues` but returns an iterator."""
+        return dict.itervalues(self)
+
+    def copy(self):
+        """Return a shallow copy of this object."""
+        return self.__class__(self)
+
+    def to_dict(self, flat=True):
+        """Return the contents as regular dict.  If `flat` is `True` the
+        returned dict will only have the first item present, if `flat` is
+        `False` all values will be returned as lists.
+
+        :param flat: If set to `False` the dict returned will have lists
+                     with all the values in it.  Otherwise it will only
+                     contain the first value for each key.
+        :return: a :class:`dict`
+        """
+        if flat:
+            return dict(self.iteritems())
+        return dict(self.lists())
+
+    def update(self, other_dict):
+        """update() extends rather than replaces existing key lists."""
+        for key, value in iter_multi_items(other_dict):
+            MultiDict.add(self, key, value)
+
+    def pop(self, key, default=_missing):
+        """Pop the first item for a list on the dict.  Afterwards the
+        key is removed from the dict, so additional values are discarded:
+
+        >>> d = MultiDict({"foo": [1, 2, 3]})
+        >>> d.pop("foo")
+        1
+        >>> "foo" in d
+        False
+
+        :param key: the key to pop.
+        :param default: if provided the value to return if the key was
+                        not in the dictionary.
+        """
+        try:
+            return dict.pop(self, key)[0]
+        except KeyError, e:
+            if default is not _missing:
+                return default
+            raise BadRequestKeyError(str(e))
+
+    def popitem(self):
+        """Pop an item from the dict."""
+        try:
+            item = dict.popitem(self)
+            return (item[0], item[1][0])
+        except KeyError, e:
+            raise BadRequestKeyError(str(e))
+
+    def poplist(self, key):
+        """Pop the list for a key from the dict.  If the key is not in the dict
+        an empty list is returned.
+
+        .. versionchanged:: 0.5
+           If the key does no longer exist a list is returned instead of
+           raising an error.
+        """
+        return dict.pop(self, key, [])
+
+    def popitemlist(self):
+        """Pop a ``(key, list)`` tuple from the dict."""
+        try:
+            return dict.popitem(self)
+        except KeyError, e:
+            raise BadRequestKeyError(str(e))
+
+    def __copy__(self):
+        return self.copy()
+
+    def __repr__(self):
+        return '%s(%r)' % (self.__class__.__name__, self.items(multi=True))
\ No newline at end of file