1 # Bugs Everywhere - a distributed bugtracker
2 # Copyright (C) 2008-2012 Chris Ball <cjb@laptop.org>
3 # Gianluca Montecchi <gian@grys.it>
4 # W. Trevor King <wking@tremily.us>
6 # This file is part of Bugs Everywhere.
8 # Bugs Everywhere is free software: you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by the Free
10 # Software Foundation, either version 2 of the License, or (at your option) any
13 # Bugs Everywhere is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
18 # You should have received a copy of the GNU General Public License along with
19 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
21 """Provides a series of useful decorators for defining various types
24 For example usage, consider the unittests at the end of the module.
29 See `PEP 318` and Michele Simionato's `decorator documentation` for
30 more information on decorators.
32 .. _PEP 318: http://www.python.org/dev/peps/pep-0318/
33 .. _decorator documentation: http://www.phyast.pitt.edu/~micheles/python/documentation.html
37 :py:mod:`libbe.storage.util.settings_object` : bundle properties into a convenient package
45 if libbe.TESTING == True:
49 class ValueCheckError (ValueError):
50 def __init__(self, name, value, allowed):
51 action = "in" # some list of allowed values
52 if type(allowed) == types.FunctionType:
53 action = "allowed by" # some allowed-value check function
54 msg = "%s not %s %s for %s" % (value, action, allowed, name)
55 ValueError.__init__(self, msg)
58 self.allowed = allowed
62 End a chain of property decorators, returning a property.
65 args["fget"] = funcs.get("fget", None)
66 args["fset"] = funcs.get("fset", None)
67 args["fdel"] = funcs.get("fdel", None)
68 args["doc"] = funcs.get("doc", None)
70 #print "Creating a property with"
71 #for key, val in args.items(): print key, value
72 return property(**args)
74 def doc_property(doc=None):
76 Add a docstring to a chain of property decorators.
78 def decorator(funcs=None):
80 Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...}
81 or a function fn() returning such a dict.
83 if hasattr(funcs, "__call__"):
84 funcs = funcs() # convert from function-arg to dict
89 def local_property(name, null=None, mutable_null=False):
91 Define get/set access to per-parent-instance local storage. Uses
92 ._<name>_value to store the value for a particular owner instance.
93 If the ._<name>_value attribute does not exist, returns null.
95 If mutable_null == True, we only release deepcopies of the null to
99 if hasattr(funcs, "__call__"):
101 fget = funcs.get("fget", None)
102 fset = funcs.get("fset", None)
106 if mutable_null == True:
107 ret_null = copy.deepcopy(null)
110 value = getattr(self, "_%s_value" % name, ret_null)
112 def _fset(self, value):
113 setattr(self, "_%s_value" % name, value)
116 funcs["fget"] = _fget
117 funcs["fset"] = _fset
122 def settings_property(name, null=None):
124 Similar to local_property, except where local_property stores the
125 value in instance._<name>_value, settings_property stores the
126 value in instance.settings[name].
128 def decorator(funcs):
129 if hasattr(funcs, "__call__"):
131 fget = funcs.get("fget", None)
132 fset = funcs.get("fset", None)
136 value = self.settings.get(name, null)
138 def _fset(self, value):
139 self.settings[name] = value
142 funcs["fget"] = _fget
143 funcs["fset"] = _fset
149 # Allow comparison and caching with _original_ values for mutables,
159 def _hash_mutable_value(value):
161 def _init_mutable_property_cache(self):
162 if not hasattr(self, "_mutable_property_cache_hash"):
163 # first call to _fget for any mutable property
164 self._mutable_property_cache_hash = {}
165 self._mutable_property_cache_copy = {}
166 def _set_cached_mutable_property(self, cacher_name, property_name, value):
167 _init_mutable_property_cache(self)
168 self._mutable_property_cache_hash[(cacher_name, property_name)] = \
169 _hash_mutable_value(value)
170 self._mutable_property_cache_copy[(cacher_name, property_name)] = \
172 def _get_cached_mutable_property(self, cacher_name, property_name, default=None):
173 _init_mutable_property_cache(self)
174 if (cacher_name, property_name) not in self._mutable_property_cache_copy:
176 return self._mutable_property_cache_copy[(cacher_name, property_name)]
177 def _cmp_cached_mutable_property(self, cacher_name, property_name, value, default=None):
178 _init_mutable_property_cache(self)
179 if (cacher_name, property_name) not in self._mutable_property_cache_hash:
180 _set_cached_mutable_property(self, cacher_name, property_name, default)
181 old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)]
182 return cmp(_hash_mutable_value(value), old_hash)
185 def defaulting_property(default=None, null=None,
186 mutable_default=False):
188 Define a default value for get access to a property.
189 If the stored value is null, then default is returned.
191 If mutable_default == True, we only release deepcopies of the
192 default to the outside world.
194 null should never escape to the outside world, so don't worry
195 about it being a mutable.
197 def decorator(funcs):
198 if hasattr(funcs, "__call__"):
200 fget = funcs.get("fget")
201 fset = funcs.get("fset")
202 name = funcs.get("name", "<unknown>")
206 if mutable_default == True:
207 return copy.deepcopy(default)
211 def _fset(self, value):
215 funcs["fget"] = _fget
216 funcs["fset"] = _fset
220 def fn_checked_property(value_allowed_fn):
222 Define allowed values for get/set access to a property.
224 def decorator(funcs):
225 if hasattr(funcs, "__call__"):
227 fget = funcs.get("fget")
228 fset = funcs.get("fset")
229 name = funcs.get("name", "<unknown>")
232 if value_allowed_fn(value) != True:
233 raise ValueCheckError(name, value, value_allowed_fn)
235 def _fset(self, value):
236 if value_allowed_fn(value) != True:
237 raise ValueCheckError(name, value, value_allowed_fn)
239 funcs["fget"] = _fget
240 funcs["fset"] = _fset
244 def checked_property(allowed=[]):
246 Define allowed values for get/set access to a property.
248 def decorator(funcs):
249 if hasattr(funcs, "__call__"):
251 fget = funcs.get("fget")
252 fset = funcs.get("fset")
253 name = funcs.get("name", "<unknown>")
256 if value not in allowed:
257 raise ValueCheckError(name, value, allowed)
259 def _fset(self, value):
260 if value not in allowed:
261 raise ValueCheckError(name, value, allowed)
263 funcs["fget"] = _fget
264 funcs["fset"] = _fset
268 def cached_property(generator, initVal=None, mutable=False):
270 Allow caching of values generated by generator(instance), where
271 instance is the instance to which this property belongs. Uses
272 ._<name>_cache to store a cache flag for a particular owner
275 When the cache flag is True or missing and the stored value is
276 initVal, the first fget call triggers the generator function,
277 whose output is stored in _<name>_cached_value. That and
278 subsequent calls to fget will return this cached value.
280 If the input value is no longer initVal (e.g. a value has been
281 loaded from disk or set with fset), that value overrides any
282 cached value, and this property has no effect.
284 When the cache flag is False and the stored value is initVal, the
285 generator is not cached, but is called on every fget.
287 The cache flag is missing on initialization. Particular instances
288 may override by setting their own flag.
290 In the case that mutable == True, all caching is disabled and the
291 generator is called whenever the cached value would otherwise be
294 def decorator(funcs):
295 if hasattr(funcs, "__call__"):
297 fget = funcs.get("fget")
298 name = funcs.get("name", "<unknown>")
300 cache = getattr(self, "_%s_cache" % name, True)
303 if cache == True and mutable == False:
304 if hasattr(self, "_%s_cached_value" % name):
305 value = getattr(self, "_%s_cached_value" % name)
307 value = generator(self)
308 setattr(self, "_%s_cached_value" % name, value)
310 value = generator(self)
312 funcs["fget"] = _fget
316 def primed_property(primer, initVal=None, unprimeableVal=None):
318 Just like a cached_property, except that instead of returning a
319 new value and running fset to cache it, the primer attempts some
320 background manipulation (e.g. loads data into instance.settings)
321 such that a _second_ pass through fget succeeds. If the second
322 pass doesn't succeed (e.g. no readable storage), we give up and
323 return unprimeableVal.
325 The 'cache' flag becomes a 'prime' flag, with priming taking place
326 whenever ._<name>_prime is True, or is False or missing and
329 def decorator(funcs):
330 if hasattr(funcs, "__call__"):
332 fget = funcs.get("fget")
333 name = funcs.get("name", "<unknown>")
335 prime = getattr(self, "_%s_prime" % name, False)
338 if prime == True or (prime == False and value == initVal):
341 if prime == False and value == initVal:
342 return unprimeableVal
344 funcs["fget"] = _fget
348 def change_hook_property(hook, mutable=False, default=None):
349 """Call the function `hook` whenever a value different from the
350 current value is set.
352 This is useful for saving changes to disk, etc. This function is
353 called *after* the new value has been stored, allowing you to
354 change the stored value if you want.
356 In the case of mutables, things are slightly trickier. Because
357 the property-owning class has no way of knowing when the value
358 changes. We work around this by caching a private deepcopy of the
359 mutable value, and checking for changes whenever the property is
360 set (obviously) or retrieved (to check for external changes). So
361 long as you're conscientious about accessing the property after
362 making external modifications, mutability won't be a problem::
364 t.x.append(5) # external modification
365 t.x # dummy access notices change and triggers hook
367 See :py:class:`testChangeHookMutableProperty` for an example of the
373 `hook(instance, old_value, new_value)`, where `instance` is a
374 reference to the class instance to which this property belongs.
376 def decorator(funcs):
377 if hasattr(funcs, "__call__"):
379 fget = funcs.get("fget")
380 fset = funcs.get("fset")
381 name = funcs.get("name", "<unknown>")
382 def _fget(self, new_value=None, from_fset=False): # only used if mutable == True
383 if from_fset == True:
384 value = new_value # compare new value with cached
386 value = fget(self) # compare current value with cached
387 if _cmp_cached_mutable_property(self, "change hook property", name, value, default) != 0:
388 # there has been a change, cache new value
389 old_value = _get_cached_mutable_property(self, "change hook property", name, default)
390 _set_cached_mutable_property(self, "change hook property", name, value)
391 if from_fset == True: # return previously cached value
393 else: # the value changed while we weren't looking
394 hook(self, old_value, value)
396 def _fset(self, value):
397 if mutable == True: # get cached previous value
398 old_value = _fget(self, new_value=value, from_fset=True)
400 old_value = fget(self)
402 if value != old_value:
403 hook(self, old_value, value)
405 funcs["fget"] = _fget
406 funcs["fset"] = _fset
410 if libbe.TESTING == True:
411 class DecoratorTests(unittest.TestCase):
412 def testLocalDoc(self):
415 @doc_property("A fancy property")
418 self.failUnless(Test.x.__doc__ == "A fancy property",
420 def testLocalProperty(self):
423 @local_property(name="LOCAL")
427 self.failUnless(t.x == None, str(t.x))
428 t.x = 'z' # the first set initializes ._LOCAL_value
429 self.failUnless(t.x == 'z', str(t.x))
430 self.failUnless("_LOCAL_value" in dir(t), dir(t))
431 self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value)
432 def testSettingsProperty(self):
435 @settings_property(name="attr")
441 self.failUnless(t.x == None, str(t.x))
442 t.x = 'z' # the first set initializes ._LOCAL_value
443 self.failUnless(t.x == 'z', str(t.x))
444 self.failUnless("attr" in t.settings, t.settings)
445 self.failUnless(t.settings["attr"] == 'z', t.settings["attr"])
446 def testDefaultingLocalProperty(self):
449 @defaulting_property(default='y', null='x')
450 @local_property(name="DEFAULT", null=5)
453 self.failUnless(t.x == 5, str(t.x))
455 self.failUnless(t.x == 'y', str(t.x))
457 self.failUnless(t.x == 'y', str(t.x))
459 self.failUnless(t.x == 'z', str(t.x))
461 self.failUnless(t.x == 5, str(t.x))
462 def testCheckedLocalProperty(self):
465 @checked_property(allowed=['x', 'y', 'z'])
466 @local_property(name="CHECKED")
469 self._CHECKED_value = 'x'
471 self.failUnless(t.x == 'x', str(t.x))
475 except ValueCheckError, e:
477 self.failUnless(type(e) == ValueCheckError, type(e))
478 def testTwoCheckedLocalProperties(self):
481 @checked_property(allowed=['x', 'y', 'z'])
482 @local_property(name="X")
486 @checked_property(allowed=['a', 'b', 'c'])
487 @local_property(name="A")
496 except ValueCheckError, e:
498 self.failUnless(type(e) == ValueCheckError, type(e))
505 except ValueCheckError, e:
507 self.failUnless(type(e) == ValueCheckError, type(e))
511 def testFnCheckedLocalProperty(self):
514 @fn_checked_property(lambda v : v in ['x', 'y', 'z'])
515 @local_property(name="CHECKED")
518 self._CHECKED_value = 'x'
520 self.failUnless(t.x == 'x', str(t.x))
524 except ValueCheckError, e:
526 self.failUnless(type(e) == ValueCheckError, type(e))
527 def testCachedLocalProperty(self):
531 def __call__(self, owner):
536 @cached_property(generator=Gen(), initVal=None)
537 @local_property(name="CACHED")
540 self.failIf("_CACHED_cache" in dir(t),
541 getattr(t, "_CACHED_cache", None))
542 self.failUnless(t.x == 1, t.x)
543 self.failUnless(t.x == 1, t.x)
544 self.failUnless(t.x == 1, t.x)
546 self.failUnless(t.x == 8, t.x)
547 self.failUnless(t.x == 8, t.x)
548 t._CACHED_cache = False # Caching is off, but the stored value
549 val = t.x # is 8, not the initVal (None), so we
550 self.failUnless(val == 8, val) # get 8.
551 t._CACHED_value = None # Now we've set the stored value to None
552 val = t.x # so future calls to fget (like this)
553 self.failUnless(val == 2, val) # will call the generator every time...
555 self.failUnless(val == 3, val)
557 self.failUnless(val == 4, val)
558 t._CACHED_cache = True # We turn caching back on, and get
559 self.failUnless(t.x == 1, str(t.x)) # the original cached value.
560 del t._CACHED_cached_value # Removing that value forces a
561 self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call
562 self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which
563 self.failUnless(t.x == 5, str(t.x)) # we get the new cached value.
564 def testPrimedLocalProperty(self):
567 self.settings["PRIMED"] = self.primeVal
569 @primed_property(primer=prime, initVal=None, unprimeableVal=2)
570 @settings_property(name="PRIMED")
574 self.primeVal = "initialized"
576 self.failIf("_PRIMED_prime" in dir(t),
577 getattr(t, "_PRIMED_prime", None))
578 self.failUnless(t.x == "initialized", t.x)
580 self.failUnless(t.x == 1, t.x)
582 self.failUnless(t.x == "initialized", t.x)
583 t._PRIMED_prime = True
585 self.failUnless(t.x == "initialized", t.x)
586 t._PRIMED_prime = False
588 self.failUnless(t.x == 3, t.x)
592 self.failUnless(t.x == 2, t.x)
593 def testChangeHookLocalProperty(self):
595 def _hook(self, old, new):
600 @change_hook_property(_hook)
601 @local_property(name="HOOKED")
605 self.failUnless(t.old == None, t.old)
606 self.failUnless(t.new == 1, t.new)
608 self.failUnless(t.old == None, t.old)
609 self.failUnless(t.new == 1, t.new)
611 self.failUnless(t.old == 1, t.old)
612 self.failUnless(t.new == 2, t.new)
613 def testChangeHookMutableProperty(self):
615 def _hook(self, old, new):
621 @change_hook_property(_hook, mutable=True)
622 @local_property(name="HOOKED")
627 self.failUnless(t.old == None, t.old)
628 self.failUnless(t.new == [], t.new)
629 self.failUnless(t.hook_calls == 1, t.hook_calls)
633 self.failUnless(t.old == [], t.old)
634 self.failUnless(t.new == [5], t.new)
635 self.failUnless(t.hook_calls == 2, t.hook_calls)
637 self.failUnless(t.old == [5], t.old)
638 self.failUnless(t.new == [], t.new)
639 self.failUnless(t.hook_calls == 3, t.hook_calls)
640 # now append without reassigning. this doesn't trigger the
641 # change, since we don't ever set t.x, only get it and mess
642 # with it. It does, however, update our t.new, since t.new =
643 # t.x and is not a static copy.
645 self.failUnless(t.old == [5], t.old)
646 self.failUnless(t.new == [5], t.new)
647 self.failUnless(t.hook_calls == 3, t.hook_calls)
648 # however, the next t.x get _will_ notice the change...
650 self.failUnless(t.old == [], t.old)
651 self.failUnless(t.new == [5], t.new)
652 self.failUnless(t.hook_calls == 4, t.hook_calls)
653 t.x.append(6) # this append(6) is not noticed yet
654 self.failUnless(t.old == [], t.old)
655 self.failUnless(t.new == [5,6], t.new)
656 self.failUnless(t.hook_calls == 4, t.hook_calls)
657 # this append(7) is not noticed, but the t.x get causes the
658 # append(6) to be noticed
660 self.failUnless(t.old == [5], t.old)
661 self.failUnless(t.new == [5,6,7], t.new)
662 self.failUnless(t.hook_calls == 5, t.hook_calls)
663 a = t.x # now the append(7) is noticed
664 self.failUnless(t.old == [5,6], t.old)
665 self.failUnless(t.new == [5,6,7], t.new)
666 self.failUnless(t.hook_calls == 6, t.hook_calls)
668 suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests)