432 lines
12 KiB
Python
432 lines
12 KiB
Python
# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
|
|
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
|
import cgi
|
|
import copy
|
|
import six
|
|
import sys
|
|
|
|
try:
|
|
# Python 3
|
|
from collections.abc import MutableMapping as DictMixin
|
|
except ImportError:
|
|
# Python 2
|
|
from UserDict import DictMixin
|
|
|
|
class MultiDict(DictMixin):
|
|
|
|
"""
|
|
An ordered dictionary that can have multiple values for each key.
|
|
Adds the methods getall, getone, mixed, and add to the normal
|
|
dictionary interface.
|
|
"""
|
|
|
|
def __init__(self, *args, **kw):
|
|
if len(args) > 1:
|
|
raise TypeError(
|
|
"MultiDict can only be called with one positional argument")
|
|
if args:
|
|
if hasattr(args[0], 'iteritems'):
|
|
items = args[0].iteritems()
|
|
elif hasattr(args[0], 'items'):
|
|
items = args[0].items()
|
|
else:
|
|
items = args[0]
|
|
self._items = list(items)
|
|
else:
|
|
self._items = []
|
|
self._items.extend(six.iteritems(kw))
|
|
|
|
def __getitem__(self, key):
|
|
for k, v in self._items:
|
|
if k == key:
|
|
return v
|
|
raise KeyError(repr(key))
|
|
|
|
def __setitem__(self, key, value):
|
|
try:
|
|
del self[key]
|
|
except KeyError:
|
|
pass
|
|
self._items.append((key, value))
|
|
|
|
def add(self, key, value):
|
|
"""
|
|
Add the key and value, not overwriting any previous value.
|
|
"""
|
|
self._items.append((key, value))
|
|
|
|
def getall(self, key):
|
|
"""
|
|
Return a list of all values matching the key (may be an empty list)
|
|
"""
|
|
result = []
|
|
for k, v in self._items:
|
|
if type(key) == type(k) and key == k:
|
|
result.append(v)
|
|
return result
|
|
|
|
def getone(self, key):
|
|
"""
|
|
Get one value matching the key, raising a KeyError if multiple
|
|
values were found.
|
|
"""
|
|
v = self.getall(key)
|
|
if not v:
|
|
raise KeyError('Key not found: %r' % key)
|
|
if len(v) > 1:
|
|
raise KeyError('Multiple values match %r: %r' % (key, v))
|
|
return v[0]
|
|
|
|
def mixed(self):
|
|
"""
|
|
Returns a dictionary where the values are either single
|
|
values, or a list of values when a key/value appears more than
|
|
once in this dictionary. This is similar to the kind of
|
|
dictionary often used to represent the variables in a web
|
|
request.
|
|
"""
|
|
result = {}
|
|
multi = {}
|
|
for key, value in self._items:
|
|
if key in result:
|
|
# We do this to not clobber any lists that are
|
|
# *actual* values in this dictionary:
|
|
if key in multi:
|
|
result[key].append(value)
|
|
else:
|
|
result[key] = [result[key], value]
|
|
multi[key] = None
|
|
else:
|
|
result[key] = value
|
|
return result
|
|
|
|
def dict_of_lists(self):
|
|
"""
|
|
Returns a dictionary where each key is associated with a
|
|
list of values.
|
|
"""
|
|
result = {}
|
|
for key, value in self._items:
|
|
if key in result:
|
|
result[key].append(value)
|
|
else:
|
|
result[key] = [value]
|
|
return result
|
|
|
|
def __delitem__(self, key):
|
|
items = self._items
|
|
found = False
|
|
for i in range(len(items)-1, -1, -1):
|
|
if type(items[i][0]) == type(key) and items[i][0] == key:
|
|
del items[i]
|
|
found = True
|
|
if not found:
|
|
raise KeyError(repr(key))
|
|
|
|
def __contains__(self, key):
|
|
for k, v in self._items:
|
|
if type(k) == type(key) and k == key:
|
|
return True
|
|
return False
|
|
|
|
has_key = __contains__
|
|
|
|
def clear(self):
|
|
self._items = []
|
|
|
|
def copy(self):
|
|
return MultiDict(self)
|
|
|
|
def setdefault(self, key, default=None):
|
|
for k, v in self._items:
|
|
if key == k:
|
|
return v
|
|
self._items.append((key, default))
|
|
return default
|
|
|
|
def pop(self, key, *args):
|
|
if len(args) > 1:
|
|
raise TypeError("pop expected at most 2 arguments, got "
|
|
+ repr(1 + len(args)))
|
|
for i in range(len(self._items)):
|
|
if type(self._items[i][0]) == type(key) and self._items[i][0] == key:
|
|
v = self._items[i][1]
|
|
del self._items[i]
|
|
return v
|
|
if args:
|
|
return args[0]
|
|
else:
|
|
raise KeyError(repr(key))
|
|
|
|
def popitem(self):
|
|
return self._items.pop()
|
|
|
|
def update(self, other=None, **kwargs):
|
|
if other is None:
|
|
pass
|
|
elif hasattr(other, 'items'):
|
|
self._items.extend(other.items())
|
|
elif hasattr(other, 'keys'):
|
|
for k in other.keys():
|
|
self._items.append((k, other[k]))
|
|
else:
|
|
for k, v in other:
|
|
self._items.append((k, v))
|
|
if kwargs:
|
|
self.update(kwargs)
|
|
|
|
def __repr__(self):
|
|
items = ', '.join(['(%r, %r)' % v for v in self._items])
|
|
return '%s([%s])' % (self.__class__.__name__, items)
|
|
|
|
def __len__(self):
|
|
return len(self._items)
|
|
|
|
##
|
|
## All the iteration:
|
|
##
|
|
|
|
def keys(self):
|
|
return [k for k, v in self._items]
|
|
|
|
def iterkeys(self):
|
|
for k, v in self._items:
|
|
yield k
|
|
|
|
__iter__ = iterkeys
|
|
|
|
def items(self):
|
|
return self._items[:]
|
|
|
|
def iteritems(self):
|
|
return iter(self._items)
|
|
|
|
def values(self):
|
|
return [v for k, v in self._items]
|
|
|
|
def itervalues(self):
|
|
for k, v in self._items:
|
|
yield v
|
|
|
|
class UnicodeMultiDict(DictMixin):
|
|
"""
|
|
A MultiDict wrapper that decodes returned values to unicode on the
|
|
fly. Decoding is not applied to assigned values.
|
|
|
|
The key/value contents are assumed to be ``str``/``strs`` or
|
|
``str``/``FieldStorages`` (as is returned by the ``paste.request.parse_``
|
|
functions).
|
|
|
|
Can optionally also decode keys when the ``decode_keys`` argument is
|
|
True.
|
|
|
|
``FieldStorage`` instances are cloned, and the clone's ``filename``
|
|
variable is decoded. Its ``name`` variable is decoded when ``decode_keys``
|
|
is enabled.
|
|
|
|
"""
|
|
def __init__(self, multi=None, encoding=None, errors='strict',
|
|
decode_keys=False):
|
|
self.multi = multi
|
|
if encoding is None:
|
|
encoding = sys.getdefaultencoding()
|
|
self.encoding = encoding
|
|
self.errors = errors
|
|
self.decode_keys = decode_keys
|
|
if self.decode_keys:
|
|
items = self.multi._items
|
|
for index, item in enumerate(items):
|
|
key, value = item
|
|
key = self._encode_key(key)
|
|
items[index] = (key, value)
|
|
|
|
def _encode_key(self, key):
|
|
if self.decode_keys:
|
|
try:
|
|
key = key.encode(self.encoding, self.errors)
|
|
except AttributeError:
|
|
pass
|
|
return key
|
|
|
|
def _decode_key(self, key):
|
|
if self.decode_keys:
|
|
try:
|
|
key = key.decode(self.encoding, self.errors)
|
|
except AttributeError:
|
|
pass
|
|
return key
|
|
|
|
def _decode_value(self, value):
|
|
"""
|
|
Decode the specified value to unicode. Assumes value is a ``str`` or
|
|
`FieldStorage`` object.
|
|
|
|
``FieldStorage`` objects are specially handled.
|
|
"""
|
|
if isinstance(value, cgi.FieldStorage):
|
|
# decode FieldStorage's field name and filename
|
|
decode_name = self.decode_keys and isinstance(value.name, six.binary_type)
|
|
if six.PY2 or decode_name:
|
|
value = copy.copy(value)
|
|
if decode_name:
|
|
value.name = value.name.decode(self.encoding, self.errors)
|
|
if six.PY2:
|
|
value.filename = value.filename.decode(self.encoding, self.errors)
|
|
else:
|
|
try:
|
|
value = value.decode(self.encoding, self.errors)
|
|
except AttributeError:
|
|
pass
|
|
return value
|
|
|
|
def __getitem__(self, key):
|
|
key = self._encode_key(key)
|
|
return self._decode_value(self.multi.__getitem__(key))
|
|
|
|
def __setitem__(self, key, value):
|
|
key = self._encode_key(key)
|
|
self.multi.__setitem__(key, value)
|
|
|
|
def add(self, key, value):
|
|
"""
|
|
Add the key and value, not overwriting any previous value.
|
|
"""
|
|
key = self._encode_key(key)
|
|
self.multi.add(key, value)
|
|
|
|
def getall(self, key):
|
|
"""
|
|
Return a list of all values matching the key (may be an empty list)
|
|
"""
|
|
key = self._encode_key(key)
|
|
return [self._decode_value(v) for v in self.multi.getall(key)]
|
|
|
|
def getone(self, key):
|
|
"""
|
|
Get one value matching the key, raising a KeyError if multiple
|
|
values were found.
|
|
"""
|
|
key = self._encode_key(key)
|
|
return self._decode_value(self.multi.getone(key))
|
|
|
|
def mixed(self):
|
|
"""
|
|
Returns a dictionary where the values are either single
|
|
values, or a list of values when a key/value appears more than
|
|
once in this dictionary. This is similar to the kind of
|
|
dictionary often used to represent the variables in a web
|
|
request.
|
|
"""
|
|
unicode_mixed = {}
|
|
for key, value in six.iteritems(self.multi.mixed()):
|
|
if isinstance(value, list):
|
|
value = [self._decode_value(value) for value in value]
|
|
else:
|
|
value = self._decode_value(value)
|
|
unicode_mixed[self._decode_key(key)] = value
|
|
return unicode_mixed
|
|
|
|
def dict_of_lists(self):
|
|
"""
|
|
Returns a dictionary where each key is associated with a
|
|
list of values.
|
|
"""
|
|
unicode_dict = {}
|
|
for key, value in six.iteritems(self.multi.dict_of_lists()):
|
|
value = [self._decode_value(value) for value in value]
|
|
unicode_dict[self._decode_key(key)] = value
|
|
return unicode_dict
|
|
|
|
def __delitem__(self, key):
|
|
key = self._encode_key(key)
|
|
self.multi.__delitem__(key)
|
|
|
|
def __contains__(self, key):
|
|
key = self._encode_key(key)
|
|
return self.multi.__contains__(key)
|
|
|
|
has_key = __contains__
|
|
|
|
def clear(self):
|
|
self.multi.clear()
|
|
|
|
def copy(self):
|
|
return UnicodeMultiDict(self.multi.copy(), self.encoding, self.errors,
|
|
decode_keys=self.decode_keys)
|
|
|
|
def setdefault(self, key, default=None):
|
|
key = self._encode_key(key)
|
|
return self._decode_value(self.multi.setdefault(key, default))
|
|
|
|
def pop(self, key, *args):
|
|
key = self._encode_key(key)
|
|
return self._decode_value(self.multi.pop(key, *args))
|
|
|
|
def popitem(self):
|
|
k, v = self.multi.popitem()
|
|
return (self._decode_key(k), self._decode_value(v))
|
|
|
|
def __repr__(self):
|
|
items = ', '.join(['(%r, %r)' % v for v in self.items()])
|
|
return '%s([%s])' % (self.__class__.__name__, items)
|
|
|
|
def __len__(self):
|
|
return self.multi.__len__()
|
|
|
|
##
|
|
## All the iteration:
|
|
##
|
|
|
|
def keys(self):
|
|
return [self._decode_key(k) for k in self.multi.iterkeys()]
|
|
|
|
def iterkeys(self):
|
|
for k in self.multi.iterkeys():
|
|
yield self._decode_key(k)
|
|
|
|
__iter__ = iterkeys
|
|
|
|
def items(self):
|
|
return [(self._decode_key(k), self._decode_value(v)) for \
|
|
k, v in six.iteritems(self.multi)]
|
|
|
|
def iteritems(self):
|
|
for k, v in six.iteritems(self.multi):
|
|
yield (self._decode_key(k), self._decode_value(v))
|
|
|
|
def values(self):
|
|
return [self._decode_value(v) for v in self.multi.itervalues()]
|
|
|
|
def itervalues(self):
|
|
for v in self.multi.itervalues():
|
|
yield self._decode_value(v)
|
|
|
|
__test__ = {
|
|
'general': """
|
|
>>> d = MultiDict(a=1, b=2)
|
|
>>> d['a']
|
|
1
|
|
>>> d.getall('c')
|
|
[]
|
|
>>> d.add('a', 2)
|
|
>>> d['a']
|
|
1
|
|
>>> d.getall('a')
|
|
[1, 2]
|
|
>>> d['b'] = 4
|
|
>>> d.getall('b')
|
|
[4]
|
|
>>> d.keys()
|
|
['a', 'a', 'b']
|
|
>>> d.items()
|
|
[('a', 1), ('a', 2), ('b', 4)]
|
|
>>> d.mixed()
|
|
{'a': [1, 2], 'b': 4}
|
|
>>> MultiDict([('a', 'b')], c=2)
|
|
MultiDict([('a', 'b'), ('c', 2)])
|
|
"""}
|
|
|
|
if __name__ == '__main__':
|
|
import doctest
|
|
doctest.testmod()
|