import collections as col
import six
import types
import warnings
from .nesteddict import NestedDict
from .exceptions import LevelError
from .shared import get_filter_func
from .utils import (
GetSetFunctionClass, GetSetAmbiguousTupleFunctionClass,
list_add, list_index, list_is_unique,
tuple_constructor,
dict_to_string, get_str_func,
replace_none, identity,
)
from .compat import iter_to_list
FLATTENED_LEVEL_NAME_SEPARATOR = "___"
[docs]class StructuredNestedDict(col.OrderedDict):
def __init__(self, *args, **kwargs):
"""Extension of OrderedDict that exposes advanced operations on nested
dicts of fixed depth
Parameters
----------
data: dict, or list
Nested dictionary
levels: int
Number of levels
level_names: list
List of level names
"""
# Extract kwargs
level_names = kwargs.pop("level_names", None)
if level_names is None:
levels = kwargs.pop("levels", 1)
else:
levels = kwargs.pop("levels", len(level_names))
self._nested_initialized = False
# Custom initialization
self._levels = levels
if level_names is None:
self._level_names_is_set = False
self._level_names = None # Lazily initialize
else:
assert len(level_names) == self._levels
assert list_is_unique(level_names)
self._level_names_is_set = True
self._level_names = level_names
# Initialize superclass
super(self.__class__, self).__init__(*args, **kwargs)
[docs] @classmethod
def groupby(cls, data, by, levels=None, level_names=None):
"""Initialize by grouping elements from list
Parameters
----------
data: list or dictionary
List or dictionary to group by
by: function
Function applied to list elements to form key elements
levels: int
Number of levels
level_names: list
List of level names
Returns
-------
StructuredNestedDict
"""
new_dict = col.OrderedDict()
if isinstance(data, dict):
for key, val in six.iteritems(data):
new_dict[by(key)] = val
else:
for elem in data:
new_dict[by(elem)] = elem
new_sndict = StructuredNestedDict(new_dict)
if levels is None and level_names is None:
return new_sndict
elif levels is not None and level_names is None:
return new_sndict.stratify(levels=levels-1)
elif levels is None and level_names is not None:
levels = len(level_names)
return new_sndict.stratify(levels=levels-1) \
.replace_metadata(level_names=level_names)
else:
raise RuntimeError("Don't supply both levels and level_names")
# ==== Properties ==== #
@property
def dim(self):
"""Dimensions of whole StructuredNestedDict, up to defined level
Returns
-------
tuple:
Tuple of widths of nested dictionaries, one per level
"""
if self.levels == 1:
return len(self),
dim_list = [[0] * (self._levels - 1)]
for sub_dict in self.values():
dim_list.append(sub_dict.dim)
return (len(self),) + tuple(list_add(dim_list))
@property
def levels(self):
"""Number of levels
Returns
-------
int
"""
return self._levels
@property
def level_names(self):
"""Names of levels. Defaults to ["level0", "level1", ...] is no names
are provided
Returns
-------
list
"""
if self._level_names_is_set:
return tuple(self._level_names)
else:
return ["level{i}".format(i=i) for i in range(self._levels)]
@property
def dim_dict(self):
"""Dimensions of whole StructuredNestedDict as dict, up to defined level
Returns
-------
dict
Dimensions keyed by level name
"""
return dict(zip(self.level_names, self._dim))
# ==== Iterators ==== #
[docs] def iterflatten(self, levels=-1, named=True):
"""Returns an iterator with multiple levels flattened
WARNING:
If there are empty dictionaries within the levels being flattened,
their keys will be lost. This is consistent with the logic of
flattening - those dictionaries contain no values for a
(level-tuple)-keyed dictionary.
Note:
.flatten(levels=0) does nothing,
.flatten(levels=1) compresses 1 level (i.e. keys will be 2-ples)
.flatten(levels=-1) compresses all levels
Parameters
----------
levels: int, default=1
Number of levels to flatten by.
Defaults to flattening one level
named: bool
Whether output key-tuples are namedtuples
Returns
-------
StructuredNestedDict
"""
levels = self._wrap_level(levels)
if levels is None:
levels = self.levels - 1
else:
levels = self._wrap_level(levels)
if named:
key_tup_class = self.get_named_tuple(levels=levels + 1)
else:
key_tup_class = tuple_constructor
for key_ls, val in self._iterflatten(levels=levels):
yield key_tup_class(*key_ls), val
def _iterflatten(self, levels):
"""Underlying iterator for flattening"""
levels = self._wrap_level(levels)
# flatten 0 = nothing, flatten 1 = 1
for key, val in six.iteritems(self):
# TODO: stop levels from being too deep
if levels > 0:
if not isinstance(val, StructuredNestedDict):
raise LevelError()
for partial_key_tup, sub_val in val._iterflatten(levels - 1):
yield (key,) + partial_key_tup, sub_val
else:
yield (key,), val
[docs] def iterflatten_keys(self, levels=-1, named=True):
"""Returns an iterator with of keys of flattened dict
WARNING:
If there are empty dictionaries within the levels being flattened,
their keys will be lost. This is consistent with the logic of
flattening - those dictionaries contain no values for a
(level-tuple)-keyed dictionary.
Note:
.flatten(levels=0) does nothing,
.flatten(levels=1) compresses 1 level (i.e. keys will be 2-ples)
.flatten(levels=-1) compresses all levels
Parameters
----------
levels: int, default=1
Number of levels to flatten by.
Defaults to flattening one level
named: bool
Whether output key-tuples are namedtuples
Returns
-------
list
"""
levels = self._wrap_level(levels)
for key, _ in self.iterflatten(levels=levels, named=named):
yield key
[docs] def iterflatten_values(self, levels=-1):
"""Returns an iterator with of values of flattened dict
WARNING:
If there are empty dictionaries within the levels being flattened,
their keys will be lost. This is consistent with the logic of
flattening - those dictionaries contain no values for a
(level-tuple)-keyed dictionary.
Note:
.flatten(levels=0) does nothing,
.flatten(levels=1) compresses 1 level (i.e. keys will be 2-ples)
.flatten(levels=-1) compresses all levels
Parameters
----------
levels: int, default=-1
Number of levels to flatten by.
Defaults to flattening all levels.
Returns
-------
list
"""
levels = self._wrap_level(levels)
for _, values in self.iterflatten(levels=levels, named=False):
yield values
[docs] def flatten(self, levels=-1, named=True, flattened_name=None):
"""Returns an StructuredNestedDict with multiple levels flattened
WARNING:
If there are empty dictionaries within the levels being flattened,
their keys will be lost. This is consistent with the logic of
flattening - those dictionaries contain no values for a
(level-tuple)-keyed dictionary.
Note:
.flatten(levels=0) does nothing,
.flatten(levels=1) compresses 1 level (i.e. keys will be 2-ples)
.flatten(levels=-1) compresses all levels
Parameters
----------
levels: int, default=1
Number of levels to flatten by.
Defaults to flattening one level
named: bool
Whether output key-tuples are namedtuples
flattened_name: str
Name of new flattened level. Defaults to original level
names joined by "___"
Returns
-------
StructuredNestedDict
"""
levels = self._wrap_level(levels)
if not self._level_names_is_set:
new_level_names = None
else:
if flattened_name is None:
flattened_name = FLATTENED_LEVEL_NAME_SEPARATOR.join(
self.level_names[:levels + 1]
)
new_level_names = (flattened_name,) + self.level_names[levels + 1:]
return self.__class__(
self.iterflatten(levels=levels, named=named),
levels=self.levels - levels,
level_names=new_level_names,
)
[docs] def flatten_keys(self, levels=-1, named=True):
"""Returns an list with of keys of flattened dict
WARNING:
If there are empty dictionaries within the levels being flattened,
their keys will be lost. This is consistent with the logic of
flattening - those dictionaries contain no values for a
(level-tuple)-keyed dictionary.
Note:
.flatten(levels=0) does nothing,
.flatten(levels=1) compresses 1 level (i.e. keys will be 2-ples)
.flatten(levels=-1) compresses all levels
Parameters
----------
levels: int, default=1
Number of levels to flatten by.
Defaults to flattening one level
named: bool
Whether output key-tuples are namedtuples
Returns
-------
list
"""
levels = self._wrap_level(levels)
return list(self.iterflatten_keys(levels=levels, named=named))
[docs] def flatten_values(self, levels=-1):
"""Returns an list with of values of flattened dict
WARNING:
If there are empty dictionaries within the levels being flattened,
their keys will be lost. This is consistent with the logic of
flattening - those dictionaries contain no values for a
(level-tuple)-keyed dictionary.
Note:
.flatten(levels=0) does nothing,
.flatten(levels=1) compresses 1 level (i.e. keys will be 2-ples)
.flatten(levels=-1) compresses all levels
Parameters
----------
levels: int, default=-1
Number of levels to flatten by.
Defaults to flattening all levels.
Returns
-------
list
"""
levels = self._wrap_level(levels)
return list(self.iterflatten_values(levels=levels))
[docs] def unique_keys(self, named=False, sort_keys=True):
"""Returns the unique keys in each level of the dictionary
Parameters
----------
named: bool
If True, return OrderedDict of list of keys of each level. If False,
return a list of list of keys.
sort_keys: bool
Whether to sort each list of keys
Returns
-------
list or dict
"""
unique_keys = col.OrderedDict()
this_level_dicts = [self]
for level, level_name in enumerate(self.level_names):
level_key_set = set()
next_level_dicts = list()
for level_dict in this_level_dicts:
level_key_set.update(level_dict.keys())
if level < self.levels - 1:
next_level_dicts += level_dict.values()
this_level_dicts = next_level_dicts
unique_keys[level_name] = list(level_key_set)
if sort_keys:
unique_keys[level_name].sort()
if named:
return unique_keys
else:
return unique_keys.values()
# ==== Dict Transformation ==== #
[docs] def stratify(self, levels=None, stratified_names=None):
"""Increases depth (nests) of StructuredNestedDict by splitting up
keys in the top-most level
Parameters
----------
levels: int, default=None
Number of levels to stratify by. Defaults to length of first key.
Note: levels must be <= length of all top-level keys
stratified_names: default=None
Names of newly created stratified levels. Must be same length
as levels.
Returns
-------
StructuredNestedDict
"""
try:
levels = replace_none(levels, len(iter_to_list(self.keys())[0]))
except IndexError:
raise LevelError("Cannot infer stratify-levels")
# stratify 0 = nothing, stratify 1 = 1
new_dict = col.OrderedDict()
for key_tup, val in six.iteritems(self):
stratified_keys = key_tup[:levels + 1]
remaining_keys = key_tup[levels + 1:]
dict_pointer = new_dict
for key in stratified_keys[:-1]:
if key not in dict_pointer:
dict_pointer[key] = col.OrderedDict()
dict_pointer = dict_pointer[key]
final_key = stratified_keys[-1]
if len(remaining_keys):
dict_pointer[final_key] = col.OrderedDict()
dict_pointer[final_key][remaining_keys] = val
else:
dict_pointer[final_key] = val
# TODO: Re-org into function
remaining_names = self.level_names[1:]
if stratified_names:
# Need pre/post stratus name
assert len(stratified_names) == levels + 1
level_names = stratified_names + remaining_names
else:
candidate = tuple(self.level_names[0].split(
FLATTENED_LEVEL_NAME_SEPARATOR
))
if len(candidate) == levels + 1:
level_names = candidate + remaining_names
else:
level_names = None
return self.__class__(
new_dict, levels=levels + self.levels,
level_names=level_names,
)
[docs] def convert(self, dict_type=None):
"""Convert all nested dictionaries to desired type.
Parameters
----------
dict_type: ['sndict', 'ndict', 'dict', 'odict']
Dict-type in string format
Returns
-------
dict, OrderedDict or NestedDict
"""
assert dict_type != "sndict", \
"For converting underlying levels to sndict, consider using " \
"sndict.new_with_metadata instead"
return self._resolve_dict_type(dict_type)([
(key, StructuredNestedDict(val).convert(dict_type)
if isinstance(val, dict) else val)
for key, val in self.iteritems()
])
[docs] def rearrange(self, level_ls=None, level_name_ls=None):
"""Rearrange levels of StructuredNestedDict
Only supply either level_ls or level_name_ls.
Note: Whether supplying level_ls or level_name_ls, the supplied list
must cover all levels contiguously from the start. I.e.
level_ls=[2, 0, 1, 3]
is valid but
level_ls=[2, 0]
is not.
Parameters
----------
level_ls: list
List of level ints. If level_ls contains level_names instead, the
arguments is passed on to level_name_ls
level_name_ls: list
List of level names
Returns
-------
StructuredNestedDict
"""
assert (level_ls is None) != (level_name_ls is None), \
"Only either level_ls or level_name_ls can be supplied"
if level_ls is not None:
assert len(level_ls) > 0
if not isinstance(level_ls[0], int):
level_name_ls = level_ls
else:
assert set(level_ls) == set(range(len(level_ls)))
if level_name_ls is not None:
assert list_is_unique(level_name_ls)
if not set(level_name_ls) == \
set(self.level_names[:len(level_name_ls)]):
raise RuntimeError("Redimensioned level_name_ls must be "
"contiguous from level 0")
level_ls = [self.level_names.index(level_name)
for level_name in level_name_ls]
return self._rearrange(level_ls)
def _rearrange(self, level_ls):
"""Underlying method for rearranging levels"""
num_levels = len(level_ls)
new_dict = col.OrderedDict()
for key_tup, val in self.iterflatten(num_levels - 1):
new_dict[tuple(list_index(key_tup, level_ls))] = val
if self._level_names_is_set:
new_level_names = tuple(list_index(self.level_names, level_ls))\
+ self.level_names[num_levels:]
else:
new_level_names = None
return self.__class__(new_dict, levels=self.levels - num_levels + 1)\
.stratify((len(level_ls)) - 1)\
.replace_metadata(level_names=new_level_names, levels=self.levels)
[docs] def swap_levels(self, level_a, level_b):
"""Swap two levels in a StructuredNestedDict
Unlike sndict.rearrange, there's no need to be contiguous
Parameters
----------
level_a: int or str
level or level_name
level_b: int or str
level or level_name
Returns
-------
StructuredNestedDict
"""
if not isinstance(level_a, int):
level_a = self.level_names.index(level_a)
if not isinstance(level_b, int):
level_b = self.level_names.index(level_b)
assert level_a != level_b
assert 0 <= level_a < self.levels
assert 0 <= level_b < self.levels
required_levels = max(level_a, level_b) + 1
new_level_ls = iter_to_list(range(required_levels))
new_level_ls[level_b], new_level_ls[level_a] = \
new_level_ls[level_a], new_level_ls[level_b]
return self._rearrange(new_level_ls)
[docs] def replace_data(self, data):
"""Return new StructuredNestedDict with different data but same metadata
Parameters
----------
data: dict, or list
Nested dictionary
Returns
-------
StructuredNestedDict
"""
return self.__class__(
data, levels=self.levels,
level_names=self._level_names if self._level_names_is_set else None,
)
[docs] def sort_keys(self, cmp=None, key=None, reverse=False):
"""Sort keys of StructuredNestedDict (top-level only)
Parameters
----------
cmp: function, optional
Comparator function
key: function, optional
Key function
reverse: bool, optional
Whether to sort in reverse
Returns
-------
StructuredNestedDict
"""
return self.replace_data([
(key, self[key])
for key in sorted(self.keys(), key=key, reverse=reverse)
])
[docs] def sort_values(self, key=None, reverse=False):
"""Sort values of StructuredNestedDict (top-level only)
Parameters
----------
key: function
Key function
reverse: bool
Whether to sort in reverse
Returns
-------
StructuredNestedDict
"""
if key is not None:
key = lambda a: key(a[1])
else:
key = lambda a: a[1]
return self.replace_data([
(key, val)
for key, val in sorted(self.items(), key=key, reverse=reverse)
])
[docs] def map(self, key_func=None, val_func=None, at_level=-1, warn=False):
"""Apply transformations to keys and values
Parameters
----------
key_func: function, optional
Function to transform keys. Defaults to identity.
val_func: function, optional
Function to transform values. Defaults to identity.
at_level: int
Level to transform keys at
warn: bool
Warn if dimensions of dictionary have been changed
Returns
-------
StructuredNestedDict
"""
at_level = self._wrap_level(at_level)
key_func = replace_none(key_func, identity)
val_func = replace_none(val_func, identity)
new_dict = self.replace_data({})
for key, val in self.iterflatten(levels=at_level, named=False):
new_dict.nested_set(key_func(key), val_func(val))
assert new_dict.dim[-1] == self.dim[-1]
if warn and new_dict.dim != self.dim:
warnings.warn(
"Empty high-level dicts may have been dropped, "
"dimensions changes from {} to {}".format(
new_dict.dim, self.dim,
))
return new_dict
[docs] def map_keys(self, key_func, at_level=-1):
"""Apply transformations to keys and values
Parameters
----------
key_func: function
Function to transform keys
at_level: int
Level to transform keys at
Returns
-------
StructuredNestedDict
"""
return self.map(key_func=key_func, at_level=at_level)
[docs] def map_values(self, val_func, at_level=-1):
"""Apply transformations to keys and values
Parameters
----------
val_func: function
Function to transform values
at_level: int
Level to transform values at
Returns
-------
StructuredNestedDict
"""
return self.map(val_func=val_func, at_level=at_level)
# ==== Getters, Setters and Selectors ==== #
[docs] def filter_key(self, criteria_ls, filter_out=False, drop_empty=False):
"""Filter StructuredNestedDict by criteria.
The criteria used in the following ways, based on type:
1. slice(None): Keep all
2. function: Keep if function(key) is True
3. list, set: Keep if key in list/set
4. other: Keep if key==other
Parameters
----------
criteria_ls: list or dict
Filter based on criteria
filter_out: bool
Whether to filter in or out
drop_empty:
Whether to drop empty nested dictionaries (nested dictionaries
with all elements filtered out)
Returns
-------
StructuredNestedDict
"""
if len(criteria_ls) == 0:
raise KeyError("criteria_ls cannot be empty")
if isinstance(criteria_ls, dict):
new_criteria_dict = col.OrderedDict()
for level_name in self.level_names:
if level_name not in criteria_ls:
new_criteria_dict[level_name] = slice(None)
else:
new_criteria_dict[level_name] = criteria_ls.pop(level_name)
if criteria_ls:
raise RuntimeError("Unused criteria for: {}".format(
criteria_ls.keys()
))
criteria_ls = new_criteria_dict.values()
filter_func_ls = [
get_filter_func(criteria, filter_out=filter_out)
for criteria in criteria_ls
]
return self._filter_key(self, filter_func_ls, drop_empty)
@classmethod
def _filter_key(cls, obj, filter_func_ls, drop_empty):
"""Underlying method for filter_key"""
if not filter_func_ls or not isinstance(obj, StructuredNestedDict):
return obj
filter_func = filter_func_ls[0]
new_dict = col.OrderedDict()
for key, val in six.iteritems(obj):
if not filter_func(key):
continue
new_val = cls._filter_key(val, filter_func_ls[1:], drop_empty)
if drop_empty and isinstance(new_val, StructuredNestedDict) \
and len(new_val) == 0:
continue
new_dict[key] = new_val
return obj.replace_data(new_dict)
[docs] def filter_values(self, criteria, filter_out=False,
level=None, drop_empty=False):
"""Filter StructuredNestedDict values by criteria.
The criteria used in the following ways, based on type:
1. slice(None): Keep all
2. function: Keep if function(key) is True
3. list, set: Keep if key in list/set
4. other: Keep if key==other
Parameters
----------
criteria: See above
Filter based on criteria
filter_out: bool
Whether to filter in or out
drop_empty:
Whether to drop empty nested dictionaries (nested dictionaries
with all elements filtered out)
Returns
-------
StructuredNestedDict
"""
filter_func = get_filter_func(criteria, filter_out=filter_out)
level = replace_none(level, self.levels - 1)
return self._filter_values(self, filter_func, level, drop_empty)
@classmethod
def _filter_values(cls, obj, filter_func, levels_remaining, drop_empty):
"""Underlying method for filter_values"""
if levels_remaining == 0:
return obj.replace_data([
(key, val)
for (key, val) in six.iteritems(obj)
if filter_func(val)
])
else:
new_dict = col.OrderedDict()
for key, val in six.iteritems(obj):
new_val = cls._filter_values(val, filter_func,
levels_remaining - 1, drop_empty)
if drop_empty and len(new_val) == 0:
continue
new_dict[key] = new_val
return obj.replace_data(new_dict)
[docs] def nested_set(self, key_list, value):
"""Set a value within nested dicts, creating StructuredNestedDict at
depth if they don't exist yet
Note: Only allowed to set up to level of StructuredNestedDict
Parameters
----------
key_list: list
List of keys, one for each dict depth
value: object
Value to set nested
"""
self._check_key_list(key_list)
dict_pointer = self
for key in key_list[:-1]:
if key not in dict_pointer:
dict_pointer[key] = self.__class__()
dict_pointer = dict_pointer[key]
dict_pointer[key_list[-1]] = value
[docs] def nested_setdefault(self, key_list, default=None):
"""Nested version of dict.setdefault. Set a value within nested dicts,
creating StructuredNestedDict at depth if they don't exist yet
Note: Only allowed to set up to level of StructuredNestedDict
Parameters
----------
key_list: list
List of keys, one for each dict depth
default: object, optional
Value to set nested
"""
self._check_key_list(key_list)
if self.has_nested_key(key_list):
return self.nested_get(key_list)
else:
self.nested_set(key_list, default)
return default
[docs] def nested_get(self, key_list):
"""Get value at depth
Parameters
----------
key_list: list
List of keys, one for each dict depth
Returns
-------
obj
"""
self._check_key_list(key_list)
pointer = self
for key in key_list:
pointer = pointer[key]
return pointer
[docs] def has_nested_key(self, key_list):
"""Check if nested keys are valid
Parameters
----------
key_list: list
List of keys, one for each dict depth
Returns
-------
bool
"""
self._check_key_list(key_list)
try:
self.nested_get(key_list)
return True
except KeyError:
return False
def _get_multiple(self, key_or_criteria_ls):
"""Check whether list has keys or criteria (i.e. if any of the criteria
lead to special filtering/getting functions"""
if any(map(_is_criteria, key_or_criteria_ls)):
return self.filter_key(criteria_ls=key_or_criteria_ls)\
.flatten_values(len(key_or_criteria_ls) - 1)
else:
return self.nested_get(key_or_criteria_ls)
def _set_multiple(self, key_or_criteria_ls, val):
"""Check whether list has keys or criteria (i.e. if any of the criteria
lead to special setting functions"""
# TODO: Currently implementation is lazy and suboptimal,
# repeatedly tests earlier keys
if any(map(_is_criteria, key_or_criteria_ls)):
selected_keys = self.filter_key(criteria_ls=key_or_criteria_ls)\
.flatten_keys(len(key_or_criteria_ls) - 1)
for key_ls in selected_keys:
print(key_ls)
self.nested_set(key_ls, val)
else:
self.nested_set(key_or_criteria_ls, val)
@property
def ixkeys(self):
"""Indexer that allows for indexing by nested key list e.g.
my_ndict.ix["key1", "key2", "key3"]
or with criteria such as
my_sndict.ix["key1", :, lambda _: "3" in _]
Note that for a single key, a tuple/list needs to be provided, e.g.
my_sndict.ix["key1",]
Returns
-------
Indexable
"""
return GetSetFunctionClass(
get_func=self._get_multiple,
set_func=self._set_multiple,
)
@property
def ix(self):
"""Indexer that allows for indexing by nested key/criteria list e.g.
my_ndict.ix["key1", "key2", "key3"]
or with criteria such as
my_sndict.ix["key1", :, lambda _: "3" in _]
Also supports:
my_sndict.ix["key1"]
If behavior is ambiguous, use sndict.ixkeys[key_list] and sndict[key]
directly instead
Returns
-------
Indexable
"""
return GetSetAmbiguousTupleFunctionClass(
get_func=self._get_multiple,
set_func=self._set_multiple,
)
# ==== Other ==== #
def __repr__(self):
args_string_ls = [
dict_to_string(self),
"levels={levels}".format(levels=self._levels),
]
if self._level_names_is_set:
args_string_ls.append("level_names={level_names}".format(
level_names=self.level_names
))
return "{class_name}({args_string})".format(
class_name=self.__class__.__name__,
args_string=", ".join(args_string_ls),
)
def __setitem__(self, key, value):
if self.levels > 1:
if isinstance(value, StructuredNestedDict) \
and value.levels == self.levels - 1 \
and value.level_names[1:] == self.level_names[1:]:
# If dictionary is already StructuredNestedDict, and it looks
# like we expect it to, skip overhead of initializing anew
pass
elif isinstance(value, dict):
value = StructuredNestedDict(
value, levels=self.levels - 1,
level_names=self.level_names[1:]
if self._level_names_is_set else None,
)
if not isinstance(value, StructuredNestedDict) or \
value.levels != self.levels - 1:
raise TypeError(
"Inserted item needs to be a StructuredNestedDict "
"with level={}".format(self.levels - 1))
super(self.__class__, self).__setitem__(key, value)
[docs] def to_tree_string(self, indent=" - ",
key_mode="str",
val_mode="type"):
"""Returns structure of NestedDict in tree format string
Parameters
----------
indent: str
Indentation string for levels
key_mode: "type", "str" or "repr"
How to serialize key
val_mode: "type", "str" or "repr"
How to serialize terminal value
Returns
-------
str
"""
key_str = get_str_func(key_mode)
val_str = get_str_func(val_mode)
def _dfs_print(dictionary, level, indent_):
string_ = ""
if level > 0:
indent_str = " " * (level - 1) + "'-"
else:
indent_str = ""
for key, val in six.iteritems(dictionary):
if level < self.levels - 1:
string_ += "{}{}:\n".format(
indent_str, key_str(key))
string_ += _dfs_print(val, level + 1, indent_)
else:
string_ += "{}{}: {}\n".format(
indent_str, key_str(key), val_str(val))
return string_
return _dfs_print(self, 0, indent)
[docs] def get_named_tuple(self, levels):
"""Get namedtuple class for named keys
Parameters
----------
levels: Number of levels to construct named keys for
Returns
-------
class
"""
return col.namedtuple("KeyTuple", self.level_names[:levels])
@staticmethod
def _resolve_dict_type(dict_type):
"""Resolve dict type based on string
Parameters
----------
dict_type: ['sndict', 'ndict', 'dict', 'odict']
Dict-type in string format
Returns
-------
class
"""
if dict_type in [dict, col.OrderedDict, StructuredNestedDict]:
dict_class = dict_type
elif dict_type is None or dict_type == "sndict":
dict_class = StructuredNestedDict
elif dict_type == "ndict":
dict_class = NestedDict
elif dict_type == "dict":
dict_class = dict
elif dict_type == "odict":
dict_class = col.OrderedDict
else:
raise KeyError(dict_type)
return dict_class
@staticmethod
def _check_key_list(key_list):
"""Check if key_list is valid"""
if len(key_list) == 0:
raise KeyError("key_list cannot be empty")
def _wrap_level(self, level):
"""Wrap a level argument"""
return _wrap_level(level, allowed_level=self.levels)
def _wrap_level(level, allowed_level):
"""Wrap levels given a maximum allowed level"""
if level < 0:
wrapped_level = allowed_level + level
else:
wrapped_level = level
if wrapped_level < 0 or wrapped_level >= allowed_level:
raise LevelError(
"Level {level} not valid for allowed_levels {allowed_level}".format(
level=level, allowed_level=allowed_level
))
return wrapped_level
def _is_criteria(key_or_criteria):
"""Check if argument is a key or filter-criteria"""
if key_or_criteria == slice(None):
return True
elif isinstance(key_or_criteria, (list, set, types.FunctionType)):
return True
else:
return False
sndict = StructuredNestedDict