1 # Bugs Everywhere - a distributed bugtracker
2 # Copyright (C) 2008 W. Trevor King <wking@drexel.edu>
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.
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.
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/>.
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.
23 http://www.python.org/dev/peps/pep-0318/
25 http://www.phyast.pitt.edu/~micheles/python/documentation.html
26 for more information on decorators.
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)
37 self.allowed = allowed
41 End a chain of property decorators, returning a property.
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)
49 #print "Creating a property with"
50 #for key, val in args.items(): print key, value
51 return property(**args)
53 def doc_property(doc=None):
55 Add a docstring to a chain of property decorators.
57 def decorator(funcs=None):
59 Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...}
60 or a function fn() returning such a dict.
62 if hasattr(funcs, "__call__"):
63 funcs = funcs() # convert from function-arg to dict
68 def local_property(name):
70 Define get/set access to per-parent-instance local storage. Uses
71 ._<name>_value to store the value for a particular owner instance.
74 if hasattr(funcs, "__call__"):
76 fget = funcs.get("fget", None)
77 fset = funcs.get("fset", None)
81 value = getattr(self, "_%s_value" % name, None)
83 def _fset(self, value):
84 setattr(self, "_%s_value" % name, value)
93 def settings_property(name):
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].
100 if hasattr(funcs, "__call__"):
102 fget = funcs.get("fget", None)
103 fset = funcs.get("fset", None)
107 value = self.settings.get(name, None)
109 def _fset(self, value):
110 self.settings[name] = value
113 funcs["fget"] = _fget
114 funcs["fset"] = _fset
119 def defaulting_property(default=None, null=None):
121 Define a default value for get access to a property.
122 If the stored value is null, then default is returned.
124 def decorator(funcs):
125 if hasattr(funcs, "__call__"):
127 fget = funcs.get("fget")
133 funcs["fget"] = _fget
137 def checked_property(allowed=[]):
139 Define allowed values for get/set access to a property.
141 def decorator(funcs):
142 if hasattr(funcs, "__call__"):
144 fget = funcs.get("fget")
145 fset = funcs.get("fset")
146 name = funcs.get("name", "<unknown>")
149 if value not in allowed:
150 raise ValueCheckError(name, value, allowed)
152 def _fset(self, value):
153 if value not in allowed:
154 raise ValueCheckError(name, value, allowed)
156 funcs["fget"] = _fget
157 funcs["fset"] = _fset
161 def cached_property(generator, initVal=None):
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
172 If caching is True, but the stored value == initVal, the parameter
173 is considered 'uninitialized', and the generator is called anyway.
175 def decorator(funcs):
176 if hasattr(funcs, "__call__"):
178 fget = funcs.get("fget")
179 fset = funcs.get("fset")
180 name = funcs.get("name", "<unknown>")
182 cache = getattr(self, "_%s_cache" % name, True)
185 if cache == False or (cache == True and value == initVal):
186 value = generator(self)
189 funcs["fget"] = _fget
193 def primed_property(primer, initVal=None):
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.
200 The 'cache' flag becomes a 'prime' flag, with priming taking place
201 whenever ._<name>_prime is True, or is False or missing and
204 def decorator(funcs):
205 if hasattr(funcs, "__call__"):
207 fget = funcs.get("fget")
208 name = funcs.get("name", "<unknown>")
210 prime = getattr(self, "_%s_prime" % name, False)
213 if prime == True or (prime == False and value == initVal):
217 funcs["fget"] = _fget
221 def change_hook_property(hook):
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.
228 def decorator(funcs):
229 if hasattr(funcs, "__call__"):
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)
239 funcs["fset"] = _fset
244 class DecoratorTests(unittest.TestCase):
245 def testLocalDoc(self):
248 @doc_property("A fancy property")
251 self.failUnless(Test.x.__doc__ == "A fancy property",
253 def testLocalProperty(self):
256 @local_property(name="LOCAL")
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):
268 @settings_property(name="attr")
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):
282 @defaulting_property(default='y', null='x')
283 @local_property(name="DEFAULT")
286 self.failUnless(t.x == None, str(t.x))
288 self.failUnless(t.x == 'y', str(t.x))
290 self.failUnless(t.x == 'y', str(t.x))
292 self.failUnless(t.x == 'z', str(t.x))
293 def testCheckedLocalProperty(self):
296 @checked_property(allowed=['x', 'y', 'z'])
297 @local_property(name="CHECKED")
300 self._CHECKED_value = 'x'
302 self.failUnless(t.x == 'x', str(t.x))
306 except ValueCheckError, e:
308 self.failUnless(type(e) == ValueCheckError, type(e))
309 def testTwoCheckedLocalProperties(self):
312 @checked_property(allowed=['x', 'y', 'z'])
313 @local_property(name="X")
317 @checked_property(allowed=['a', 'b', 'c'])
318 @local_property(name="A")
327 except ValueCheckError, e:
329 self.failUnless(type(e) == ValueCheckError, type(e))
336 except ValueCheckError, e:
338 self.failUnless(type(e) == ValueCheckError, type(e))
342 def testCachedLocalProperty(self):
346 def __call__(self, owner):
351 @cached_property(generator=Gen(), initVal=None)
352 @local_property(name="CACHED")
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)
360 self.failUnless(t.x == 8, t.x)
361 self.failUnless(t.x == 8, t.x)
362 t._CACHED_cache = False
364 self.failUnless(val == 2, val)
366 self.failUnless(val == 3, val)
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):
376 self.settings["PRIMED"] = "initialized"
378 @primed_property(primer=prime, initVal=None)
379 @settings_property(name="PRIMED")
384 self.failIf("_PRIMED_prime" in dir(t), getattr(t, "_PRIMED_prime", None))
385 self.failUnless(t.x == "initialized", t.x)
387 self.failUnless(t.x == 1, t.x)
389 self.failUnless(t.x == "initialized", t.x)
390 t._PRIMED_prime = True
392 self.failUnless(t.x == "initialized", t.x)
393 t._PRIMED_prime = False
395 self.failUnless(t.x == 3, t.x)
396 def testChangeHookLocalProperty(self):
398 def _hook(self, old, new):
403 @change_hook_property(_hook)
404 @local_property(name="HOOKED")
408 self.failUnless(t.old == None, t.old)
409 self.failUnless(t.new == 1, t.new)
411 self.failUnless(t.old == None, t.old)
412 self.failUnless(t.new == 1, t.new)
414 self.failUnless(t.old == 1, t.old)
415 self.failUnless(t.new == 2, t.new)
417 suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests)