| |
| """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]) |
|
|
|
|