Created
September 22, 2016 17:08
-
-
Save zzl0/9565c0862cd43ae4e25d0d190239a40a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from inspect import isgenerator | |
import re | |
import copy | |
class Dict(dict): | |
""" | |
Dict is a subclass of dict, which allows you to get AND SET(!!) | |
items in the dict using the attribute syntax! | |
When you previously had to write: | |
my_dict = {'a': {'b': {'c': [1, 2, 3]}}} | |
you can now do the same simply by: | |
my_Dict = Dict() | |
my_Dict.a.b.c = [1, 2, 3] | |
Or for instance, if you'd like to add some additional stuff, | |
where you'd with the normal dict would write | |
my_dict['a']['b']['d'] = [4, 5, 6], | |
you may now do the AWESOME | |
my_Dict.a.b.d = [4, 5, 6] | |
instead. But hey, you can always use the same syntax as a regular dict, | |
however, this will not raise TypeErrors or AttributeErrors at any time | |
while you try to get an item. A lot like a defaultdict. | |
""" | |
def __init__(self, *args, **kwargs): | |
""" | |
If we're initialized with a dict, make sure we turn all the | |
subdicts into Dicts as well. | |
""" | |
for arg in args: | |
if not arg: | |
continue | |
elif isinstance(arg, dict): | |
for key, val in arg.items(): | |
self[key] = self._hook(val) | |
elif isinstance(arg, tuple) and (not isinstance(arg[0], tuple)): | |
self[arg[0]] = self._hook(arg[1]) | |
elif isinstance(arg, (list, tuple)) or isgenerator(arg): | |
for key, val in arg: | |
self[key] = self._hook(val) | |
else: | |
raise TypeError("Dict does not understand " | |
"{0} types".format(type(arg))) | |
for key, val in kwargs.items(): | |
self[key] = val | |
def __setattr__(self, name, value): | |
""" | |
setattr is called when the syntax a.b = 2 is used to set a value. | |
""" | |
if hasattr(Dict, name): | |
raise AttributeError("'Dict' object attribute " | |
"'{0}' is read-only".format(name)) | |
else: | |
self[name] = value | |
def __setitem__(self, name, value): | |
""" | |
This is called when trying to set a value of the Dict using []. | |
E.g. some_instance_of_Dict['b'] = val. If 'val | |
""" | |
super(Dict, self).__setitem__(name, value) | |
@classmethod | |
def _hook(cls, item): | |
""" | |
Called to ensure that each dict-instance that are being set | |
is a addict Dict. Recurses. | |
""" | |
if isinstance(item, dict): | |
return cls(item) | |
elif isinstance(item, (list, tuple)): | |
return type(item)(cls._hook(elem) for elem in item) | |
return item | |
def __getattr__(self, item): | |
return self.__getitem__(item) | |
def __getitem__(self, name): | |
""" | |
This is called when the Dict is accessed by []. E.g. | |
some_instance_of_Dict['a']; | |
If the name is in the dict, we return it. Otherwise we set both | |
the attr and item to a new instance of Dict. | |
""" | |
if name not in self: | |
self[name] = Dict() | |
return super(Dict, self).__getitem__(name) | |
def __delattr__(self, name): | |
""" Is invoked when del some_addict.b is called. """ | |
del self[name] | |
_re_pattern = re.compile('[a-zA-Z_][a-zA-Z0-9_]*') | |
def __dir__(self): | |
""" | |
Return a list of addict object attributes. | |
This includes key names of any dict entries, filtered to the subset of | |
valid attribute names (e.g. alphanumeric strings beginning with a | |
letter or underscore). Also includes attributes of parent dict class. | |
""" | |
dict_keys = [] | |
for k in self.keys(): | |
if isinstance(k, str): | |
m = self._re_pattern.match(k) | |
if m: | |
dict_keys.append(m.string) | |
obj_attrs = list(dir(Dict)) | |
return dict_keys + obj_attrs | |
def _ipython_display_(self): | |
print(str(self)) # pragma: no cover | |
def _repr_html_(self): | |
return str(self) | |
def prune(self, prune_zero=False, prune_empty_list=True): | |
""" | |
Removes all empty Dicts and falsy stuff inside the Dict. | |
E.g | |
>>> a = Dict() | |
>>> a.b.c.d | |
{} | |
>>> a.a = 2 | |
>>> a | |
{'a': 2, 'b': {'c': {'d': {}}}} | |
>>> a.prune() | |
>>> a | |
{'a': 2} | |
Set prune_zero=True to remove 0 values | |
E.g | |
>>> a = Dict() | |
>>> a.b.c.d = 0 | |
>>> a.prune(prune_zero=True) | |
>>> a | |
{} | |
Set prune_empty_list=False to have them persist | |
E.g | |
>>> a = Dict({'a': []}) | |
>>> a.prune() | |
>>> a | |
{} | |
>>> a = Dict({'a': []}) | |
>>> a.prune(prune_empty_list=False) | |
>>> a | |
{'a': []} | |
""" | |
for key, val in list(self.items()): | |
if ((not val) and ((val != 0) or prune_zero) and | |
not isinstance(val, list)): | |
del self[key] | |
elif isinstance(val, Dict): | |
val.prune(prune_zero, prune_empty_list) | |
if not val: | |
del self[key] | |
elif isinstance(val, (list, tuple)): | |
new_iter = self._prune_iter(val, prune_zero, prune_empty_list) | |
if (not new_iter) and prune_empty_list: | |
del self[key] | |
else: | |
if isinstance(val, tuple): | |
new_iter = tuple(new_iter) | |
self[key] = new_iter | |
@classmethod | |
def _prune_iter(cls, some_iter, prune_zero=False, prune_empty_list=True): | |
new_iter = [] | |
for item in some_iter: | |
if item == 0 and prune_zero: | |
continue | |
elif isinstance(item, Dict): | |
item.prune(prune_zero, prune_empty_list) | |
if item: | |
new_iter.append(item) | |
elif isinstance(item, (list, tuple)): | |
new_item = type(item)( | |
cls._prune_iter(item, prune_zero, prune_empty_list)) | |
if new_item or not prune_empty_list: | |
new_iter.append(new_item) | |
else: | |
new_iter.append(item) | |
return new_iter | |
def to_dict(self): | |
""" Recursively turn your addict Dicts into dicts. """ | |
base = {} | |
for key, value in self.items(): | |
if isinstance(value, type(self)): | |
base[key] = value.to_dict() | |
elif isinstance(value, (list, tuple)): | |
base[key] = type(value)( | |
item.to_dict() if isinstance(item, type(self)) else | |
item for item in value) | |
else: | |
base[key] = value | |
return base | |
def copy(self): | |
""" | |
Return a disconnected deep copy of self. Children of type Dict, list | |
and tuple are copied recursively while values that are instances of | |
other mutable objects are not copied. | |
""" | |
return Dict(self.to_dict()) | |
def __deepcopy__(self, memo): | |
""" Return a disconnected deep copy of self. """ | |
y = self.__class__() | |
memo[id(self)] = y | |
for key, value in self.items(): | |
y[copy.deepcopy(key, memo)] = copy.deepcopy(value, memo) | |
return y | |
def update(self, *args, **kwargs): | |
other = {} | |
if args: | |
if len(args) > 1: | |
raise TypeError() | |
other.update(args[0]) | |
other.update(kwargs) | |
for k, v in other.items(): | |
if ((k not in self) or | |
(not isinstance(self[k], dict)) or | |
(not isinstance(v, dict))): | |
self[k] = v | |
else: | |
self[k].update(v) | |
@classmethod | |
def from_data(cls, data): | |
if isinstance(data, dict): | |
r = cls() | |
r.update({k: cls.from_data(v) for k, v in data.items()}) | |
return r | |
elif isinstance(data, list): | |
return [cls.from_data(x) for x in data] | |
else: | |
return data | |
def __getnewargs__(self): | |
return tuple(self.items()) | |
def __getstate__(self): | |
return self | |
def __setstate__(self, state): | |
self.update(state) | |
if __name__ == '__main__': | |
data = { | |
'a': 1, | |
'b': { | |
'v1': 1, | |
'v2': 3 | |
}, | |
'c': { | |
'vals': [ | |
{'v1': 1}, | |
{'v2': 2} | |
] | |
} | |
} | |
d = Dict.from_data(data) | |
print d.a | |
print d.b.v1 | |
print d.c.vals[1].v2 | |
d.zzl.test = 3 | |
print d.zzl.test | |
print d.to_dict() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
cf: https://github.com/mewwts/addict
I just add
from_data
method