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