# (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
"""
Gives a multi-value dictionary object (MultiDict) plus several wrappers
"""
from collections import MutableMapping
import binascii
import warnings
from webob.compat import (
PY3,
iteritems_,
itervalues_,
url_encode,
)
__all__ = ['MultiDict', 'NestedMultiDict', 'NoVars', 'GetDict']
class MultiDict(MutableMapping):
"""
An ordered dictionary that can have multiple values for each key.
Adds the methods getall, getone, mixed and extend 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 = list(args[0].iteritems())
elif hasattr(args[0], 'items'):
items = list(args[0].items())
else:
items = list(args[0])
self._items = items
else:
self._items = []
if kw:
self._items.extend(kw.items())
@classmethod
def view_list(cls, lst):
"""
Create a dict that is a view on the given list
"""
if not isinstance(lst, list):
raise TypeError(
"%s.view_list(obj) takes only actual list objects, not %r"
% (cls.__name__, lst))
obj = cls()
obj._items = lst
return obj
@classmethod
def from_fieldstorage(cls, fs):
"""
Create a dict from a cgi.FieldStorage instance
"""
obj = cls()
# fs.list can be None when there's nothing to parse
for field in fs.list or ():
charset = field.type_options.get('charset', 'utf8')
transfer_encoding = field.headers.get('Content-Transfer-Encoding', None)
supported_transfer_encoding = {
'base64' : binascii.a2b_base64,
'quoted-printable' : binascii.a2b_qp
}
if PY3: # pragma: no cover
if charset == 'utf8':
decode = lambda b: b
else:
decode = lambda b: b.encode('utf8').decode(charset)
else:
decode = lambda b: b.decode(charset)
if field.filename:
field.filename = decode(field.filename)
obj.add(field.name, field)
else:
value = field.value
if transfer_encoding in supported_transfer_encoding:
if PY3: # pragma: no cover
# binascii accepts bytes
value = value.encode('utf8')
value = supported_transfer_encoding[transfer_encoding](value)
if PY3: # pragma: no cover
# binascii returns bytes
value = value.decode('utf8')
obj.add(field.name, decode(value))
return obj
def __getitem__(self, key):
for k, v in reversed(self._items):
if k == key:
return v
raise KeyError(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)
"""
return [v for k, v in self._items if k == key]
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.
"""
r = {}
for key, val in self.items():
r.setdefault(key, []).append(val)
return r
def __delitem__(self, key):
items = self._items
found = False
for i in range(len(items)-1, -1, -1):
if items[i][0] == key:
del items[i]
found = True
if not found:
raise KeyError(key)
def __contains__(self, key):
for k, v in self._items:
if k == key:
return True
return False
has_key = __contains__
def clear(self):
del self._items[:]
def copy(self):
return self.__class__(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 %s"
% repr(1 + len(args)))
for i in range(len(self._items)):
if self._items[i][0] == key:
v = self._items[i][1]
del self._items[i]
return v
if args:
return args[0]
else:
raise KeyError(key)
def popitem(self):
return self._items.pop()
def update(self, *args, **kw):
if args:
lst = args[0]
if len(lst) != len(dict(lst)):
# this does not catch the cases where we overwrite existing
# keys, but those would produce too many warning
msg = ("Behavior of MultiDict.update() has changed "
"and overwrites duplicate keys. Consider using .extend()"
)
warnings.warn(msg, UserWarning, stacklevel=2)
MutableMapping.update(self, *args, **kw)
def extend(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 = map('(%r, %r)'.__mod__, _hide_passwd(self.items()))
return '%s([%s])' % (self.__class__.__name__, ', '.join(items))
def __len__(self):
return len(self._items)
##
## All the iteration:
##
def iterkeys(self):
for k, v in self._items:
yield k
if PY3: # pragma: no cover
keys = iterkeys
else:
def keys(self):
return [k for k, v in self._items]
__iter__ = iterkeys
def iteritems(self):
return iter(self._items)
if PY3: # pragma: no cover
items = iteritems
else:
def items(self):
return self._items[:]
def itervalues(self):
for k, v in self._items:
yield v
if PY3: # pragma: no cover
values = itervalues
else:
def values(self):
return [v for k, v in self._items]
_dummy = object()
class GetDict(MultiDict):
# def __init__(self, data, tracker, encoding, errors):
# d = lambda b: b.decode(encoding, errors)
# data = [(d(k), d(v)) for k,v in data]
def __init__(self, data, env):
self.env = env
MultiDict.__init__(self, data)
def on_change(self):
e = lambda t: t.encode('utf8')
data = [(e(k), e(v)) for k,v in self.items()]
qs = url_encode(data)
self.env['QUERY_STRING'] = qs
self.env['webob._parsed_query_vars'] = (self, qs)
def __setitem__(self, key, value):
MultiDict.__setitem__(self, key, value)
self.on_change()
def add(self, key, value):
MultiDict.add(self, key, value)
self.on_change()
def __delitem__(self, key):
MultiDict.__delitem__(self, key)
self.on_change()
def clear(self):
MultiDict.clear(self)
self.on_change()
def setdefault(self, key, default=None):
result = MultiDict.setdefault(self, key, default)
self.on_change()
return result
def pop(self, key, *args):
result = MultiDict.pop(self, key, *args)
self.on_change()
return result
def popitem(self):
result = MultiDict.popitem(self)
self.on_change()
return result
def update(self, *args, **kwargs):
MultiDict.update(self, *args, **kwargs)
self.on_change()
def extend(self, *args, **kwargs):
MultiDict.extend(self, *args, **kwargs)
self.on_change()
def __repr__(self):
items = map('(%r, %r)'.__mod__, _hide_passwd(self.items()))
# TODO: GET -> GetDict
return 'GET([%s])' % (', '.join(items))
def copy(self):
# Copies shouldn't be tracked
return MultiDict(self)
class NestedMultiDict(MultiDict):
"""
Wraps several MultiDict objects, treating it as one large MultiDict
"""
def __init__(self, *dicts):
self.dicts = dicts
def __getitem__(self, key):
for d in self.dicts:
value = d.get(key, _dummy)
if value is not _dummy:
return value
raise KeyError(key)
def _readonly(self, *args, **kw):
raise KeyError("NestedMultiDict objects are read-only")
__setitem__ = _readonly
add = _readonly
__delitem__ = _readonly
clear = _readonly
setdefault = _readonly
pop = _readonly
popitem = _readonly
update = _readonly
def getall(self, key):
result = []
for d in self.dicts:
result.extend(d.getall(key))
return result
# Inherited:
# getone
# mixed
# dict_of_lists
def copy(self):
return MultiDict(self)
def __contains__(self, key):
for d in self.dicts:
if key in d:
return True
return False
has_key = __contains__
def __len__(self):
v = 0
for d in self.dicts:
v += len(d)
return v
def __nonzero__(self):
for d in self.dicts:
if d:
return True
return False
def iteritems(self):
for d in self.dicts:
for item in iteritems_(d):
yield item
if PY3: # pragma: no cover
items = iteritems
else:
def items(self):
return list(self.iteritems())
def itervalues(self):
for d in self.dicts:
for value in itervalues_(d):
yield value
if PY3: # pragma: no cover
values = itervalues
else:
def values(self):
return list(self.itervalues())
def __iter__(self):
for d in self.dicts:
for key in d:
yield key
iterkeys = __iter__
if PY3: # pragma: no cover
keys = iterkeys
else:
def keys(self):
return list(self.iterkeys())
class NoVars(object):
"""
Represents no variables; used when no variables
are applicable.
This is read-only
"""
def __init__(self, reason=None):
self.reason = reason or 'N/A'
def __getitem__(self, key):
raise KeyError("No key %r: %s" % (key, self.reason))
def __setitem__(self, *args, **kw):
raise KeyError("Cannot add variables: %s" % self.reason)
add = __setitem__
setdefault = __setitem__
update = __setitem__
def __delitem__(self, *args, **kw):
raise KeyError("No keys to delete: %s" % self.reason)
clear = __delitem__
pop = __delitem__
popitem = __delitem__
def get(self, key, default=None):
return default
def getall(self, key):
return []
def getone(self, key):
return self[key]
def mixed(self):
return {}
dict_of_lists = mixed
def __contains__(self, key):
return False
has_key = __contains__
def copy(self):
return self
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__,
self.reason)
def __len__(self):
return 0
def __cmp__(self, other):
return cmp({}, other)
def iterkeys(self):
return iter([])
if PY3: # pragma: no cover
keys = iterkeys
items = iterkeys
values = iterkeys
else:
def keys(self):
return []
items = keys
values = keys
itervalues = iterkeys
iteritems = iterkeys
__iter__ = iterkeys
def _hide_passwd(items):
for k, v in items:
if ('password' in k
or 'passwd' in k
or 'pwd' in k
):
yield k, '******'
else:
yield k, v