Source code for flatdict

"""FlatDict is a dict object that allows for single level, delimited
key/value pair mapping of nested dictionaries.

"""
import collections

__version__ = '3.2.0'

NO_DEFAULT = object()

# Python 2/3 string compat
try:
    basestring
except NameError:
    basestring = str


[docs]class FlatDict(collections.MutableMapping): """:class:`~flatdict.FlatDict` is a dictionary object that allows for single level, delimited key/value pair mapping of nested dictionaries. The default delimiter value is ``:`` but can be changed in the constructor or by calling :meth:`FlatDict.set_delimiter`. """ _COERCE = dict def __init__(self, value=None, delimiter=':'): super(FlatDict, self).__init__() self._values = {} self._delimiter = delimiter self.update(value) def __contains__(self, key): """Check to see if the key exists, checking for both delimited and not delimited key values. :param mixed key: The key to check for """ if self._has_delimiter(key): pk, ck = key.split(self._delimiter, 1) return pk in self._values and ck in self._values[pk] return key in self._values def __delitem__(self, key): """Delete the item for the specified key, automatically dealing with nested children. :param mixed key: The key to use :raises: KeyError """ if key not in self: raise KeyError if self._has_delimiter(key): pk, ck = key.split(self._delimiter, 1) del self._values[pk][ck] if not self._values[pk]: del self._values[pk] else: del self._values[key] def __eq__(self, other): """Check for equality against the other value :param other: The value to compare :type other: FlatDict :rtype: bool :raises: TypeError """ if isinstance(other, dict): return sorted(self.as_dict()) == sorted(other) elif not isinstance(other, self.__class__): raise TypeError return sorted(self.as_dict()) == sorted(other.as_dict()) def __ne__(self, other): """Check for inequality against the other value :param other: The value to compare :type other: dict or FlatDict :rtype: bool """ return not self.__eq__(other) def __getitem__(self, key): """Get an item for the specified key, automatically dealing with nested children. :param mixed key: The key to use :rtype: mixed :raises: KeyError """ values = self._values for part in key.split(self._delimiter): values = values[part] return values def __iter__(self): """Iterate over the flat dictionary key and values :rtype: Iterator :raises: RuntimeError """ return iter(self.keys()) def __len__(self): """Return the number of items. :rtype: int """ return len(self.keys()) def __reduce__(self): """Return state information for pickling :rtype: tuple """ return type(self), (self.as_dict(), self._delimiter) def __repr__(self): """Return the string representation of the instance. :rtype: str """ return '<{} id={} {}>"'.format(self.__class__.__name__, id(self), str(self)) def __setitem__(self, key, value): """Assign the value to the key, dynamically building nested FlatDict items where appropriate. :param mixed key: The key for the item :param mixed value: The value for the item :raises: TypeError """ if isinstance(value, self._COERCE) and not isinstance(value, FlatDict): value = self.__class__(value, self._delimiter) if self._has_delimiter(key): pk, ck = key.split(self._delimiter, 1) if pk not in self._values: self._values[pk] = self.__class__({ck: value}, self._delimiter) return elif not isinstance(self._values[pk], FlatDict): raise TypeError( 'Assignment to invalid type for key {}'.format(pk)) self._values[pk][ck] = value else: self._values[key] = value def __str__(self): """Return the string value of the instance. :rtype: str """ return '{{{}}}'.format(', '.join( ['{!r}: {!r}'.format(k, self[k]) for k in self.keys()]))
[docs] def as_dict(self): """Return the :class:`~flatdict.FlatDict` as a :class:`dict` :rtype: dict """ out = dict({}) for key in self.keys(): if self._has_delimiter(key): pk, ck = key.split(self._delimiter, 1) if self._has_delimiter(ck): ck = ck.split(self._delimiter, 1)[0] if isinstance(self._values[pk], FlatDict) and pk not in out: out[pk] = {} if isinstance(self._values[pk][ck], FlatDict): out[pk][ck] = self._values[pk][ck].as_dict() else: out[pk][ck] = self._values[pk][ck] else: out[key] = self._values[key] return out
[docs] def clear(self): """Remove all items from the flat dictionary.""" self._values.clear()
[docs] def copy(self): """Return a shallow copy of the flat dictionary. :rtype: flatdict.FlatDict """ return self.__class__(self.as_dict(), delimiter=self._delimiter)
[docs] def get(self, key, d=None): """Return the value for key if key is in the flat dictionary, else default. If default is not given, it defaults to ``None``, so that this method never raises :exc:`KeyError`. :param mixed key: The key to get :param mixed d: The default value :rtype: mixed """ try: return self.__getitem__(key) except KeyError: return d
[docs] def items(self): """Return a copy of the flat dictionary's list of ``(key, value)`` pairs. .. note:: CPython implementation detail: Keys and values are listed in an arbitrary order which is non-random, varies across Python implementations, and depends on the flat dictionary's history of insertions and deletions. :rtype: list """ return [(k, self.__getitem__(k)) for k in self.keys()]
[docs] def iteritems(self): """Return an iterator over the flat dictionary's (key, value) pairs. See the note for :meth:`flatdict.FlatDict.items`. Using ``iteritems()`` while adding or deleting entries in the flat dictionary may raise :exc:`RuntimeError` or fail to iterate over all entries. :rtype: Iterator :raises: RuntimeError """ for item in self.items(): yield item
[docs] def iterkeys(self): """Iterate over the flat dictionary's keys. See the note for :meth:`flatdict.FlatDict.items`. Using ``iterkeys()`` while adding or deleting entries in the flat dictionary may raise :exc:`RuntimeError` or fail to iterate over all entries. :rtype: Iterator :raises: RuntimeError """ for key in self.keys(): yield key
[docs] def itervalues(self): """Return an iterator over the flat dictionary's values. See the note :meth:`flatdict.FlatDict.items`. Using ``itervalues()`` while adding or deleting entries in the flat dictionary may raise a :exc:`RuntimeError` or fail to iterate over all entries. :rtype: Iterator :raises: RuntimeError """ for value in self.values(): yield value
[docs] def keys(self): """Return a copy of the flat dictionary's list of keys. See the note for :meth:`flatdict.FlatDict.items`. :rtype: list """ keys = [] for key, value in self._values.items(): if isinstance(value, (FlatDict, dict)): nested = [self._delimiter.join([key, k]) for k in value.keys()] keys += nested if nested else [key] else: keys.append(key) return sorted(keys)
[docs] def pop(self, key, default=NO_DEFAULT): """If key is in the flat dictionary, remove it and return its value, else return default. If default is not given and key is not in the dictionary, :exc:`KeyError` is raised. :param mixed key: The key name :param mixed default: The default value :rtype: mixed """ if key not in self and default != NO_DEFAULT: return default value = self[key] self.__delitem__(key) return value
[docs] def setdefault(self, key, default): """If key is in the flat dictionary, return its value. If not, insert key with a value of default and return default. default defaults to ``None``. :param mixed key: The key name :param mixed default: The default value :rtype: mixed """ if key not in self or not self.__getitem__(key): self.__setitem__(key, default) return self.__getitem__(key)
[docs] def set_delimiter(self, delimiter): """Override the default or passed in delimiter with a new value. If the requested delimiter already exists in a key, a :exc:`ValueError` will be raised. :param str delimiter: The delimiter to use :raises: ValueError """ for key in self.keys(): if delimiter in key: raise ValueError('Key {!r} collides with delimiter {!r}', key, delimiter) self._delimiter = delimiter for key in self._values.keys(): if isinstance(self._values[key], FlatDict): self._values[key].set_delimiter(delimiter)
[docs] def update(self, other=None, **kwargs): """Update the flat dictionary with the key/value pairs from other, overwriting existing keys. ``update()`` accepts either another flat dictionary object or an iterable of key/value pairs (as tuples or other iterables of length two). If keyword arguments are specified, the flat dictionary is then updated with those key/value pairs: ``d.update(red=1, blue=2)``. :param iterable other: Iterable of key, value pairs :rtype: None """ [self.__setitem__(k, v) for k, v in dict(other or kwargs).items()]
[docs] def values(self): """Return a copy of the flat dictionary's list of values. See the note for :meth:`flatdict.FlatDict.items`. :rtype: list """ return [self.__getitem__(k) for k in self.keys()]
def _has_delimiter(self, key): """Checks to see if the key contains the delimiter. :rtype: bool """ return isinstance(key, basestring) and self._delimiter in key
[docs]class FlatterDict(FlatDict): """Like :class:`~flatdict.FlatDict` but also coerces lists and sets to child-dict instances with the offset as the key. Alternative to the implementation added in v1.2 of FlatDict. """ _COERCE = list, tuple, set, dict, FlatDict _ARRAYS = list, set, tuple def __init__(self, value=None, delimiter=':'): self.original_type = type(value) if self.original_type in self._ARRAYS: value = {str(i): v for i, v in enumerate(value)} super(FlatterDict, self).__init__(value, delimiter) def __setitem__(self, key, value): """Assign the value to the key, dynamically building nested FlatDict items where appropriate. :param mixed key: The key for the item :param mixed value: The value for the item :raises: TypeError """ if isinstance(value, self._COERCE) and \ not isinstance(value, FlatterDict): value = self.__class__(value, self._delimiter) if self._has_delimiter(key): pk, ck = key.split(self._delimiter, 1) if pk not in self._values: self._values[pk] = self.__class__({ck: value}, self._delimiter) return if getattr(self._values[pk], 'original_type', None) in self._ARRAYS: try: int(ck) except ValueError: raise TypeError( 'Assignment to invalid type for key {}{}{}'.format( pk, self._delimiter, ck)) elif not isinstance(self._values[pk], FlatterDict): raise TypeError( 'Assignment to invalid type for key {}'.format(pk)) self._values[pk][ck] = value else: self._values[key] = value
[docs] def as_dict(self): """Return the :class:`~flatdict.FlatterDict` as a nested :class:`dict`. :rtype: dict """ out = {} for key in self.keys(): if self._has_delimiter(key): pk, ck = key.split(self._delimiter, 1) if self._has_delimiter(ck): ck = ck.split(self._delimiter, 1)[0] if isinstance(self._values[pk], FlatterDict) and pk not in out: if self._values[pk].original_type == tuple: out[pk] = tuple(self._child_as_list(pk)) elif self._values[pk].original_type == list: out[pk] = self._child_as_list(pk) elif self._values[pk].original_type == set: out[pk] = set(self._child_as_list(pk)) elif self._values[pk].original_type == dict: out[pk] = self._values[pk].as_dict() else: out[key] = self._values[key] return out
def _child_as_list(self, pk, ck=None): """Returns a list of values from the child FlatterDict instance with string based integer keys. :param str pk: The parent key :param str ck: The child key, optional :rtype: list """ if ck is None: subset = self._values[pk] else: subset = self._values[pk][ck] # Check if keys has delimiter, which implies deeply nested dict keys = subset.keys() if any(self._has_delimiter(k) for k in keys): out = [] split_keys = [k.split(self._delimiter)[0] for k in keys] for k in sorted(set(split_keys)): if subset[k].original_type == tuple: out.append(tuple(self._child_as_list(pk, k))) elif subset[k].original_type == list: out.append(self._child_as_list(pk, k)) elif subset[k].original_type == set: out.append(set(self._child_as_list(pk, k))) elif subset[k].original_type == dict: out.append(subset[k].as_dict()) return out return [subset[k] for k in sorted(keys, key=lambda x: int(x))]