123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- # encoding: utf-8
- """A dict subclass that supports attribute style access.
- Can probably be replaced by types.SimpleNamespace from Python 3.3
- """
- # Copyright (c) IPython Development Team.
- # Distributed under the terms of the Modified BSD License.
- __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 key is an str it might be a class member or instance var
- if isinstance(key, str):
- # I can't simply call hasattr here because it calls getattr, which
- # calls self.__getattr__, which returns True for keys in
- # self._data. But I only want keys in the class and in
- # self.__dict__
- 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)
- 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)
- <... 'builtin_function_or_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:
- raise AttributeError(key)
- 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)
- # policies for conflict resolution: two argument functions which return
- # the value that will go in the new struct
- 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
- # default policy is to keep current keys when there's a conflict
- conflict_solve = dict.fromkeys(self, preserve)
- # the conflict_solve dictionary is given by the user 'inverted': we
- # need a name-function mapping, it comes as a function -> names
- # dict. Make a local copy (b/c we'll make changes), replace user
- # strings for the three builtin policies and invert it.
- 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])
|