238 lines
7.7 KiB
Python
238 lines
7.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
bison.utils
|
|
~~~~~~~~~~~
|
|
|
|
Utilities for `bison`.
|
|
"""
|
|
|
|
import collections
|
|
|
|
|
|
def build_dot_value(key, value):
|
|
"""Build new dictionaries based off of the dot notation key.
|
|
|
|
For example, if a key were 'x.y.z' and the value was 'foo',
|
|
we would expect a return value of: ('x', {'y': {'z': 'foo'}})
|
|
|
|
Args:
|
|
key (str): The key to build a dictionary off of.
|
|
value: The value associated with the dot notation key.
|
|
|
|
Returns:
|
|
tuple: A 2-tuple where the first element is the key of
|
|
the outermost scope (e.g. left-most in the dot
|
|
notation key) and the value is the constructed value
|
|
for that key (e.g. a dictionary)
|
|
"""
|
|
# if there is no nesting in the key (as specified by the
|
|
# presence of dot notation), then the key/value pair here
|
|
# are the final key value pair.
|
|
if key.count('.') == 0:
|
|
return key, value
|
|
|
|
# otherwise, we will need to construct as many dictionaries
|
|
# as there are dot components to hold the value.
|
|
final_value = value
|
|
reverse_split = key.split('.')[::-1]
|
|
end = len(reverse_split) - 1
|
|
for idx, k in enumerate(reverse_split):
|
|
if idx == end:
|
|
return k, final_value
|
|
final_value = {k: final_value}
|
|
|
|
|
|
class DotDict(dict):
|
|
"""A dictionary which supports getting and setting with dot notation keys."""
|
|
|
|
def __init__(self, dct=None):
|
|
if dct is None:
|
|
dct = {}
|
|
super(DotDict, self).__init__(dct)
|
|
|
|
def __getitem__(self, item):
|
|
# x.__getitem__(y) <==> x[y], so this makes x[y] and x.get(y) go
|
|
# through the same code path (e.g. __getattribute__)
|
|
return self.get(item, None)
|
|
|
|
def __setitem__(self, key, value):
|
|
# if there are no dots in the key, its a normal set
|
|
if key.count('.') == 0:
|
|
super(DotDict, self).__setitem__(key, value)
|
|
return
|
|
|
|
# otherwise, traverse the key components to set the value
|
|
first, remainder = key.split('.', 1)
|
|
if first in self:
|
|
elem = super(DotDict, self).__getitem__(first)
|
|
if isinstance(elem, dict):
|
|
dd = DotDict(elem)
|
|
dd.__setitem__(remainder, value)
|
|
elem.update(dd)
|
|
else:
|
|
k, v = build_dot_value(key, value)
|
|
super(DotDict, self).__setitem__(k, v)
|
|
|
|
else:
|
|
k, v = build_dot_value(key, value)
|
|
super(DotDict, self).__setitem__(k, v)
|
|
|
|
def __contains__(self, item):
|
|
# if there are no dots in the key, it is a normal contains
|
|
if item.count('.') == 0:
|
|
return super(DotDict, self).__contains__(item)
|
|
|
|
# otherwise, traverse the key components to check contains
|
|
first, remainder = item.split('.', 1)
|
|
if first in self:
|
|
v = super(DotDict, self).__getitem__(first)
|
|
if isinstance(v, (dict, DotDict)):
|
|
return DotDict(v).__contains__(remainder)
|
|
return False
|
|
return False
|
|
|
|
# ---------------------------------------
|
|
# Public Facing Methods
|
|
# ---------------------------------------
|
|
|
|
def get(self, key, default=None):
|
|
"""Get a value from the `DotDict`.
|
|
|
|
The `key` parameter can either be a regular string key,
|
|
e.g. "foo", or it can be a string key with dot notation,
|
|
e.g. "foo.bar.baz", to signify a nested lookup.
|
|
|
|
The default value is returned if any level of the key's
|
|
components are not found.
|
|
|
|
Args:
|
|
key (str): The key to get the value for.
|
|
default: The return value should the given key
|
|
not exist in the `DotDict`.
|
|
"""
|
|
# if there are no dots in the key, its a normal get
|
|
if key.count('.') == 0:
|
|
return super(DotDict, self).get(key, default)
|
|
|
|
# set the return value to the default
|
|
value = default
|
|
|
|
# split the key into the first component and the rest of
|
|
# the components. the first component corresponds to this
|
|
# DotDict. the remainder components correspond to any nested
|
|
# DotDicts.
|
|
first, remainder = key.split('.', 1)
|
|
if first in self:
|
|
value = super(DotDict, self).get(first, default)
|
|
|
|
# if the value for the key at this level is a dictionary,
|
|
# then pass the remainder to that DotDict.
|
|
if isinstance(value, (dict, DotDict)):
|
|
return DotDict(value).get(remainder, default)
|
|
|
|
# TODO: support lists
|
|
|
|
return value
|
|
|
|
def delete(self, key):
|
|
"""Remove a value from the `DotDict`.
|
|
|
|
The `key` parameter can either be a regular string key,
|
|
e.g. "foo", or it can be a string key with dot notation,
|
|
e.g. "foo.bar.baz", to signify a nested element.
|
|
|
|
If the key does not exist in the `DotDict`, it will continue
|
|
silently.
|
|
|
|
Args:
|
|
key (str): The key to remove.
|
|
"""
|
|
dct = self
|
|
keys = key.split('.')
|
|
last_key = keys[-1]
|
|
for k in keys:
|
|
# if the key is the last one, e.g. 'z' in 'x.y.z', try
|
|
# to delete it from its dict.
|
|
if k == last_key:
|
|
del dct[k]
|
|
break
|
|
|
|
# if the dct is a DotDict, get the value for the key `k` from it.
|
|
if isinstance(dct, DotDict):
|
|
dct = super(DotDict, dct).__getitem__(k)
|
|
|
|
# otherwise, just get the value from the default __getitem__
|
|
# implementation.
|
|
else:
|
|
dct = dct.__getitem__(k)
|
|
if not isinstance(dct, (DotDict, dict)):
|
|
raise KeyError(
|
|
'Subkey "{}" in "{}" invalid for deletion'.format(k, key)
|
|
)
|
|
|
|
def merge(self, source):
|
|
"""Merge the dictionary with the values from another dictionary
|
|
(or other DotDict).
|
|
|
|
This is preferable to using `update` in some cases. Merging will recursively
|
|
update a dictionary, while updating will just overwrite. As an example, if
|
|
we have the DotDict with the values
|
|
|
|
>>> d = DotDict({
|
|
... 'foo': {
|
|
... 'bar': True
|
|
... }
|
|
... })
|
|
>>> d
|
|
{'foo': {'bar': True}}
|
|
|
|
Then for update, we would get:
|
|
|
|
>>> d.update({'foo': {'baz': False}})
|
|
>>> d
|
|
{'foo': {'baz': False}}
|
|
|
|
For a merge, we would get:
|
|
|
|
>>> d.merge({'foo': {'baz': False}})
|
|
>>> d
|
|
{'foo': {'bar': True, 'baz': False}}
|
|
|
|
So, an `update` will replace the specified dictionary, but a `merge` will
|
|
combine the values.
|
|
|
|
Args:
|
|
source: The dict/iterable which will be used to update the DotDict.
|
|
"""
|
|
_merge(self, source)
|
|
|
|
|
|
def _merge(d, u):
|
|
"""Merge two dictionaries (or DotDicts) together.
|
|
|
|
Args:
|
|
d: The dictionary/DotDict to merge into.
|
|
u: The source of the data to merge.
|
|
"""
|
|
for k, v in u.items():
|
|
# if we have a mapping, recursively merge the values
|
|
if isinstance(v, collections.Mapping):
|
|
d[k] = _merge(d.get(k, {}), v)
|
|
|
|
# if d (the dict to merge into) is a dict, just add the
|
|
# value to the dict.
|
|
elif isinstance(d, collections.MutableMapping):
|
|
d[k] = v
|
|
|
|
# otherwise if d (the dict to merge into) is not a dict (e.g. when
|
|
# recursing into it, `d.get(k, {})` may not be a dict), then do what
|
|
# `update` does and prefer the new value.
|
|
#
|
|
# this means that something like `{'foo': 1}` when updated with
|
|
# `{'foo': {'bar': 1}}` would have the original value (`1`) overwritten
|
|
# and would become: `{'foo': {'bar': 1}}`
|
|
else:
|
|
d = {k: v}
|
|
|
|
return d
|