Spaces:
No application file
No application file
| # encoding: utf-8 | |
| """A dict subclass that supports attribute style access. | |
| Authors: | |
| * Fernando Perez (original) | |
| * Brian Granger (refactoring to a dict subclass) | |
| """ | |
| #----------------------------------------------------------------------------- | |
| # Copyright (C) 2008-2011 The IPython Development Team | |
| # | |
| # Distributed under the terms of the BSD License. The full license is in | |
| # the file COPYING, distributed as part of this software. | |
| #----------------------------------------------------------------------------- | |
| #----------------------------------------------------------------------------- | |
| # Imports | |
| #----------------------------------------------------------------------------- | |
| __all__ = ['Struct'] | |
| #----------------------------------------------------------------------------- | |
| # Code | |
| #----------------------------------------------------------------------------- | |
| 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) 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) | |
| # 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]) | |