Added libbe/properties to make property management easier.
[be.git] / libbe / properties.py
1 # Bugs Everywhere - a distributed bugtracker
2 # Copyright (C) 2008 W. Trevor King <wking@drexel.edu>
3 #
4 #   This program is free software: you can redistribute it and/or modify
5 #   it under the terms of the GNU General Public License as published by
6 #   the Free Software Foundation, either version 3 of the License, or
7 #   (at your option) any later version.
8 #
9 #   This program is distributed in the hope that it will be useful,
10 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #   GNU General Public License for more details.
13 #
14 #   You should have received a copy of the GNU General Public License
15 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 """
18 This module provides a series of useful decorators for defining
19 various types of properties.  For example usage, consider the
20 unittests at the end of the module.
21
22 See
23   http://www.python.org/dev/peps/pep-0318/
24 and
25   http://www.phyast.pitt.edu/~micheles/python/documentation.html
26 for more information on decorators.
27 """
28
29 import unittest
30
31 class ValueCheckError (ValueError):
32     def __init__(self, name, value, allowed):
33         msg = "%s not in %s for %s" % (value, allowed, name)
34         ValueError.__init__(self, msg)
35         self.name = name
36         self.value = value
37         self.allowed = allowed
38
39 def Property(funcs):
40     """
41     End a chain of property decorators, returning a property.
42     """
43     args = {}
44     args["fget"] = funcs.get("fget", None)
45     args["fset"] = funcs.get("fset", None)
46     args["fdel"] = funcs.get("fdel", None)
47     args["doc"] = funcs.get("doc", None)
48     
49     #print "Creating a property with"
50     #for key, val in args.items(): print key, value
51     return property(**args)
52
53 def doc_property(doc=None):
54     """
55     Add a docstring to a chain of property decorators.
56     """
57     def decorator(funcs=None):
58         """
59         Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...}
60         or a function fn() returning such a dict.
61         """
62         if hasattr(funcs, "__call__"):
63             funcs = funcs() # convert from function-arg to dict
64         funcs["doc"] = doc
65         return funcs
66     return decorator
67
68 def local_property(name):
69     """
70     Define get/set access to per-parent-instance local storage.  Uses
71     ._<name>_value to store the value for a particular owner instance.
72     """
73     def decorator(funcs):
74         if hasattr(funcs, "__call__"):
75             funcs = funcs()
76         fget = funcs.get("fget", None)
77         fset = funcs.get("fset", None)
78         def _fget(self):
79             if fget is not None:
80                 fget(self)
81             value = getattr(self, "_%s_value" % name, None)
82             return value
83         def _fset(self, value):
84             setattr(self, "_%s_value" % name, value)
85             if fset is not None:
86                 fset(self, value)
87         funcs["fget"] = _fget
88         funcs["fset"] = _fset
89         funcs["name"] = name
90         return funcs
91     return decorator
92
93 def settings_property(name):
94     """
95     Similar to local_property, except where local_property stores the
96     value in instance._<name>_value, settings_property stores the
97     value in instance.settings[name].
98     """
99     def decorator(funcs):
100         if hasattr(funcs, "__call__"):
101             funcs = funcs()
102         fget = funcs.get("fget", None)
103         fset = funcs.get("fset", None)
104         def _fget(self):
105             if fget is not None:
106                 fget(self)
107             value = self.settings.get(name, None)
108             return value
109         def _fset(self, value):
110             self.settings[name] = value
111             if fset is not None:
112                 fset(self, value)
113         funcs["fget"] = _fget
114         funcs["fset"] = _fset
115         funcs["name"] = name
116         return funcs
117     return decorator
118
119 def defaulting_property(default=None, null=None):
120     """
121     Define a default value for get access to a property.
122     If the stored value is null, then default is returned.
123     """
124     def decorator(funcs):
125         if hasattr(funcs, "__call__"):
126             funcs = funcs()
127         fget = funcs.get("fget")
128         def _fget(self):
129             value = fget(self)
130             if value == null:
131                 return default
132             return value
133         funcs["fget"] = _fget
134         return funcs
135     return decorator
136
137 def checked_property(allowed=[]):
138     """
139     Define allowed values for get/set access to a property.
140     """
141     def decorator(funcs):
142         if hasattr(funcs, "__call__"):
143             funcs = funcs()
144         fget = funcs.get("fget")
145         fset = funcs.get("fset")
146         name = funcs.get("name", "<unknown>")
147         def _fget(self):
148             value = fget(self)
149             if value not in allowed:
150                 raise ValueCheckError(name, value, allowed)
151             return value
152         def _fset(self, value):
153             if value not in allowed:
154                 raise ValueCheckError(name, value, allowed)
155             fset(self, value)
156         funcs["fget"] = _fget
157         funcs["fset"] = _fset
158         return funcs
159     return decorator
160
161 def cached_property(generator, initVal=None):
162     """
163     Allow caching of values generated by generator(instance), where
164     instance is the instance to which this property belongs.  Uses
165     ._<name>_cache to store a cache flag for a particular owner
166     instance.  When the cache flag is True (or missing), the normal
167     value is returned.  Otherwise the generator is called (and it's
168     output stored) for every get.  The cache flag is missing on
169     initialization.  Particular instances may override by setting
170     their own flag.
171     
172     If caching is True, but the stored value == initVal, the parameter
173     is considered 'uninitialized', and the generator is called anyway.
174     """
175     def decorator(funcs):
176         if hasattr(funcs, "__call__"):
177             funcs = funcs()
178         fget = funcs.get("fget")
179         fset = funcs.get("fset")
180         name = funcs.get("name", "<unknown>")
181         def _fget(self):
182             cache = getattr(self, "_%s_cache" % name, True)
183             if cache == True:
184                 value = fget(self)
185             if cache == False or (cache == True and value == initVal):
186                 value = generator(self)
187                 fset(self, value)
188             return value
189         funcs["fget"] = _fget
190         return funcs
191     return decorator
192
193 def primed_property(primer, initVal=None):
194     """
195     Just like a generator_property, except that instead of returning a
196     new value and running fset to cache it, the primer performs some
197     background manipulation (e.g. loads data into instance.settings)
198     such that a _second_ pass through fget succeeds.
199
200     The 'cache' flag becomes a 'prime' flag, with priming taking place
201     whenever ._<name>_prime is True, or is False or missing and
202     value == initVal.
203     """
204     def decorator(funcs):
205         if hasattr(funcs, "__call__"):
206             funcs = funcs()
207         fget = funcs.get("fget")
208         name = funcs.get("name", "<unknown>")
209         def _fget(self):
210             prime = getattr(self, "_%s_prime" % name, False)
211             if prime == False:
212                 value = fget(self)
213             if prime == True or (prime == False and value == initVal):
214                 primer(self)
215                 value = fget(self)
216             return value
217         funcs["fget"] = _fget
218         return funcs
219     return decorator
220
221 def change_hook_property(hook):
222     """
223     Call the function hook(instance, old_value, new_value) whenever a
224     value different from the current value is set (instance is a a
225     reference to the class instance to which this property belongs).
226     This is useful for saving changes to disk, etc.
227     """
228     def decorator(funcs):
229         if hasattr(funcs, "__call__"):
230             funcs = funcs()
231         fget = funcs.get("fget")
232         fset = funcs.get("fset")
233         name = funcs.get("name", "<unknown>")
234         def _fset(self, value):
235             old_value = fget(self)
236             if value != old_value:
237                 hook(self, old_value, value)
238             fset(self, value)
239         funcs["fset"] = _fset
240         return funcs
241     return decorator
242
243
244 class DecoratorTests(unittest.TestCase):
245     def testLocalDoc(self):
246         class Test(object):
247             @Property
248             @doc_property("A fancy property")
249             def x():
250                 return {}
251         self.failUnless(Test.x.__doc__ == "A fancy property",
252                         Test.x.__doc__)
253     def testLocalProperty(self):
254         class Test(object):
255             @Property
256             @local_property(name="LOCAL")
257             def x():
258                 return {}
259         t = Test()
260         self.failUnless(t.x == None, str(t.x))
261         t.x = 'z' # the first set initializes ._LOCAL_value
262         self.failUnless(t.x == 'z', str(t.x))
263         self.failUnless("_LOCAL_value" in dir(t), dir(t))
264         self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value)
265     def testSettingsProperty(self):
266         class Test(object):
267             @Property
268             @settings_property(name="attr")
269             def x():
270                 return {}
271             def __init__(self):
272                 self.settings = {}
273         t = Test()
274         self.failUnless(t.x == None, str(t.x))
275         t.x = 'z' # the first set initializes ._LOCAL_value
276         self.failUnless(t.x == 'z', str(t.x))
277         self.failUnless("attr" in t.settings, t.settings)
278         self.failUnless(t.settings["attr"] == 'z', t.settings["attr"])
279     def testDefaultingLocalProperty(self):
280         class Test(object):
281             @Property
282             @defaulting_property(default='y', null='x')
283             @local_property(name="DEFAULT")
284             def x(): return {}
285         t = Test()
286         self.failUnless(t.x == None, str(t.x)) 
287         t.x = 'x'
288         self.failUnless(t.x == 'y', str(t.x))
289         t.x = 'y'
290         self.failUnless(t.x == 'y', str(t.x))
291         t.x = 'z'
292         self.failUnless(t.x == 'z', str(t.x))
293     def testCheckedLocalProperty(self):
294         class Test(object):
295             @Property
296             @checked_property(allowed=['x', 'y', 'z'])
297             @local_property(name="CHECKED")
298             def x(): return {}
299             def __init__(self):
300                 self._CHECKED_value = 'x'
301         t = Test()
302         self.failUnless(t.x == 'x', str(t.x))
303         try:
304             t.x = None
305             e = None
306         except ValueCheckError, e:
307             pass
308         self.failUnless(type(e) == ValueCheckError, type(e))
309     def testTwoCheckedLocalProperties(self):
310         class Test(object):
311             @Property
312             @checked_property(allowed=['x', 'y', 'z'])
313             @local_property(name="X")
314             def x(): return {}
315
316             @Property
317             @checked_property(allowed=['a', 'b', 'c'])
318             @local_property(name="A")
319             def a(): return {}
320             def __init__(self):
321                 self._A_value = 'a'
322                 self._X_value = 'x'
323         t = Test()
324         try:
325             t.x = 'a'
326             e = None
327         except ValueCheckError, e:
328             pass
329         self.failUnless(type(e) == ValueCheckError, type(e))
330         t.x = 'x'
331         t.x = 'y'
332         t.x = 'z'
333         try:
334             t.a = 'x'
335             e = None
336         except ValueCheckError, e:
337             pass
338         self.failUnless(type(e) == ValueCheckError, type(e))
339         t.a = 'a'
340         t.a = 'b'
341         t.a = 'c'
342     def testCachedLocalProperty(self):
343         class Gen(object):
344             def __init__(self):
345                 self.i = 0
346             def __call__(self, owner):
347                 self.i += 1
348                 return self.i
349         class Test(object):
350             @Property
351             @cached_property(generator=Gen(), initVal=None)
352             @local_property(name="CACHED")
353             def x(): return {}
354         t = Test()
355         self.failIf("_CACHED_cache" in dir(t), getattr(t, "_CACHED_cache", None))
356         self.failUnless(t.x == 1, t.x)
357         self.failUnless(t.x == 1, t.x)
358         self.failUnless(t.x == 1, t.x)
359         t.x = 8
360         self.failUnless(t.x == 8, t.x)
361         self.failUnless(t.x == 8, t.x)
362         t._CACHED_cache = False
363         val = t.x
364         self.failUnless(val == 2, val)
365         val = t.x
366         self.failUnless(val == 3, val)
367         val = t.x
368         self.failUnless(val == 4, val)
369         t._CACHED_cache = True
370         self.failUnless(t.x == 4, str(t.x))
371         self.failUnless(t.x == 4, str(t.x))
372         self.failUnless(t.x == 4, str(t.x))
373     def testPrimedLocalProperty(self):
374         class Test(object):
375             def prime(self):
376                 self.settings["PRIMED"] = "initialized"
377             @Property
378             @primed_property(primer=prime, initVal=None)
379             @settings_property(name="PRIMED")
380             def x(): return {}
381             def __init__(self):
382                 self.settings={}
383         t = Test()
384         self.failIf("_PRIMED_prime" in dir(t), getattr(t, "_PRIMED_prime", None))
385         self.failUnless(t.x == "initialized", t.x)
386         t.x = 1
387         self.failUnless(t.x == 1, t.x)
388         t.x = None
389         self.failUnless(t.x == "initialized", t.x)
390         t._PRIMED_prime = True
391         t.x = 3
392         self.failUnless(t.x == "initialized", t.x)
393         t._PRIMED_prime = False
394         t.x = 3
395         self.failUnless(t.x == 3, t.x)
396     def testChangeHookLocalProperty(self):
397         class Test(object):
398             def _hook(self, old, new):
399                 self.old = old
400                 self.new = new
401
402             @Property
403             @change_hook_property(_hook)
404             @local_property(name="HOOKED")
405             def x(): return {}
406         t = Test()
407         t.x = 1
408         self.failUnless(t.old == None, t.old)
409         self.failUnless(t.new == 1, t.new)
410         t.x = 1
411         self.failUnless(t.old == None, t.old)
412         self.failUnless(t.new == 1, t.new)
413         t.x = 2
414         self.failUnless(t.old == 1, t.old)
415         self.failUnless(t.new == 2, t.new)
416
417 suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests)
418