ipstruct.py
416 lines
| 15.2 KiB
| text/x-python
|
PythonLexer
fperez
|
r0 | # -*- coding: utf-8 -*- | ||
"""Mimic C structs with lots of extra functionality. | ||||
Fernando Perez
|
r1853 | """ | ||
fperez
|
r0 | |||
#***************************************************************************** | ||||
# Copyright (C) 2001-2004 Fernando Perez <fperez@colorado.edu> | ||||
# | ||||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#***************************************************************************** | ||||
__all__ = ['Struct'] | ||||
import types | ||||
vivainio
|
r464 | import pprint | ||
fperez
|
r52 | |||
Brian Granger
|
r2023 | from IPython.utils.genutils import list2dict2 | ||
fperez
|
r0 | |||
class Struct: | ||||
"""Class to mimic C structs but also provide convenient dictionary-like | ||||
functionality. | ||||
Instances can be initialized with a dictionary, a list of key=value pairs | ||||
or both. If both are present, the dictionary must come first. | ||||
Because Python classes provide direct assignment to their members, it's | ||||
easy to overwrite normal methods (S.copy = 1 would destroy access to | ||||
S.copy()). For this reason, all builtin method names are protected and | ||||
can't be assigned to. An attempt to do s.copy=1 or s['copy']=1 will raise | ||||
a KeyError exception. If you really want to, you can bypass this | ||||
protection by directly assigning to __dict__: s.__dict__['copy']=1 will | ||||
still work. Doing this will break functionality, though. As in most of | ||||
Python, namespace protection is weakly enforced, so feel free to shoot | ||||
yourself if you really want to. | ||||
Note that this class uses more memory and is *much* slower than a regular | ||||
dictionary, so be careful in situations where memory or performance are | ||||
critical. But for day to day use it should behave fine. It is particularly | ||||
convenient for storing configuration data in programs. | ||||
+,+=,- and -= are implemented. +/+= do merges (non-destructive updates), | ||||
-/-= remove keys from the original. See the method descripitions. | ||||
This class allows a quick access syntax: both s.key and s['key'] are | ||||
valid. This syntax has a limitation: each 'key' has to be explicitly | ||||
accessed by its original name. The normal s.key syntax doesn't provide | ||||
access to the keys via variables whose values evaluate to the desired | ||||
keys. An example should clarify this: | ||||
Define a dictionary and initialize both with dict and k=v pairs: | ||||
>>> d={'a':1,'b':2} | ||||
>>> s=Struct(d,hi=10,ho=20) | ||||
Fernando Perez
|
r1280 | |||
fperez
|
r0 | The return of __repr__ can be used to create a new instance: | ||
>>> s | ||||
Fernando Perez
|
r1280 | Struct({'__allownew': True, 'a': 1, 'b': 2, 'hi': 10, 'ho': 20}) | ||
Note: the special '__allownew' key is used for internal purposes. | ||||
fperez
|
r0 | __str__ (called by print) shows it's not quite a regular dictionary: | ||
>>> print s | ||||
Fernando Perez
|
r1280 | Struct({'__allownew': True, 'a': 1, 'b': 2, 'hi': 10, 'ho': 20}) | ||
fperez
|
r0 | Access by explicitly named key with dot notation: | ||
>>> s.a | ||||
1 | ||||
Fernando Perez
|
r1280 | |||
fperez
|
r0 | Or like a dictionary: | ||
>>> s['a'] | ||||
1 | ||||
Fernando Perez
|
r1280 | |||
fperez
|
r0 | If you want a variable to hold the key value, only dictionary access works: | ||
>>> key='hi' | ||||
>>> s.key | ||||
Traceback (most recent call last): | ||||
File "<stdin>", line 1, in ? | ||||
AttributeError: Struct instance has no attribute 'key' | ||||
Fernando Perez
|
r1280 | |||
fperez
|
r0 | >>> s[key] | ||
10 | ||||
Another limitation of the s.key syntax (and Struct(key=val) | ||||
initialization): keys can't be numbers. But numeric keys can be used and | ||||
accessed using the dictionary syntax. Again, an example: | ||||
Fernando Perez
|
r1435 | This doesn't work (prompt changed to avoid confusing the test system): | ||
->> s=Struct(4='hi') | ||||
Fernando Perez
|
r1280 | Traceback (most recent call last): | ||
... | ||||
fperez
|
r0 | SyntaxError: keyword can't be an expression | ||
Fernando Perez
|
r1280 | |||
fperez
|
r0 | But this does: | ||
>>> s=Struct() | ||||
>>> s[4]='hi' | ||||
>>> s | ||||
Fernando Perez
|
r1280 | Struct({4: 'hi', '__allownew': True}) | ||
fperez
|
r0 | >>> s[4] | ||
'hi' | ||||
""" | ||||
# Attributes to which __setitem__ and __setattr__ will block access. | ||||
# Note: much of this will be moot in Python 2.2 and will be done in a much | ||||
# cleaner way. | ||||
__protected = ('copy dict dictcopy get has_attr has_key items keys ' | ||||
'merge popitem setdefault update values ' | ||||
'__make_dict __dict_invert ').split() | ||||
def __init__(self,dict=None,**kw): | ||||
"""Initialize with a dictionary, another Struct, or by giving | ||||
explicitly the list of attributes. | ||||
Both can be used, but the dictionary must come first: | ||||
Struct(dict), Struct(k1=v1,k2=v2) or Struct(dict,k1=v1,k2=v2). | ||||
""" | ||||
vivainio
|
r463 | self.__dict__['__allownew'] = True | ||
fperez
|
r0 | if dict is None: | ||
dict = {} | ||||
if isinstance(dict,Struct): | ||||
dict = dict.dict() | ||||
elif dict and type(dict) is not types.DictType: | ||||
raise TypeError,\ | ||||
'Initialize with a dictionary or key=val pairs.' | ||||
dict.update(kw) | ||||
# do the updating by hand to guarantee that we go through the | ||||
# safety-checked __setitem__ | ||||
for k,v in dict.items(): | ||||
self[k] = v | ||||
vivainio
|
r463 | |||
fperez
|
r0 | |||
def __setitem__(self,key,value): | ||||
"""Used when struct[key] = val calls are made.""" | ||||
if key in Struct.__protected: | ||||
raise KeyError,'Key '+`key`+' is a protected key of class Struct.' | ||||
vivainio
|
r463 | if not self['__allownew'] and key not in self.__dict__: | ||
raise KeyError( | ||||
"Can't create unknown attribute %s - Check for typos, or use allow_new_attr to create new attributes!" % | ||||
key) | ||||
fperez
|
r0 | self.__dict__[key] = value | ||
def __setattr__(self, key, value): | ||||
"""Used when struct.key = val calls are made.""" | ||||
self.__setitem__(key,value) | ||||
def __str__(self): | ||||
"""Gets called by print.""" | ||||
vivainio
|
r464 | return 'Struct('+ pprint.pformat(self.__dict__)+')' | ||
fperez
|
r0 | |||
def __repr__(self): | ||||
"""Gets called by repr. | ||||
A Struct can be recreated with S_new=eval(repr(S_old)).""" | ||||
vivainio
|
r464 | return self.__str__() | ||
fperez
|
r0 | |||
def __getitem__(self,key): | ||||
"""Allows struct[key] access.""" | ||||
return self.__dict__[key] | ||||
def __contains__(self,key): | ||||
Fernando Perez
|
r1759 | """Allows use of the 'in' operator. | ||
Examples: | ||||
>>> s = Struct(x=1) | ||||
>>> 'x' in s | ||||
True | ||||
>>> 'y' in s | ||||
False | ||||
>>> s[4] = None | ||||
>>> 4 in s | ||||
True | ||||
>>> s.z = None | ||||
>>> 'z' in s | ||||
True | ||||
""" | ||||
return key in self.__dict__ | ||||
fperez
|
r0 | |||
def __iadd__(self,other): | ||||
"""S += S2 is a shorthand for S.merge(S2).""" | ||||
self.merge(other) | ||||
return self | ||||
def __add__(self,other): | ||||
"""S + S2 -> New Struct made form S and S.merge(S2)""" | ||||
Sout = self.copy() | ||||
Sout.merge(other) | ||||
return Sout | ||||
def __sub__(self,other): | ||||
"""Return S1-S2, where all keys in S2 have been deleted (if present) | ||||
from S1.""" | ||||
Sout = self.copy() | ||||
Sout -= other | ||||
return Sout | ||||
def __isub__(self,other): | ||||
"""Do in place S = S - S2, meaning all keys in S2 have been deleted | ||||
(if present) from S1.""" | ||||
for k in other.keys(): | ||||
if self.has_key(k): | ||||
del self.__dict__[k] | ||||
def __make_dict(self,__loc_data__,**kw): | ||||
"Helper function for update and merge. Return a dict from data." | ||||
if __loc_data__ == None: | ||||
dict = {} | ||||
elif type(__loc_data__) is types.DictType: | ||||
dict = __loc_data__ | ||||
elif isinstance(__loc_data__,Struct): | ||||
dict = __loc_data__.__dict__ | ||||
else: | ||||
raise TypeError, 'Update with a dict, a Struct or key=val pairs.' | ||||
if kw: | ||||
dict.update(kw) | ||||
return dict | ||||
def __dict_invert(self,dict): | ||||
"""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 dict.items(): | ||||
if type(lst) is types.StringType: | ||||
lst = lst.split() | ||||
for entry in lst: | ||||
outdict[entry] = k | ||||
return outdict | ||||
def clear(self): | ||||
"""Clear all attributes.""" | ||||
self.__dict__.clear() | ||||
def copy(self): | ||||
"""Return a (shallow) copy of a Struct.""" | ||||
return Struct(self.__dict__.copy()) | ||||
def dict(self): | ||||
"""Return the Struct's dictionary.""" | ||||
return self.__dict__ | ||||
def dictcopy(self): | ||||
"""Return a (shallow) copy of the Struct's dictionary.""" | ||||
return self.__dict__.copy() | ||||
def popitem(self): | ||||
"""S.popitem() -> (k, v), remove and return some (key, value) pair as | ||||
a 2-tuple; but raise KeyError if S is empty.""" | ||||
return self.__dict__.popitem() | ||||
def update(self,__loc_data__=None,**kw): | ||||
"""Update (merge) with data from another Struct or from a dictionary. | ||||
Optionally, one or more key=value pairs can be given at the end for | ||||
direct update.""" | ||||
Fernando Perez
|
r1759 | # The funny name __loc_data__ is to prevent a common variable name | ||
# which could be a fieled of a Struct to collide with this | ||||
# parameter. The problem would arise if the function is called with a | ||||
# keyword with this same name that a user means to add as a Struct | ||||
# field. | ||||
fperez
|
r0 | newdict = Struct.__make_dict(self,__loc_data__,**kw) | ||
Fernando Perez
|
r1759 | for k,v in newdict.iteritems(): | ||
fperez
|
r0 | self[k] = v | ||
def merge(self,__loc_data__=None,__conflict_solve=None,**kw): | ||||
"""S.merge(data,conflict,k=v1,k=v2,...) -> merge data and k=v into S. | ||||
This is similar to 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 update method's | ||||
behavior). | ||||
conflict is a dictionary of binary functions which will be used to | ||||
solve key conflicts. It must have the following structure: | ||||
conflict == { fn1 : [Skey1,Skey2,...], fn2 : [Skey3], etc } | ||||
Values must be lists or whitespace separated strings which are | ||||
automatically converted to lists of strings by calling string.split(). | ||||
Each key of conflict is a function which defines a policy for | ||||
resolving conflicts when merging with the input data. Each fn must be | ||||
a binary function which returns the desired outcome for a key | ||||
conflict. These functions will be called as fn(old,new). | ||||
An example is probably in order. Suppose you are merging the struct S | ||||
with a dict D and the following conflict policy dict: | ||||
S.merge(D,{fn1:['a','b',4], fn2:'key_c key_d'}) | ||||
If the key 'a' is found in both S and D, the merge method will call: | ||||
S['a'] = fn1(S['a'],D['a']) | ||||
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 works for strings! | ||||
You can use those four words (as strings) as keys in conflict instead | ||||
of defining them as functions, and the merge method will substitute | ||||
the appropriate functions for you. That is, the call | ||||
S.merge(D,{'preserve':'a b c','add':[4,5,'d'],my_function:[6]}) | ||||
will automatically substitute the functions preserve and add for the | ||||
names 'preserve' and 'add' before making any function calls. | ||||
For more complicated conflict resolution policies, you still need to | ||||
construct your own functions. """ | ||||
data_dict = Struct.__make_dict(self,__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 = list2dict2(self.keys(),default = 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), | ||||
Fernando Perez
|
r1280 | ('add',add), ('add_flip',add_flip), | ||
('add_s',add_s)]: | ||||
fperez
|
r0 | 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(Struct.__dict_invert(self,inv_conflict_solve_user)) | ||||
#print 'merge. conflict_solve: '; pprint(conflict_solve) # dbg | ||||
#print '*'*50,'in merger. conflict_solver:'; pprint(conflict_solve) | ||||
fperez
|
r5 | for key in data_dict: | ||
fperez
|
r0 | if key not in self: | ||
self[key] = data_dict[key] | ||||
else: | ||||
self[key] = conflict_solve[key](self[key],data_dict[key]) | ||||
def has_key(self,key): | ||||
"""Like has_key() dictionary method.""" | ||||
return self.__dict__.has_key(key) | ||||
def hasattr(self,key): | ||||
"""hasattr function available as a method. | ||||
Implemented like has_key, to make sure that all available keys in the | ||||
internal dictionary of the Struct appear also as attributes (even | ||||
numeric keys).""" | ||||
return self.__dict__.has_key(key) | ||||
def items(self): | ||||
"""Return the items in the Struct's dictionary, in the same format | ||||
as a call to {}.items().""" | ||||
return self.__dict__.items() | ||||
def keys(self): | ||||
"""Return the keys in the Struct's dictionary, in the same format | ||||
as a call to {}.keys().""" | ||||
return self.__dict__.keys() | ||||
def values(self,keys=None): | ||||
"""Return the values in the Struct's dictionary, in the same format | ||||
as a call to {}.values(). | ||||
Can be called with an optional argument keys, which must be a list or | ||||
tuple of keys. In this case it returns only the values corresponding | ||||
to those keys (allowing a form of 'slicing' for Structs).""" | ||||
if not keys: | ||||
return self.__dict__.values() | ||||
else: | ||||
ret=[] | ||||
for k in keys: | ||||
ret.append(self[k]) | ||||
return ret | ||||
def get(self,attr,val=None): | ||||
Fernando Perez
|
r1280 | """S.get(k[,d]) -> S[k] if k in S, else d. d defaults to None.""" | ||
fperez
|
r0 | try: | ||
return self[attr] | ||||
except KeyError: | ||||
return val | ||||
def setdefault(self,attr,val=None): | ||||
Fernando Perez
|
r1280 | """S.setdefault(k[,d]) -> S.get(k,d), also set S[k]=d if k not in S""" | ||
fperez
|
r0 | if not self.has_key(attr): | ||
self[attr] = val | ||||
return self.get(attr,val) | ||||
vivainio
|
r463 | |||
def allow_new_attr(self, allow = True): | ||||
""" Set whether new attributes can be created inside struct | ||||
Fernando Perez
|
r1280 | This can be used to catch typos by verifying that the attribute user | ||
tries to change already exists in this Struct. | ||||
vivainio
|
r463 | """ | ||
self['__allownew'] = allow | ||||
fperez
|
r0 | # end class Struct | ||