# -*- coding: utf-8 -*-
__author__ = 'Jon Nappi'
__all__ = ['Dict', 'BiDirectionalMap', 'MultiMap']
[docs]class Dict(dict):
"""Overriden :const:`dict` type with iadd functionality which will allow
you to append two dictionaries together. ie::
>>> d = Dict(a=1, b=2)
>>> d += {'c': 3, 'd': 4}
>>> d
... {'a': 1, 'b': 2, 'c': 3, 'd': 4}
"""
def __iadd__(self, other):
if not isinstance(other, dict):
msg = 'Can not concatenate Dict and {}'.format(type(other))
raise TypeError(msg)
for key, val in other.items():
self[key] = val
return self
[docs]class BiDirectionalMap(Dict):
"""a bidirectional map, or hash bag, is an associative data structure in
which the (key, value) pairs form a one-to-one correspondence. Thus the
binary relation is functional in each direction: value can also act as a
key to key. A pair (a, b) thus provides a unique coupling between a and b
so that b can be found when a is used as a key and a can be found when b is
used as a key.
"""
def __init__(self, iterable=None, **kwargs):
"""Create a new instance of a :class:`bidirectionaldict`
:param iterable: An iterable of 2-tuples
:param kwargs: Explitily specified key value pairs for this
:class:`bidirectionaldict`
"""
super(BiDirectionalMap, self).__init__()
self.__keys = []
self.__vals = []
if iterable:
self.__keys = [k for (k, v) in iterable]
self.__vals = [v for (k, v) in iterable]
for key, val in kwargs.items():
self.__keys.append(key)
self.__vals.append(val)
def __contains__(self, item):
"""Contains method determines if *item* is in this
:class:`bidirectionaldict`'s keys or values
:param item: An arbitrary key or value to search for
:return: :const:`True` if *item* is in this :class:`bidirectionaldict`,
:const:`False` otherwise
"""
return item in self.__keys or item in self.__vals
[docs] def get(self, k, d=None):
"""Return self[k] if k is in this :class:`bidirectionaldict`, otherwise
return *d*
:param k: A key to return from this :class:`bidirectionaldict`
:param d: The default value to return if *k* is not in this
:class:`bidirectionaldict`
:return: The value mapped to by *k* or *d* if *k* is not in this
:class:`bidirectionaldict`
"""
try:
return self[k]
except KeyError:
return d
def __setitem__(self, key, value):
"""Set self[key] to value."""
try:
index = self.__keys.index(key)
self.__vals[index] = value
except ValueError:
try:
index = self.__vals.index(key)
self.__keys[index] = value
except ValueError:
self.__keys.append(key)
self.__vals.append(value)
def __getitem__(self, y):
"""x.__getitem__(y) <==> x[y]"""
try:
index = self.__keys.index(y)
return self.__vals[index]
except ValueError:
try:
index = self.__vals.index(y)
return self.__keys[index]
except ValueError:
raise KeyError(y)
def __str__(self):
"""Overriden str representation that iterates through all keys and
values contained in this :class:`bidirectionaldict`
"""
if len(self.__keys) == 0:
return '{}'
output = '{'
fmt = '{}: {}, '
for key, val in zip(self.__keys, self.__vals):
output += fmt.format(repr(key), repr(val))
return output[:-2] + '}'
__repr__ = __str__
def __len__(self):
"""__keys and __values will always be the same length, so return the
length of __keys to be somewhat consistent with the default
:const:`dict`'s __len__ method
"""
return len(self.__keys)
[docs] def keys(self):
"""Return a generator of the keys in this :class:`BiDirectionalDict`"""
return (key for key in self.__keys)
[docs] def values(self):
"""Return a generator of the values in this :class:`BiDirectionalDict`
"""
return (value for value in self.__vals)
[docs] def items(self):
"""Return a 2-tuple of the (key, value) pairs in this
:class:`BiDirectionalDict`
"""
return ((key, value) for (key, value) in zip(self.__keys, self.__vals))
[docs]class MultiMap(Dict):
"""A :class:`MultiMap` is a generalization of a :const:`dict` type in which
more than one value may be associated with and returned for a given key
"""
[docs] def update(self, other=None, **kwargs):
"""Update this :class:`MultiMap` with either the
:param other: Another :const:`dict` to merge into this
:class:`MultiMap` or an iterable of (key, value) 2-tuples
:param kwargs: Arbitrary keyword args to merge into this
:class:`MultiMap`
"""
if other is not None and hasattr(other, 'keys'):
for key in other:
self[key] = other[key]
elif other is not None and hasattr(other, '__iter__'):
for key, val in other:
self[key] = val
for key, val in kwargs.items():
self[key] = val
[docs] def setdefault(self, k, d=None):
"""If *k* is not contained in this :class:`MultiMap` then store the
value *d* in it.
:param k: The key to set the value for
:param d: The default value to assign to key *k*
:return: The value stored at key *k*
"""
if k not in self:
self[k] = d
return self[k]
def _append_key(self, key, value):
"""Handle either adding the *key*, *value* pair to the :const:`dict` or
appending *value* to the list stored at *key*
"""
if isinstance(self[key], list):
self[key].append(value)
else:
super(MultiMap, self).__setitem__(key, [self.get(key), value])
def __iadd__(self, other):
"""Overriden __iadd__ functionality that will append values from
*other* if any of the keys match
:param other: Another :const:`dict` type to merge into this
:class:`MultiMap`
"""
if not isinstance(other, dict):
msg = 'Can not concatenate Dict and {}'.format(type(other))
raise TypeError(msg)
for key, val in other.items():
if key in self:
self._append_key(key, val)
else:
self[key] = val
def __setitem__(self, key, value):
"""If *key* is in this :class:`MultiMap` then
:param key: The key to assign *value* to
:param value: The *value* to assign to *key*
"""
if key in self:
self._append_key(key, value)
else:
super(MultiMap, self).__setitem__(key, value)