# Copyright 2015 Oliver Cope
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""
An order preserving multidict implementation
"""
from collections.abc import MutableMapping
from itertools import repeat
from typing import Any
from typing import Dict
from typing import Set
[docs]class MultiDict(MutableMapping):
"""
Dictionary-like object that supports multiple values per key. Insertion
order is preserved.
Synopsis:
>>> from fresco.multidict import MultiDict
>>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> d['a']
1
>>> d['b']
3
>>> d.getlist('a')
[1, 2]
>>> d.getlist('b')
[3]
>>> list(d.allitems())
[('a', 1), ('a', 2), ('b', 3)]
"""
setdefault = MutableMapping.setdefault
def __init__(self, *args, **kwargs):
"""
MultiDicts can be constructed in the following ways:
1. From a sequence of ``(key, value)`` pairs:
>>> from fresco.multidict import MultiDict
>>> MultiDict([('a', 1), ('a', 2)]) # doctest: +ELLIPSIS
MultiDict(...)
2. Initialized from another MultiDict:
>>> from fresco.multidict import MultiDict
>>> d = MultiDict([('a', 1), ('a', 2)])
>>> MultiDict(d) # doctest: +ELLIPSIS
MultiDict(...)
3. Initialized from a regular dict:
>>> from fresco.multidict import MultiDict
>>> MultiDict({'a': 1}) # doctest: +ELLIPSIS
MultiDict(...)
4. From keyword arguments:
>>> from fresco.multidict import MultiDict
>>> MultiDict(a=1) # doctest: +ELLIPSIS
MultiDict(...)
"""
if len(args) > 1:
raise TypeError(
"%s expected at most 2 arguments, got %d"
% (self.__class__.__name__, 1 + len(args))
)
self.clear()
self.update(*args, **kwargs)
def __getitem__(self, key):
"""
Return the first item associated with ``key``:
>>> d = MultiDict([('a', 1), ('a', 2)])
>>> d['a']
1
"""
try:
return self._dict[key][0]
except IndexError:
raise KeyError(key)
def __setitem__(self, key, value):
"""
Set the items associated with key to a single item, ``value``.
>>> d = MultiDict()
>>> d['b'] = 3
>>> d
MultiDict([('b', 3)])
"""
_order = [(k, v) for k, v in self._order if k != key] # type: ignore
_order.append((key, value))
self._order = _order
self._dict[key] = [value]
def __delitem__(self, key):
"""
Delete all values associated with ``key``
"""
del self._dict[key]
self._order = [(k, v) for (k, v) in self._order if k != key]
def __iter__(self):
"""
Return an iterator over all keys
"""
return (k for k in self._dict)
[docs] def get(self, key, default=None):
"""
Return the first available value for key ``key``, or ``default`` if no
such value exists:
>>> d = MultiDict([('a', 1), ('a', 2)])
>>> print(d.get('a'))
1
>>> print(d.get('b'))
None
"""
return self._dict.get(key, [default])[0]
[docs] def getlist(self, key):
"""
Return a list of all values for key ``key``.
"""
return self._dict.get(key, [])
[docs] def copy(self):
"""\
Return a shallow copy of the dictionary:
>>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> copy = d.copy()
>>> copy
MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> copy is d
False
"""
return self.__class__(self)
[docs] @classmethod
def fromkeys(cls, seq, value=None):
"""\
Create a new MultiDict with keys from seq and values set to value.
Example:
>>> MultiDict.fromkeys(['a', 'b'])
MultiDict([('a', None), ('b', None)])
Keys can be repeated:
>>> d = MultiDict.fromkeys(['a', 'b', 'a'])
>>> d.getlist('a')
[None, None]
>>> d.getlist('b')
[None]
"""
return cls(zip(seq, repeat(value)))
[docs] def items(self):
"""\
Return a list of ``(key, value)`` tuples. Only the first ``(key,
value)`` is returned where keys have multiple values:
>>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> list(d.items())
[('a', 1), ('b', 3)]
"""
seen: Set[Any] = set()
for k, v in self._order:
if k in seen:
continue
yield k, v
seen.add(k)
[docs] def listitems(self):
"""\
Like ``items``, but returns lists of values:
>>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> list(d.listitems())
[('a', [1, 2]), ('b', [3])]
"""
for k in self.keys():
yield k, self._dict[k]
[docs] def allitems(self):
"""\
Return ``(key, value)`` pairs for each item in the MultiDict.
Items with multiple keys will have multiple key-value pairs returned:
>>> from fresco.multidict import MultiDict
>>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> list(d.allitems())
[('a', 1), ('a', 2), ('b', 3)]
"""
return iter(self._order)
[docs] def keys(self):
"""\
Return dictionary keys. Each key is returned only once, even if
multiple values are present.
>>> from fresco.multidict import MultiDict
>>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> list(d.keys())
['a', 'b']
"""
return (k for k, v in self.items())
[docs] def values(self):
"""\
Return values from the dictionary. Where keys have multiple values,
only the first is returned:
>>> from fresco.multidict import MultiDict
>>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> list(d.values())
[1, 3]
"""
return (v for k, v in self.items())
[docs] def listvalues(self):
"""\
Return an iterator over lists of values. Each item will be the list of
values associated with a single key.
Example usage:
>>> from fresco.multidict import MultiDict
>>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> list(d.listvalues())
[[1, 2], [3]]
"""
return (self._dict[k] for k in self.keys())
[docs] def pop(self, key, *args):
"""
Dictionary ``pop`` method. Return and remove the value associated with
``key``. If more than one value is associated with ``key``, only the
first is returned.
Example usage:
>>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> d.pop('a')
1
>>> d
MultiDict([('a', 2), ('b', 3)])
>>> d.pop('a')
2
>>> d
MultiDict([('b', 3)])
>>> d.pop('a')
Traceback (most recent call last):
...
KeyError: 'a'
"""
if len(args) > 1:
raise TypeError(
"pop expected at most 2 arguments, got %d" % (1 + len(args))
)
try:
value = self._dict[key].pop(0)
except (KeyError, IndexError):
if args:
return args[0]
raise KeyError(key)
self._order.remove((key, value))
return value
[docs] def popitem(self):
"""
Return and remove a ``(key, value)`` pair from the dictionary.
The item popped is always the most recently added key and the first
value associated with it:
>>> d = MultiDict([('a', 1), ('a', 2), ('b', 3)])
>>> d.popitem()
('b', 3)
>>> d.popitem()
('a', 1)
>>> d.popitem()
('a', 2)
>>> d.popitem() #doctest: +ELLIPSIS
Traceback (most recent call last):
...
KeyError: 'popitem(): dictionary is empty'
"""
try:
key = self._order[-1][0]
except IndexError:
raise KeyError("popitem(): dictionary is empty")
return key, self.pop(key)
[docs] def update(self, *args, **kwargs):
r"""
Update the MultiDict from another MultiDict, regular dictionary or a
iterable of ``(key, value)`` pairs. New keys overwrite old keys - use
:meth:`extend` if you want new keys to be added to old keys.
Updating from another MultiDict:
>>> d = MultiDict([('name', 'eric'), ('job', 'lumberjack')])
>>> d.update(MultiDict([('mood', 'okay')]))
>>> d
MultiDict([('name', 'eric'), ('job', 'lumberjack'), ('mood', 'okay')])
from a dictionary:
>>> d = MultiDict([('name', 'eric'), ('hobby', 'shopping')])
>>> d.update({'hobby': 'pressing wild flowers'})
>>> d
MultiDict([('name', 'eric'), ('hobby', 'pressing wild flowers')])
an iterable of ``(key, value)`` pairs:
>>> d = MultiDict([('name', 'eric'), ('occupation', 'lumberjack')])
>>> d.update([('hobby', 'shopping'),
... ('hobby', 'pressing wild flowers')])
>>> d
MultiDict([('name', 'eric'), ...])
or keyword arguments:
>>> d = MultiDict([('name', 'eric'),\
... ('occupation', 'lumberjack')])
>>> d.update(mood='okay')
"""
if len(args) > 1:
raise TypeError("expected at most 1 argument, got %d" % (1 + len(args),))
if args:
other = args[0]
else:
other = []
return self._update(other, True, **kwargs)
[docs] def extend(self, *args, **kwargs):
"""
Extend the MultiDict with another MultiDict, regular dictionary or a
iterable of ``(key, value)`` pairs. This is similar to :meth:`update`
except that new keys are added to old keys.
"""
if len(args) > 1:
raise TypeError("expected at most 1 argument, got %d", (1 + len(args),))
if args:
other = args[0]
else:
other = []
return self._update(other, False, **kwargs)
def _update(self, *args, **kwargs):
"""
Update the MultiDict from another object and optionally kwargs.
:param other: the other MultiDict, dict, or iterable (first positional
arg)
:param replace: if ``True``, entries will replace rather than extend
existing entries (second positional arg)
"""
other, replace = args
if isinstance(other, self.__class__):
items = list(other.allitems())
elif isinstance(other, dict):
items = list(other.items())
else:
items = list(other)
if kwargs:
items += list(kwargs.items())
if replace:
replaced = {k for k, v in items if k in self._dict}
self._order = [(k, v) for (k, v) in self._order if k not in replaced]
for key in replaced:
self._dict[key] = []
for k, v in items:
self._dict.setdefault(k, []).append(v)
self._order.append((k, v))
def __repr__(self):
"""
``__repr__`` representation of object
"""
return "%s(%r)" % (self.__class__.__name__, list(self.allitems()))
def __str__(self):
"""
``__str__`` representation of object
"""
return repr(self)
def __len__(self):
"""
Return the total number of keys stored.
"""
return len(self._dict)
def __eq__(self, other):
return isinstance(other, MultiDict) and self._order == other._order
def __ne__(self, other):
return not (self == other)
[docs] def clear(self):
self._order = []
self._dict: Dict[Any, Any] = {}