"""
Easily work with deeply nested data and write
clean code using FlexDict; a small subclass of
dict. FlexDict provides automatic and arbitrary
levels of nesting along with additional utility
methods.
"""
__version__ = '0.0.1.a1'
[docs]class FlexDict(dict):
"""
Provides automatic and arbitrary levels of
nesting along with additional utility methods.
Args:
data (dict): Data to initialize the FlexDict with.
Attributes:
locked (bool): Flag indicating if auto-nesting is locked.
"""
def __init__(self, data=None):
super(FlexDict, self).__init__()
self.locked = False
if data:
if isinstance(data, dict):
for items in self.__kv(data):
self.__setitem__(*items)
else:
raise ValueError(
'FlexDict can only be initialized with instances of dict!'
)
def __hash__(self):
return id(self)
def __eq__(self, other):
if isinstance(other, dict):
return self.flatten() == FlexDict(other).flatten()
return False
def __getitem__(self, key):
key = self.__sanitize(key)
if isinstance(key, list):
for _, k in enumerate(key, start=1):
self = self[k]
return self
try:
return dict.__getitem__(self, key)
except KeyError:
if not self.locked:
return self.setdefault(key, FlexDict())
raise
def __setitem__(self, key, val):
key = self.__sanitize(key)
if isinstance(key, list):
for i, k in enumerate(key[:-1], start=1):
if not self.locked or k in self:
self = self[k]
else:
if i == len(key) - 1:
raise KeyError(k)
self[key[-1]] = val
else:
dict.__setitem__(
self, key, FlexDict(val) if isinstance(val, dict) else val
)
@staticmethod
def __sanitize(key):
if isinstance(key, (list, set, tuple)):
return list(key)
if isinstance(key, dict):
raise TypeError('unhashable type: \'dict\'')
return key
def __kv(self, data, results=None):
if results is None:
results = []
for key, value in data.items():
if isinstance(value, dict) and value:
for item in self.__kv(value, results + [key]):
yield item
else:
yield results + [key], value
def __k(self, data):
for key, value in data.items():
if isinstance(value, dict):
yield key
for nested_key in self.__k(value):
yield nested_key
else:
yield key
def __v(self, data):
for _, value in data.items():
if isinstance(value, dict) and value:
for nested_value in self.__v(value):
yield nested_value
else:
yield value
def __lock(self, lock, inplace, data=None):
if not data:
data = self if inplace else FlexDict(self)
data.locked = lock
for key, val in data.items():
if isinstance(val, FlexDict):
data[key].locked = lock
self.__lock(lock, inplace, data[key])
return None if inplace else data
def __contains(self, superset, subset, dicts):
if superset == subset:
return True
if dicts:
superset = {
key: value
for sub_dict in dicts
for key, value in sub_dict.items()
}
dicts = []
for sub_key, sub_val in subset.items():
for sup_key, sup_value in superset.items():
if {sup_key: sup_value} == {sub_key: sub_val}:
return True
if isinstance(sup_value, dict):
if sup_value == subset:
return True
dicts.append(sup_value)
if dicts:
return self.__contains(superset, subset, dicts)
return False
[docs] def get(self, keys, default=None):
"""
Gets a value from the dictionary with the provided keys.
Args:
keys: Keys pointing to the target value.
default (any): Default value to return if target does not exists.
Returns:
any: The corresponding dictionary value.
"""
keys = self.__sanitize(keys)
if isinstance(keys, list):
for key in keys:
if key in self:
self = self[key]
else:
return default
return self
if keys in self:
return self[keys]
return default
[docs] def set(self, keys, value, overwrite=True, increment=False):
"""
Sets a dictionary value with the given keys.
Args:
keys (any): Key(s) pointing to the value.
value (any): Value to set.
overwrite (bool): If `False`, only sets a value if it not exists.
increment (bool):
Increments the value by `value` if set to `True`.
`overwrite` argument has no effect on this.
Causes the method to return the target value.
Returns:
Union[int, float, None]:
Final state of the target value if `increment` is enabled.
"""
keys = self.__sanitize(keys)
if not increment:
if overwrite or keys[-1] in self.get(keys[:-1], default=[]):
self[keys] = value
return None
self[keys] = value if (
not self[keys]
) else self[keys] + value
return self[keys]
[docs] def keys(self, nested=False, unique=False):
"""
Gets keys from the dictionary.
Args:
nested (bool): Gets all keys recursively if set to `True`.
unique (bool): Gets only the unique keys if set to `True`.
Returns:
Union[dict_keys, list, set]:
dict_keys
If `nested` is `False` and `unique` is `False`.
list
If `nested` is `True` and `unique` is `False`.
set
If `unique` is `True`.
"""
return dict.keys(self) if not nested else (
list(self.__k(self)) if not unique else set(self.__k(self))
)
[docs] def values(self, nested=False, unique=False):
"""
Gets values from the dictionary.
Args:
nested (bool): Gets all values recursively if set to `True`.
unique (bool): Gets only the unique values if set to `True`.
Returns:
Union[dict_values list, set]:
dict_values
If `nested` is `False` and `unique` is `False`.
list:
If `nested` is `True` and `unique` is `False`.
list:
If `unique` is `True`.
"""
vals = (
list(dict.values(self))
if not nested
else list(self.__v(self))
)
return (
vals
if not unique
else list(set(vals))
if not nested
else set(vals)
)
[docs] def pop(self):
"""
Removes and returns the last key-value pair from the dictionary.
Returns:
Union[FlexDict, None]:
FlexDict
The last key-value pair of the dictionary.
None
If `self` is empty.
"""
if self:
key, val = list(self.items())[-1]
del self[key]
return FlexDict({key: val})
return None
[docs] def length(self, nested=False, unique=False):
"""
Counts the number of keys inside the dictionary.
Args:
nested (bool): Counts all keys recursively if set to `True`.
unique (bool): Counts only the unique keys if set to `True`.
Returns:
int: Number of keys.
"""
return len(self.keys(nested=nested, unique=unique))
[docs] def size(self, unique=False):
"""
Counts the number of keys and values inside the dictionary.
Args:
nested (bool): Counts all items recursively if set to `True`.
unique (bool): Counts only the unique items if set to `True`.
Returns:
int: Number of items.
"""
return len(self.keys(nested=True, unique=unique)) + len(
self.values(nested=True, unique=unique)
)
[docs] def flatten(self):
"""
Flattens the dictionary.
Returns:
list: A list of tuples containing key-paths and values.
"""
return list(self.__kv(self))
[docs] def lock(self, inplace=True):
"""
Locks the automatic nesting mechanism.
Args:
inplace (bool): Creates a locked copy if `True`.
Returns:
Union[None, FlexDict]:
None
If `inplace` is set to `True`.
FlexDict
If `inplace`set to `False`.
"""
return self.__lock(True, inplace)
[docs] def unlock(self, inplace=True):
"""
Unlocks the automatic nesting mechanism.
Args:
inplace (bool): Creates an unlocked copy if `True`.
Returns:
Union[None, FlexDict]:
None
If `inplace` is set to `True`.
FlexDict
If `inplace`set to `False`.
"""
return self.__lock(False, inplace)
[docs] def contains(self, subset):
"""
Checks if this dictionary is a superset of a given one.
Args:
subset (dict): Dictionary to check if it is a subset.
Returns:
bool: `True` if `self` contains `subset` else `False`.
"""
return self.__contains(self, subset, [])
[docs] def inside(self, superset):
"""
Checks if this dictionary is a subset of a given one.
Args:
superset (dict): Dictionary to check if it is a superset.
Returns:
bool: `True` if `self` is inside the `superset` else `False`.
"""
return self.__contains(superset, self, [])