|
|
|
"""A dict subclass that supports attribute style access. |
|
|
|
Authors: |
|
|
|
* Fernando Perez (original) |
|
* Brian Granger (refactoring to a dict subclass) |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = ['Struct'] |
|
|
|
|
|
|
|
|
|
|
|
|
|
class Struct(dict): |
|
"""A dict subclass with attribute style access. |
|
|
|
This dict subclass has a a few extra features: |
|
|
|
* Attribute style access. |
|
* Protection of class members (like keys, items) when using attribute |
|
style access. |
|
* The ability to restrict assignment to only existing keys. |
|
* Intelligent merging. |
|
* Overloaded operators. |
|
""" |
|
_allownew = True |
|
def __init__(self, *args, **kw): |
|
"""Initialize with a dictionary, another Struct, or data. |
|
|
|
Parameters |
|
---------- |
|
*args : dict, Struct |
|
Initialize with one dict or Struct |
|
**kw : dict |
|
Initialize with key, value pairs. |
|
|
|
Examples |
|
-------- |
|
>>> s = Struct(a=10,b=30) |
|
>>> s.a |
|
10 |
|
>>> s.b |
|
30 |
|
>>> s2 = Struct(s,c=30) |
|
>>> sorted(s2.keys()) |
|
['a', 'b', 'c'] |
|
""" |
|
object.__setattr__(self, '_allownew', True) |
|
dict.__init__(self, *args, **kw) |
|
|
|
def __setitem__(self, key, value): |
|
"""Set an item with check for allownew. |
|
|
|
Examples |
|
-------- |
|
>>> s = Struct() |
|
>>> s['a'] = 10 |
|
>>> s.allow_new_attr(False) |
|
>>> s['a'] = 10 |
|
>>> s['a'] |
|
10 |
|
>>> try: |
|
... s['b'] = 20 |
|
... except KeyError: |
|
... print('this is not allowed') |
|
... |
|
this is not allowed |
|
""" |
|
if not self._allownew and key not in self: |
|
raise KeyError( |
|
"can't create new attribute %s when allow_new_attr(False)" % key) |
|
dict.__setitem__(self, key, value) |
|
|
|
def __setattr__(self, key, value): |
|
"""Set an attr with protection of class members. |
|
|
|
This calls :meth:`self.__setitem__` but convert :exc:`KeyError` to |
|
:exc:`AttributeError`. |
|
|
|
Examples |
|
-------- |
|
>>> s = Struct() |
|
>>> s.a = 10 |
|
>>> s.a |
|
10 |
|
>>> try: |
|
... s.get = 10 |
|
... except AttributeError: |
|
... print("you can't set a class member") |
|
... |
|
you can't set a class member |
|
""" |
|
|
|
if isinstance(key, str): |
|
|
|
|
|
|
|
|
|
if key in self.__dict__ or hasattr(Struct, key): |
|
raise AttributeError( |
|
'attr %s is a protected member of class Struct.' % key |
|
) |
|
try: |
|
self.__setitem__(key, value) |
|
except KeyError as e: |
|
raise AttributeError(e) from e |
|
|
|
def __getattr__(self, key): |
|
"""Get an attr by calling :meth:`dict.__getitem__`. |
|
|
|
Like :meth:`__setattr__`, this method converts :exc:`KeyError` to |
|
:exc:`AttributeError`. |
|
|
|
Examples |
|
-------- |
|
>>> s = Struct(a=10) |
|
>>> s.a |
|
10 |
|
>>> type(s.get) |
|
<...method'> |
|
>>> try: |
|
... s.b |
|
... except AttributeError: |
|
... print("I don't have that key") |
|
... |
|
I don't have that key |
|
""" |
|
try: |
|
result = self[key] |
|
except KeyError as e: |
|
raise AttributeError(key) from e |
|
else: |
|
return result |
|
|
|
def __iadd__(self, other): |
|
"""s += s2 is a shorthand for s.merge(s2). |
|
|
|
Examples |
|
-------- |
|
>>> s = Struct(a=10,b=30) |
|
>>> s2 = Struct(a=20,c=40) |
|
>>> s += s2 |
|
>>> sorted(s.keys()) |
|
['a', 'b', 'c'] |
|
""" |
|
self.merge(other) |
|
return self |
|
|
|
def __add__(self,other): |
|
"""s + s2 -> New Struct made from s.merge(s2). |
|
|
|
Examples |
|
-------- |
|
>>> s1 = Struct(a=10,b=30) |
|
>>> s2 = Struct(a=20,c=40) |
|
>>> s = s1 + s2 |
|
>>> sorted(s.keys()) |
|
['a', 'b', 'c'] |
|
""" |
|
sout = self.copy() |
|
sout.merge(other) |
|
return sout |
|
|
|
def __sub__(self,other): |
|
"""s1 - s2 -> remove keys in s2 from s1. |
|
|
|
Examples |
|
-------- |
|
>>> s1 = Struct(a=10,b=30) |
|
>>> s2 = Struct(a=40) |
|
>>> s = s1 - s2 |
|
>>> s |
|
{'b': 30} |
|
""" |
|
sout = self.copy() |
|
sout -= other |
|
return sout |
|
|
|
def __isub__(self,other): |
|
"""Inplace remove keys from self that are in other. |
|
|
|
Examples |
|
-------- |
|
>>> s1 = Struct(a=10,b=30) |
|
>>> s2 = Struct(a=40) |
|
>>> s1 -= s2 |
|
>>> s1 |
|
{'b': 30} |
|
""" |
|
for k in other.keys(): |
|
if k in self: |
|
del self[k] |
|
return self |
|
|
|
def __dict_invert(self, data): |
|
"""Helper function for merge. |
|
|
|
Takes a dictionary whose values are lists and returns a dict with |
|
the elements of each list as keys and the original keys as values. |
|
""" |
|
outdict = {} |
|
for k,lst in data.items(): |
|
if isinstance(lst, str): |
|
lst = lst.split() |
|
for entry in lst: |
|
outdict[entry] = k |
|
return outdict |
|
|
|
def dict(self): |
|
return self |
|
|
|
def copy(self): |
|
"""Return a copy as a Struct. |
|
|
|
Examples |
|
-------- |
|
>>> s = Struct(a=10,b=30) |
|
>>> s2 = s.copy() |
|
>>> type(s2) is Struct |
|
True |
|
""" |
|
return Struct(dict.copy(self)) |
|
|
|
def hasattr(self, key): |
|
"""hasattr function available as a method. |
|
|
|
Implemented like has_key. |
|
|
|
Examples |
|
-------- |
|
>>> s = Struct(a=10) |
|
>>> s.hasattr('a') |
|
True |
|
>>> s.hasattr('b') |
|
False |
|
>>> s.hasattr('get') |
|
False |
|
""" |
|
return key in self |
|
|
|
def allow_new_attr(self, allow = True): |
|
"""Set whether new attributes can be created in this Struct. |
|
|
|
This can be used to catch typos by verifying that the attribute user |
|
tries to change already exists in this Struct. |
|
""" |
|
object.__setattr__(self, '_allownew', allow) |
|
|
|
def merge(self, __loc_data__=None, __conflict_solve=None, **kw): |
|
"""Merge two Structs with customizable conflict resolution. |
|
|
|
This is similar to :meth:`update`, but much more flexible. First, a |
|
dict is made from data+key=value pairs. When merging this dict with |
|
the Struct S, the optional dictionary 'conflict' is used to decide |
|
what to do. |
|
|
|
If conflict is not given, the default behavior is to preserve any keys |
|
with their current value (the opposite of the :meth:`update` method's |
|
behavior). |
|
|
|
Parameters |
|
---------- |
|
__loc_data__ : dict, Struct |
|
The data to merge into self |
|
__conflict_solve : dict |
|
The conflict policy dict. The keys are binary functions used to |
|
resolve the conflict and the values are lists of strings naming |
|
the keys the conflict resolution function applies to. Instead of |
|
a list of strings a space separated string can be used, like |
|
'a b c'. |
|
**kw : dict |
|
Additional key, value pairs to merge in |
|
|
|
Notes |
|
----- |
|
The `__conflict_solve` dict is a dictionary of binary functions which will be used to |
|
solve key conflicts. Here is an example:: |
|
|
|
__conflict_solve = dict( |
|
func1=['a','b','c'], |
|
func2=['d','e'] |
|
) |
|
|
|
In this case, the function :func:`func1` will be used to resolve |
|
keys 'a', 'b' and 'c' and the function :func:`func2` will be used for |
|
keys 'd' and 'e'. This could also be written as:: |
|
|
|
__conflict_solve = dict(func1='a b c',func2='d e') |
|
|
|
These functions will be called for each key they apply to with the |
|
form:: |
|
|
|
func1(self['a'], other['a']) |
|
|
|
The return value is used as the final merged value. |
|
|
|
As a convenience, merge() provides five (the most commonly needed) |
|
pre-defined policies: preserve, update, add, add_flip and add_s. The |
|
easiest explanation is their implementation:: |
|
|
|
preserve = lambda old,new: old |
|
update = lambda old,new: new |
|
add = lambda old,new: old + new |
|
add_flip = lambda old,new: new + old # note change of order! |
|
add_s = lambda old,new: old + ' ' + new # only for str! |
|
|
|
You can use those four words (as strings) as keys instead |
|
of defining them as functions, and the merge method will substitute |
|
the appropriate functions for you. |
|
|
|
For more complicated conflict resolution policies, you still need to |
|
construct your own functions. |
|
|
|
Examples |
|
-------- |
|
This show the default policy: |
|
|
|
>>> s = Struct(a=10,b=30) |
|
>>> s2 = Struct(a=20,c=40) |
|
>>> s.merge(s2) |
|
>>> sorted(s.items()) |
|
[('a', 10), ('b', 30), ('c', 40)] |
|
|
|
Now, show how to specify a conflict dict: |
|
|
|
>>> s = Struct(a=10,b=30) |
|
>>> s2 = Struct(a=20,b=40) |
|
>>> conflict = {'update':'a','add':'b'} |
|
>>> s.merge(s2,conflict) |
|
>>> sorted(s.items()) |
|
[('a', 20), ('b', 70)] |
|
""" |
|
|
|
data_dict = dict(__loc_data__,**kw) |
|
|
|
|
|
|
|
preserve = lambda old,new: old |
|
update = lambda old,new: new |
|
add = lambda old,new: old + new |
|
add_flip = lambda old,new: new + old |
|
add_s = lambda old,new: old + ' ' + new |
|
|
|
|
|
conflict_solve = dict.fromkeys(self, preserve) |
|
|
|
|
|
|
|
|
|
|
|
if __conflict_solve: |
|
inv_conflict_solve_user = __conflict_solve.copy() |
|
for name, func in [('preserve',preserve), ('update',update), |
|
('add',add), ('add_flip',add_flip), |
|
('add_s',add_s)]: |
|
if name in inv_conflict_solve_user.keys(): |
|
inv_conflict_solve_user[func] = inv_conflict_solve_user[name] |
|
del inv_conflict_solve_user[name] |
|
conflict_solve.update(self.__dict_invert(inv_conflict_solve_user)) |
|
for key in data_dict: |
|
if key not in self: |
|
self[key] = data_dict[key] |
|
else: |
|
self[key] = conflict_solve[key](self[key],data_dict[key]) |
|
|
|
|