##// END OF EJS Templates
Updated ipstruct to fix doctest failures.
Fernando Perez -
Show More
@@ -1,394 +1,406 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Mimic C structs with lots of extra functionality.
3 3
4 4 $Id: ipstruct.py 1950 2006-11-28 19:15:35Z vivainio $"""
5 5
6 6 #*****************************************************************************
7 7 # Copyright (C) 2001-2004 Fernando Perez <fperez@colorado.edu>
8 8 #
9 9 # Distributed under the terms of the BSD License. The full license is in
10 10 # the file COPYING, distributed as part of this software.
11 11 #*****************************************************************************
12 12
13 13 from IPython import Release
14 14 __author__ = '%s <%s>' % Release.authors['Fernando']
15 15 __license__ = Release.license
16 16
17 17 __all__ = ['Struct']
18 18
19 19 import types
20 20 import pprint
21 21
22 22 from IPython.genutils import list2dict2
23 23
24 24 class Struct:
25 25 """Class to mimic C structs but also provide convenient dictionary-like
26 26 functionality.
27 27
28 28 Instances can be initialized with a dictionary, a list of key=value pairs
29 29 or both. If both are present, the dictionary must come first.
30 30
31 31 Because Python classes provide direct assignment to their members, it's
32 32 easy to overwrite normal methods (S.copy = 1 would destroy access to
33 33 S.copy()). For this reason, all builtin method names are protected and
34 34 can't be assigned to. An attempt to do s.copy=1 or s['copy']=1 will raise
35 35 a KeyError exception. If you really want to, you can bypass this
36 36 protection by directly assigning to __dict__: s.__dict__['copy']=1 will
37 37 still work. Doing this will break functionality, though. As in most of
38 38 Python, namespace protection is weakly enforced, so feel free to shoot
39 39 yourself if you really want to.
40 40
41 41 Note that this class uses more memory and is *much* slower than a regular
42 42 dictionary, so be careful in situations where memory or performance are
43 43 critical. But for day to day use it should behave fine. It is particularly
44 44 convenient for storing configuration data in programs.
45 45
46 46 +,+=,- and -= are implemented. +/+= do merges (non-destructive updates),
47 47 -/-= remove keys from the original. See the method descripitions.
48 48
49 49 This class allows a quick access syntax: both s.key and s['key'] are
50 50 valid. This syntax has a limitation: each 'key' has to be explicitly
51 51 accessed by its original name. The normal s.key syntax doesn't provide
52 52 access to the keys via variables whose values evaluate to the desired
53 53 keys. An example should clarify this:
54 54
55 55 Define a dictionary and initialize both with dict and k=v pairs:
56 56 >>> d={'a':1,'b':2}
57 57 >>> s=Struct(d,hi=10,ho=20)
58
58 59 The return of __repr__ can be used to create a new instance:
59 60 >>> s
60 Struct({'ho': 20, 'b': 2, 'hi': 10, 'a': 1})
61 Struct({'__allownew': True, 'a': 1, 'b': 2, 'hi': 10, 'ho': 20})
62
63 Note: the special '__allownew' key is used for internal purposes.
64
61 65 __str__ (called by print) shows it's not quite a regular dictionary:
62 66 >>> print s
63 Struct {a: 1, b: 2, hi: 10, ho: 20}
67 Struct({'__allownew': True, 'a': 1, 'b': 2, 'hi': 10, 'ho': 20})
68
64 69 Access by explicitly named key with dot notation:
65 70 >>> s.a
66 71 1
72
67 73 Or like a dictionary:
68 74 >>> s['a']
69 75 1
76
70 77 If you want a variable to hold the key value, only dictionary access works:
71 78 >>> key='hi'
72 79 >>> s.key
73 80 Traceback (most recent call last):
74 81 File "<stdin>", line 1, in ?
75 82 AttributeError: Struct instance has no attribute 'key'
83
76 84 >>> s[key]
77 85 10
78 86
79 87 Another limitation of the s.key syntax (and Struct(key=val)
80 88 initialization): keys can't be numbers. But numeric keys can be used and
81 89 accessed using the dictionary syntax. Again, an example:
82 90
83 91 This doesn't work:
84 >>> s=Struct(4='hi')
92 >>> s=Struct(4='hi') #doctest: +IGNORE_EXCEPTION_DETAIL
93 Traceback (most recent call last):
94 ...
85 95 SyntaxError: keyword can't be an expression
96
86 97 But this does:
87 98 >>> s=Struct()
88 99 >>> s[4]='hi'
89 100 >>> s
90 Struct({4: 'hi'})
101 Struct({4: 'hi', '__allownew': True})
91 102 >>> s[4]
92 103 'hi'
93 104 """
94 105
95 106 # Attributes to which __setitem__ and __setattr__ will block access.
96 107 # Note: much of this will be moot in Python 2.2 and will be done in a much
97 108 # cleaner way.
98 109 __protected = ('copy dict dictcopy get has_attr has_key items keys '
99 110 'merge popitem setdefault update values '
100 111 '__make_dict __dict_invert ').split()
101 112
102 113 def __init__(self,dict=None,**kw):
103 114 """Initialize with a dictionary, another Struct, or by giving
104 115 explicitly the list of attributes.
105 116
106 117 Both can be used, but the dictionary must come first:
107 118 Struct(dict), Struct(k1=v1,k2=v2) or Struct(dict,k1=v1,k2=v2).
108 119 """
109 120 self.__dict__['__allownew'] = True
110 121 if dict is None:
111 122 dict = {}
112 123 if isinstance(dict,Struct):
113 124 dict = dict.dict()
114 125 elif dict and type(dict) is not types.DictType:
115 126 raise TypeError,\
116 127 'Initialize with a dictionary or key=val pairs.'
117 128 dict.update(kw)
118 129 # do the updating by hand to guarantee that we go through the
119 130 # safety-checked __setitem__
120 131 for k,v in dict.items():
121 132 self[k] = v
122 133
123 134
124 135 def __setitem__(self,key,value):
125 136 """Used when struct[key] = val calls are made."""
126 137 if key in Struct.__protected:
127 138 raise KeyError,'Key '+`key`+' is a protected key of class Struct.'
128 139 if not self['__allownew'] and key not in self.__dict__:
129 140 raise KeyError(
130 141 "Can't create unknown attribute %s - Check for typos, or use allow_new_attr to create new attributes!" %
131 142 key)
132 143
133 144 self.__dict__[key] = value
134 145
135 146 def __setattr__(self, key, value):
136 147 """Used when struct.key = val calls are made."""
137 148 self.__setitem__(key,value)
138 149
139 150 def __str__(self):
140 151 """Gets called by print."""
141 152
142 153 return 'Struct('+ pprint.pformat(self.__dict__)+')'
143 154
144 155 def __repr__(self):
145 156 """Gets called by repr.
146 157
147 158 A Struct can be recreated with S_new=eval(repr(S_old))."""
148 159 return self.__str__()
149 160
150 161 def __getitem__(self,key):
151 162 """Allows struct[key] access."""
152 163 return self.__dict__[key]
153 164
154 165 def __contains__(self,key):
155 166 """Allows use of the 'in' operator."""
156 167 return self.__dict__.has_key(key)
157 168
158 169 def __iadd__(self,other):
159 170 """S += S2 is a shorthand for S.merge(S2)."""
160 171 self.merge(other)
161 172 return self
162 173
163 174 def __add__(self,other):
164 175 """S + S2 -> New Struct made form S and S.merge(S2)"""
165 176 Sout = self.copy()
166 177 Sout.merge(other)
167 178 return Sout
168 179
169 180 def __sub__(self,other):
170 181 """Return S1-S2, where all keys in S2 have been deleted (if present)
171 182 from S1."""
172 183 Sout = self.copy()
173 184 Sout -= other
174 185 return Sout
175 186
176 187 def __isub__(self,other):
177 188 """Do in place S = S - S2, meaning all keys in S2 have been deleted
178 189 (if present) from S1."""
179 190
180 191 for k in other.keys():
181 192 if self.has_key(k):
182 193 del self.__dict__[k]
183 194
184 195 def __make_dict(self,__loc_data__,**kw):
185 196 "Helper function for update and merge. Return a dict from data."
186 197
187 198 if __loc_data__ == None:
188 199 dict = {}
189 200 elif type(__loc_data__) is types.DictType:
190 201 dict = __loc_data__
191 202 elif isinstance(__loc_data__,Struct):
192 203 dict = __loc_data__.__dict__
193 204 else:
194 205 raise TypeError, 'Update with a dict, a Struct or key=val pairs.'
195 206 if kw:
196 207 dict.update(kw)
197 208 return dict
198 209
199 210 def __dict_invert(self,dict):
200 211 """Helper function for merge. Takes a dictionary whose values are
201 212 lists and returns a dict. with the elements of each list as keys and
202 213 the original keys as values."""
203 214
204 215 outdict = {}
205 216 for k,lst in dict.items():
206 217 if type(lst) is types.StringType:
207 218 lst = lst.split()
208 219 for entry in lst:
209 220 outdict[entry] = k
210 221 return outdict
211 222
212 223 def clear(self):
213 224 """Clear all attributes."""
214 225 self.__dict__.clear()
215 226
216 227 def copy(self):
217 228 """Return a (shallow) copy of a Struct."""
218 229 return Struct(self.__dict__.copy())
219 230
220 231 def dict(self):
221 232 """Return the Struct's dictionary."""
222 233 return self.__dict__
223 234
224 235 def dictcopy(self):
225 236 """Return a (shallow) copy of the Struct's dictionary."""
226 237 return self.__dict__.copy()
227 238
228 239 def popitem(self):
229 240 """S.popitem() -> (k, v), remove and return some (key, value) pair as
230 241 a 2-tuple; but raise KeyError if S is empty."""
231 242 return self.__dict__.popitem()
232 243
233 244 def update(self,__loc_data__=None,**kw):
234 245 """Update (merge) with data from another Struct or from a dictionary.
235 246 Optionally, one or more key=value pairs can be given at the end for
236 247 direct update."""
237 248
238 249 # The funny name __loc_data__ is to prevent a common variable name which
239 250 # could be a fieled of a Struct to collide with this parameter. The problem
240 251 # would arise if the function is called with a keyword with this same name
241 252 # that a user means to add as a Struct field.
242 253 newdict = Struct.__make_dict(self,__loc_data__,**kw)
243 254 for k,v in newdict.items():
244 255 self[k] = v
245 256
246 257 def merge(self,__loc_data__=None,__conflict_solve=None,**kw):
247 258 """S.merge(data,conflict,k=v1,k=v2,...) -> merge data and k=v into S.
248 259
249 260 This is similar to update(), but much more flexible. First, a dict is
250 261 made from data+key=value pairs. When merging this dict with the Struct
251 262 S, the optional dictionary 'conflict' is used to decide what to do.
252 263
253 264 If conflict is not given, the default behavior is to preserve any keys
254 265 with their current value (the opposite of the update method's
255 266 behavior).
256 267
257 268 conflict is a dictionary of binary functions which will be used to
258 269 solve key conflicts. It must have the following structure:
259 270
260 271 conflict == { fn1 : [Skey1,Skey2,...], fn2 : [Skey3], etc }
261 272
262 273 Values must be lists or whitespace separated strings which are
263 274 automatically converted to lists of strings by calling string.split().
264 275
265 276 Each key of conflict is a function which defines a policy for
266 277 resolving conflicts when merging with the input data. Each fn must be
267 278 a binary function which returns the desired outcome for a key
268 279 conflict. These functions will be called as fn(old,new).
269 280
270 281 An example is probably in order. Suppose you are merging the struct S
271 282 with a dict D and the following conflict policy dict:
272 283
273 284 S.merge(D,{fn1:['a','b',4], fn2:'key_c key_d'})
274 285
275 286 If the key 'a' is found in both S and D, the merge method will call:
276 287
277 288 S['a'] = fn1(S['a'],D['a'])
278 289
279 290 As a convenience, merge() provides five (the most commonly needed)
280 291 pre-defined policies: preserve, update, add, add_flip and add_s. The
281 292 easiest explanation is their implementation:
282 293
283 294 preserve = lambda old,new: old
284 295 update = lambda old,new: new
285 296 add = lambda old,new: old + new
286 297 add_flip = lambda old,new: new + old # note change of order!
287 298 add_s = lambda old,new: old + ' ' + new # only works for strings!
288 299
289 300 You can use those four words (as strings) as keys in conflict instead
290 301 of defining them as functions, and the merge method will substitute
291 302 the appropriate functions for you. That is, the call
292 303
293 304 S.merge(D,{'preserve':'a b c','add':[4,5,'d'],my_function:[6]})
294 305
295 306 will automatically substitute the functions preserve and add for the
296 307 names 'preserve' and 'add' before making any function calls.
297 308
298 309 For more complicated conflict resolution policies, you still need to
299 310 construct your own functions. """
300 311
301 312 data_dict = Struct.__make_dict(self,__loc_data__,**kw)
302 313
303 314 # policies for conflict resolution: two argument functions which return
304 315 # the value that will go in the new struct
305 316 preserve = lambda old,new: old
306 317 update = lambda old,new: new
307 318 add = lambda old,new: old + new
308 319 add_flip = lambda old,new: new + old # note change of order!
309 320 add_s = lambda old,new: old + ' ' + new
310 321
311 322 # default policy is to keep current keys when there's a conflict
312 323 conflict_solve = list2dict2(self.keys(),default = preserve)
313 324
314 325 # the conflict_solve dictionary is given by the user 'inverted': we
315 326 # need a name-function mapping, it comes as a function -> names
316 327 # dict. Make a local copy (b/c we'll make changes), replace user
317 328 # strings for the three builtin policies and invert it.
318 329 if __conflict_solve:
319 330 inv_conflict_solve_user = __conflict_solve.copy()
320 331 for name, func in [('preserve',preserve), ('update',update),
321 ('add',add), ('add_flip',add_flip), ('add_s',add_s)]:
332 ('add',add), ('add_flip',add_flip),
333 ('add_s',add_s)]:
322 334 if name in inv_conflict_solve_user.keys():
323 335 inv_conflict_solve_user[func] = inv_conflict_solve_user[name]
324 336 del inv_conflict_solve_user[name]
325 337 conflict_solve.update(Struct.__dict_invert(self,inv_conflict_solve_user))
326 338 #print 'merge. conflict_solve: '; pprint(conflict_solve) # dbg
327 339 #print '*'*50,'in merger. conflict_solver:'; pprint(conflict_solve)
328 340 for key in data_dict:
329 341 if key not in self:
330 342 self[key] = data_dict[key]
331 343 else:
332 344 self[key] = conflict_solve[key](self[key],data_dict[key])
333 345
334 346 def has_key(self,key):
335 347 """Like has_key() dictionary method."""
336 348 return self.__dict__.has_key(key)
337 349
338 350 def hasattr(self,key):
339 351 """hasattr function available as a method.
340 352
341 353 Implemented like has_key, to make sure that all available keys in the
342 354 internal dictionary of the Struct appear also as attributes (even
343 355 numeric keys)."""
344 356 return self.__dict__.has_key(key)
345 357
346 358 def items(self):
347 359 """Return the items in the Struct's dictionary, in the same format
348 360 as a call to {}.items()."""
349 361 return self.__dict__.items()
350 362
351 363 def keys(self):
352 364 """Return the keys in the Struct's dictionary, in the same format
353 365 as a call to {}.keys()."""
354 366 return self.__dict__.keys()
355 367
356 368 def values(self,keys=None):
357 369 """Return the values in the Struct's dictionary, in the same format
358 370 as a call to {}.values().
359 371
360 372 Can be called with an optional argument keys, which must be a list or
361 373 tuple of keys. In this case it returns only the values corresponding
362 374 to those keys (allowing a form of 'slicing' for Structs)."""
363 375 if not keys:
364 376 return self.__dict__.values()
365 377 else:
366 378 ret=[]
367 379 for k in keys:
368 380 ret.append(self[k])
369 381 return ret
370 382
371 383 def get(self,attr,val=None):
372 """S.get(k[,d]) -> S[k] if S.has_key(k), else d. d defaults to None."""
384 """S.get(k[,d]) -> S[k] if k in S, else d. d defaults to None."""
373 385 try:
374 386 return self[attr]
375 387 except KeyError:
376 388 return val
377 389
378 390 def setdefault(self,attr,val=None):
379 """S.setdefault(k[,d]) -> S.get(k,d), also set S[k]=d if not S.has_key(k)"""
391 """S.setdefault(k[,d]) -> S.get(k,d), also set S[k]=d if k not in S"""
380 392 if not self.has_key(attr):
381 393 self[attr] = val
382 394 return self.get(attr,val)
383 395
384 396 def allow_new_attr(self, allow = True):
385 397 """ Set whether new attributes can be created inside struct
386 398
387 This can be used to catch typos by verifying that the attribute user tries to
388 change already exists in this Struct.
399 This can be used to catch typos by verifying that the attribute user
400 tries to change already exists in this Struct.
389 401 """
390 402 self['__allownew'] = allow
391 403
392 404
393 405 # end class Struct
394 406
General Comments 0
You need to be logged in to leave comments. Login now