from UserDict import DictMixin class multidict(DictMixin): """ An ordered dictionary that can have multiple values for each key. Adds the methods getall, getone, 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 and kw: raise TypeError( "multidict can be called with a positional argument *or* " "keyword arguments, not both") if args: if hasattr(args[0], 'iteritems'): items = list(args[0].iteritems()) elif hasattr(args[0], 'items'): items = args[0].items() else: items = list(args[0]) self._items = items elif kw: self._items = kw.items() else: self._items = [] 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 they 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 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 __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(repr(key)) def __contains__(self, key): for k, v in self._items: if k == key: return True return False has_key = __contains__ def clear(self): self._items = [] def setdefault(self, key, default=None): for k, v in self._items: if key == k: return v self._items.append(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 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 'multidict([%s])' % 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 __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)] """} if __name__ == '__main__': import doctest doctest.testmod()