0
|
1 """Persistent Tree Dictionaries
|
|
2
|
|
3 Incidentally, this is also the Hiss Preferences System. However, PTD is
|
|
4 expected to be usable outside of Hiss."""
|
|
5
|
|
6 __author__ = "David McClosky <dmcc@bigasterisk.com>, " + \
|
|
7 "Drew Perttula <drewp@bigasterisk.com>"
|
|
8 __cvsid__ = "$Id: TreeDict.py,v 1.1 2003/07/06 08:33:06 dmcc Exp $"
|
|
9 __version__ = "$Revision: 1.1 $"[11:-2]
|
|
10
|
|
11 try:
|
|
12 # use gnosis's XML pickler if available
|
|
13 import gnosis.xml.pickle as pickle
|
|
14
|
|
15 # this hack is needed or else xml.pickle will not be able to restore
|
|
16 # methods (they will be restored as mysterious classes which contain
|
|
17 # the same attributes but no methods,
|
|
18 # gnosis.xml.pickle.util._util.originalclassname, to be specific)
|
|
19 # the important thing to get from this comment is that we cannot properly
|
|
20 # pickle classes without them being "assigned" into gnosis.xml.pickle.
|
|
21 # i hope this gets fixed in a later version of pickle.
|
|
22 except ImportError:
|
|
23 # fallback to standard library pickle
|
|
24 import pickle
|
|
25
|
|
26 def allow_class_to_be_pickled(class_obj):
|
|
27 """to be documented"""
|
|
28 name = class_obj.__name__
|
|
29 setattr(pickle, name, class_obj)
|
|
30
|
|
31 class TreeDict(dict):
|
|
32 """TreeDict is the workhorse for the preferences system. It allows
|
|
33 for simple creation and access of preference trees. It could be
|
|
34 used to store anything, though."""
|
|
35 def __getattr__(self, attr):
|
|
36 """Gets an attribute/item, but with a twist. If not present, the
|
|
37 attribute will be created, with the value set to a new TreeDict."""
|
|
38 if attr.startswith('__'): # if it's special, we let dict handle it
|
|
39 return self.dictgetattr(attr)
|
|
40 if attr in self:
|
|
41 return dict.__getitem__(self, attr)
|
|
42 else:
|
|
43 newtree = self.__class__()
|
|
44 self.set_parent_attrs(newtree, attr) # record ourselves as the
|
|
45 # parent
|
|
46 dict.__setitem__(self, attr, newtree)
|
|
47 return newtree
|
|
48 def __setattr__(self, attr, newval):
|
|
49 """Sets an attribute/item to a new value."""
|
|
50 if attr.startswith('__'): # if it's special, we let dict handle it
|
|
51 return dict.__setattr__(self, attr, newval)
|
|
52 else:
|
|
53 oldval = self[attr] or None
|
|
54 dict.__setitem__(self, attr, newval)
|
|
55 if isinstance(newval, self.__class__):
|
|
56 self.set_parent_attrs(newval, attr)
|
|
57 self.changed_callback(attr, oldval, newval)
|
|
58 def __delattr__(self, attr):
|
|
59 """Deletes an attribute/item"""
|
|
60 if attr.startswith('__'): # if it's special, we let dict handle it
|
|
61 return dict.__delattr__(self, attr)
|
|
62 else: # otherwise, they meant a normal attribute/item
|
|
63 dict.__delitem__(self, attr)
|
|
64
|
|
65 # attr and item access are now the same
|
|
66 __getitem__ = __getattr__
|
|
67 __setitem__ = __setattr__
|
|
68 __delitem__ = __delattr__
|
|
69
|
|
70 # Original getattr/setattr for storing special attributes
|
|
71 def dictgetattr(self, attr):
|
|
72 """dict's original __getattribute__ method. This is useful for
|
|
73 storing bookkeeping information (like parents) which shouldn't
|
|
74 be persistent."""
|
|
75 return dict.__getattribute__(self, attr)
|
|
76 def dictsetattr(self, attr, newval):
|
|
77 """dict's original __setattr__ method. This is useful for
|
|
78 storing bookkeeping information (like parents) which shouldn't
|
|
79 be persistent."""
|
|
80 return dict.__setattr__(self, attr, newval)
|
|
81 def set_parent_attrs(self, newtree, key):
|
|
82 """Set ourselves as the parent to a new key."""
|
|
83 # record that we are the parent of this new object
|
|
84 newtree.dictsetattr('__parent', self)
|
|
85
|
|
86 # we store the key that newtree is in
|
|
87 newtree.dictsetattr('__parent_key', key)
|
|
88 def get_path(self):
|
|
89 """Returns the path to this TreeDict."""
|
|
90 try:
|
|
91 parent = self.dictgetattr('__parent')
|
|
92 key = self.dictgetattr('__parent_key')
|
|
93 return parent.get_path() + [key]
|
|
94 except AttributeError:
|
|
95 return []
|
|
96
|
|
97 def changed_callback(self, attr, oldval, newval):
|
|
98 """Called whenever an attribute is changed. It will be called with
|
|
99 three arguments: the attribute that changed, the previous value of
|
|
100 the attribute, and the newvalue of the attribute.
|
|
101 If the attribute didn't exist before, the old value will be None.
|
|
102
|
|
103 This should be overridden by subclasses of TreeDict."""
|
|
104
|
|
105 # Tree operations
|
|
106 def tree_update(self, othertree):
|
|
107 """Recursive update(). All keys from the othertree are copied over.
|
|
108 If both this tree and the other tree have a key which is a TreeDict,
|
|
109 we will recurse into it."""
|
|
110 for key in othertree: # if the key is in the other tree, merge it into
|
|
111 # ours
|
|
112 if key in self and isinstance(self[key], self.__class__):
|
|
113 self[key].tree_update(othertree[key])
|
|
114 else: # copy othertree's branch to ours
|
|
115 self[key] = othertree[key]
|
|
116
|
|
117 # Loading and saving
|
|
118 def load(self, filename, clobber=1):
|
|
119 """Combine this TreeDict with the TreeDict previously save()d
|
|
120 to a file. If clobber=1, this tree will be clear()ed before
|
|
121 combining (this is the default)."""
|
|
122 newprefs = pickle.load(file(filename))
|
|
123 if clobber: self.clear()
|
|
124 self.tree_update(newprefs)
|
|
125 return self
|
|
126 def save(self, filename):
|
|
127 """Save this TreeDict to a file. You can later restore the TreeDict
|
|
128 by creating a TreeDict and using the load() method:
|
|
129
|
|
130 treedict.save("tree.xml")
|
|
131 newtreedict = TreeDict()
|
|
132 newtreedict.load("tree.xml")"""
|
|
133 pickle.dump(self, file(filename, 'w'))
|
|
134
|
|
135 def pprint(self, depth=0):
|
|
136 "A simple pretty printing method, useful for debugging"
|
|
137 for key in self:
|
|
138 print "%s%s =" % ('\t' * depth, key),
|
|
139 val = self[key]
|
|
140 if isinstance(val, self.__class__):
|
|
141 print
|
|
142 val.pprint(depth + 1)
|
|
143 else:
|
|
144 print repr(self[key])
|
|
145
|
|
146 allow_class_to_be_pickled(TreeDict)
|
|
147
|
|
148 if __name__ == "__main__":
|
|
149 # We subclass TreeDict here so we can demonstrate how to override
|
|
150 # changed_callback.
|
|
151 RealTreeDict = TreeDict
|
|
152 class TreeDict(RealTreeDict):
|
|
153 def changed_callback(self, attr, oldval, newval):
|
|
154 fullpath = self.get_path() + [attr]
|
|
155
|
|
156 if 0: # more information
|
|
157 print 'changed_callback %s: %s -> %s' % ('.'.join(fullpath),
|
|
158 oldval, newval)
|
|
159 else: # output looks like the Python code that built the tree
|
|
160 print 'changed_callback %s = %r' % ('.'.join(fullpath),
|
|
161 newval)
|
|
162
|
|
163 # store our new class so we can pickle :(
|
|
164 # maybe we give someone a list of classes that we use, and it handles
|
|
165 # the rest. or, we just record a list of classes seen using
|
|
166 # setattr callbacks.
|
|
167 allow_class_to_be_pickled(TreeDict)
|
|
168
|
|
169 True = 1
|
|
170 False = 0
|
|
171
|
|
172 defaults_tree = TreeDict()
|
|
173 defaults_tree.auto_scroll_to_bottom = True
|
|
174 defaults_tree.aging.enabled = True
|
|
175 defaults_tree.aging.fade_exponent = 3
|
|
176 defaults_tree.aging.user_colors = TreeDict({'user1' : 'color1',
|
|
177 'user2' : 'color2'})
|
|
178
|
|
179 import time
|
|
180 defaults_tree.current_time = time.asctime()
|
|
181
|
|
182 # on disk
|
|
183 new_tree = TreeDict()
|
|
184 new_tree.some_extra_pref = "hi mom"
|
|
185 new_tree.auto_scroll_to_bottom = False
|
|
186 new_tree.aging.user_colors.user1 = 'green'
|
|
187
|
|
188 defaults_tree.tree_update(new_tree)
|
|
189 defaults_tree.pprint()
|
|
190
|
|
191 # test load / save
|
|
192 print "---"
|
|
193 defaults_tree.save("persistence_test.xml")
|
|
194 loaded_tree = TreeDict().load("persistence_test.xml")
|
|
195 loaded_tree.pprint()
|