Skip to content

Instantly share code, notes, and snippets.

@zzl0
Created September 22, 2016 17:08
Show Gist options
  • Save zzl0/9565c0862cd43ae4e25d0d190239a40a to your computer and use it in GitHub Desktop.
Save zzl0/9565c0862cd43ae4e25d0d190239a40a to your computer and use it in GitHub Desktop.
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()
@zzl0
Copy link
Author

zzl0 commented Sep 22, 2016

cf: https://github.com/mewwts/addict

I just add from_data method

    @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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment