1 # encoding: utf-8 2 """A dict subclass that supports attribute style access. 3 4 Authors: 5 6 * Fernando Perez (original) 7 * Brian Granger (refactoring to a dict subclass) 8 """ 9 10 #----------------------------------------------------------------------------- 11 # Copyright (C) 2008-2011 The IPython Development Team 12 # 13 # Distributed under the terms of the BSD License. The full license is in 14 # the file COPYING, distributed as part of this software. 15 #----------------------------------------------------------------------------- 16 17 #----------------------------------------------------------------------------- 18 # Imports 19 #----------------------------------------------------------------------------- 20 21 __all__ = ['Struct'] 22 23 #----------------------------------------------------------------------------- 24 # Code 25 #----------------------------------------------------------------------------- 26 27 28 class Struct(dict): 29 """A dict subclass with attribute style access. 30 31 This dict subclass has a a few extra features: 32 33 * Attribute style access. 34 * Protection of class members (like keys, items) when using attribute 35 style access. 36 * The ability to restrict assignment to only existing keys. 37 * Intelligent merging. 38 * Overloaded operators. 39 """ 40 _allownew = True 41 def __init__(self, *args, **kw): 42 """Initialize with a dictionary, another Struct, or data. 43 44 Parameters 45 ---------- 46 *args : dict, Struct 47 Initialize with one dict or Struct 48 **kw : dict 49 Initialize with key, value pairs. 50 51 Examples 52 -------- 53 >>> s = Struct(a=10,b=30) 54 >>> s.a 55 10 56 >>> s.b 57 30 58 >>> s2 = Struct(s,c=30) 59 >>> sorted(s2.keys()) 60 ['a', 'b', 'c'] 61 """ 62 object.__setattr__(self, '_allownew', True) 63 dict.__init__(self, *args, **kw) 64 65 def __setitem__(self, key, value): 66 """Set an item with check for allownew. 67 68 Examples 69 -------- 70 >>> s = Struct() 71 >>> s['a'] = 10 72 >>> s.allow_new_attr(False) 73 >>> s['a'] = 10 74 >>> s['a'] 75 10 76 >>> try: 77 ... s['b'] = 20 78 ... except KeyError: 79 ... print('this is not allowed') 80 ... 81 this is not allowed 82 """ 83 if not self._allownew and key not in self: 84 raise KeyError( 85 "can't create new attribute %s when allow_new_attr(False)" % key) 86 dict.__setitem__(self, key, value) 87 88 def __setattr__(self, key, value): 89 """Set an attr with protection of class members. 90 91 This calls :meth:`self.__setitem__` but convert :exc:`KeyError` to 92 :exc:`AttributeError`. 93 94 Examples 95 -------- 96 >>> s = Struct() 97 >>> s.a = 10 98 >>> s.a 99 10 100 >>> try: 101 ... s.get = 10 102 ... except AttributeError: 103 ... print("you can't set a class member") 104 ... 105 you can't set a class member 106 """ 107 # If key is an str it might be a class member or instance var 108 if isinstance(key, str): 109 # I can't simply call hasattr here because it calls getattr, which 110 # calls self.__getattr__, which returns True for keys in 111 # self._data. But I only want keys in the class and in 112 # self.__dict__ 113 if key in self.__dict__ or hasattr(Struct, key): 114 raise AttributeError( 115 'attr %s is a protected member of class Struct.' % key 116 ) 117 try: 118 self.__setitem__(key, value) 119 except KeyError as e: 120 raise AttributeError(e) from e 121 122 def __getattr__(self, key): 123 """Get an attr by calling :meth:`dict.__getitem__`. 124 125 Like :meth:`__setattr__`, this method converts :exc:`KeyError` to 126 :exc:`AttributeError`. 127 128 Examples 129 -------- 130 >>> s = Struct(a=10) 131 >>> s.a 132 10 133 >>> type(s.get) 134 <...method'> 135 >>> try: 136 ... s.b 137 ... except AttributeError: 138 ... print("I don't have that key") 139 ... 140 I don't have that key 141 """ 142 try: 143 result = self[key] 144 except KeyError as e: 145 raise AttributeError(key) from e 146 else: 147 return result 148 149 def __iadd__(self, other): 150 """s += s2 is a shorthand for s.merge(s2). 151 152 Examples 153 -------- 154 >>> s = Struct(a=10,b=30) 155 >>> s2 = Struct(a=20,c=40) 156 >>> s += s2 157 >>> sorted(s.keys()) 158 ['a', 'b', 'c'] 159 """ 160 self.merge(other) 161 return self 162 163 def __add__(self,other): 164 """s + s2 -> New Struct made from s.merge(s2). 165 166 Examples 167 -------- 168 >>> s1 = Struct(a=10,b=30) 169 >>> s2 = Struct(a=20,c=40) 170 >>> s = s1 + s2 171 >>> sorted(s.keys()) 172 ['a', 'b', 'c'] 173 """ 174 sout = self.copy() 175 sout.merge(other) 176 return sout 177 178 def __sub__(self,other): 179 """s1 - s2 -> remove keys in s2 from s1. 180 181 Examples 182 -------- 183 >>> s1 = Struct(a=10,b=30) 184 >>> s2 = Struct(a=40) 185 >>> s = s1 - s2 186 >>> s 187 {'b': 30} 188 """ 189 sout = self.copy() 190 sout -= other 191 return sout 192 193 def __isub__(self,other): 194 """Inplace remove keys from self that are in other. 195 196 Examples 197 -------- 198 >>> s1 = Struct(a=10,b=30) 199 >>> s2 = Struct(a=40) 200 >>> s1 -= s2 201 >>> s1 202 {'b': 30} 203 """ 204 for k in other.keys(): 205 if k in self: 206 del self[k] 207 return self 208 209 def __dict_invert(self, data): 210 """Helper function for merge. 211 212 Takes a dictionary whose values are lists and returns a dict with 213 the elements of each list as keys and the original keys as values. 214 """ 215 outdict = {} 216 for k,lst in data.items(): 217 if isinstance(lst, str): 218 lst = lst.split() 219 for entry in lst: 220 outdict[entry] = k 221 return outdict 222 223 def dict(self): 224 return self 225 226 def copy(self): 227 """Return a copy as a Struct. 228 229 Examples 230 -------- 231 >>> s = Struct(a=10,b=30) 232 >>> s2 = s.copy() 233 >>> type(s2) is Struct 234 True 235 """ 236 return Struct(dict.copy(self)) 237 238 def hasattr(self, key): 239 """hasattr function available as a method. 240 241 Implemented like has_key. 242 243 Examples 244 -------- 245 >>> s = Struct(a=10) 246 >>> s.hasattr('a') 247 True 248 >>> s.hasattr('b') 249 False 250 >>> s.hasattr('get') 251 False 252 """ 253 return key in self 254 255 def allow_new_attr(self, allow = True): 256 """Set whether new attributes can be created in this Struct. 257 258 This can be used to catch typos by verifying that the attribute user 259 tries to change already exists in this Struct. 260 """ 261 object.__setattr__(self, '_allownew', allow) 262 263 def merge(self, __loc_data__=None, __conflict_solve=None, **kw): 264 """Merge two Structs with customizable conflict resolution. 265 266 This is similar to :meth:`update`, but much more flexible. First, a 267 dict is made from data+key=value pairs. When merging this dict with 268 the Struct S, the optional dictionary 'conflict' is used to decide 269 what to do. 270 271 If conflict is not given, the default behavior is to preserve any keys 272 with their current value (the opposite of the :meth:`update` method's 273 behavior). 274 275 Parameters 276 ---------- 277 __loc_data__ : dict, Struct 278 The data to merge into self 279 __conflict_solve : dict 280 The conflict policy dict. The keys are binary functions used to 281 resolve the conflict and the values are lists of strings naming 282 the keys the conflict resolution function applies to. Instead of 283 a list of strings a space separated string can be used, like 284 'a b c'. 285 **kw : dict 286 Additional key, value pairs to merge in 287 288 Notes 289 ----- 290 The `__conflict_solve` dict is a dictionary of binary functions which will be used to 291 solve key conflicts. Here is an example:: 292 293 __conflict_solve = dict( 294 func1=['a','b','c'], 295 func2=['d','e'] 296 ) 297 298 In this case, the function :func:`func1` will be used to resolve 299 keys 'a', 'b' and 'c' and the function :func:`func2` will be used for 300 keys 'd' and 'e'. This could also be written as:: 301 302 __conflict_solve = dict(func1='a b c',func2='d e') 303 304 These functions will be called for each key they apply to with the 305 form:: 306 307 func1(self['a'], other['a']) 308 309 The return value is used as the final merged value. 310 311 As a convenience, merge() provides five (the most commonly needed) 312 pre-defined policies: preserve, update, add, add_flip and add_s. The 313 easiest explanation is their implementation:: 314 315 preserve = lambda old,new: old 316 update = lambda old,new: new 317 add = lambda old,new: old + new 318 add_flip = lambda old,new: new + old # note change of order! 319 add_s = lambda old,new: old + ' ' + new # only for str! 320 321 You can use those four words (as strings) as keys instead 322 of defining them as functions, and the merge method will substitute 323 the appropriate functions for you. 324 325 For more complicated conflict resolution policies, you still need to 326 construct your own functions. 327 328 Examples 329 -------- 330 This show the default policy: 331 332 >>> s = Struct(a=10,b=30) 333 >>> s2 = Struct(a=20,c=40) 334 >>> s.merge(s2) 335 >>> sorted(s.items()) 336 [('a', 10), ('b', 30), ('c', 40)] 337 338 Now, show how to specify a conflict dict: 339 340 >>> s = Struct(a=10,b=30) 341 >>> s2 = Struct(a=20,b=40) 342 >>> conflict = {'update':'a','add':'b'} 343 >>> s.merge(s2,conflict) 344 >>> sorted(s.items()) 345 [('a', 20), ('b', 70)] 346 """ 347 348 data_dict = dict(__loc_data__,**kw) 349 350 # policies for conflict resolution: two argument functions which return 351 # the value that will go in the new struct 352 preserve = lambda old,new: old 353 update = lambda old,new: new 354 add = lambda old,new: old + new 355 add_flip = lambda old,new: new + old # note change of order! 356 add_s = lambda old,new: old + ' ' + new 357 358 # default policy is to keep current keys when there's a conflict 359 conflict_solve = dict.fromkeys(self, preserve) 360 361 # the conflict_solve dictionary is given by the user 'inverted': we 362 # need a name-function mapping, it comes as a function -> names 363 # dict. Make a local copy (b/c we'll make changes), replace user 364 # strings for the three builtin policies and invert it. 365 if __conflict_solve: 366 inv_conflict_solve_user = __conflict_solve.copy() 367 for name, func in [('preserve',preserve), ('update',update), 368 ('add',add), ('add_flip',add_flip), 369 ('add_s',add_s)]: 370 if name in inv_conflict_solve_user.keys(): 371 inv_conflict_solve_user[func] = inv_conflict_solve_user[name] 372 del inv_conflict_solve_user[name] 373 conflict_solve.update(self.__dict_invert(inv_conflict_solve_user)) 374 for key in data_dict: 375 if key not in self: 376 self[key] = data_dict[key] 377 else: 378 self[key] = conflict_solve[key](self[key],data_dict[key]) 379 380