diff options
author | Chris Ball <cjb@laptop.org> | 2009-06-25 09:53:39 -0400 |
---|---|---|
committer | Chris Ball <cjb@laptop.org> | 2009-06-25 09:53:39 -0400 |
commit | ae52bd5946df9c0d59be43824b20d33819891f93 (patch) | |
tree | c4579ec93a80f5e93b4258e8771ae39405ce9ba7 /libbe/properties.py | |
parent | 1f746ca3f2f2745bfeae998186b4a427776359a2 (diff) | |
parent | eeaf13d7d9c5e6fcad4689c988d4fc1806426d3f (diff) | |
download | bugseverywhere-ae52bd5946df9c0d59be43824b20d33819891f93.tar.gz |
Merge with W. Trevor King's tree.
Diffstat (limited to 'libbe/properties.py')
-rw-r--r-- | libbe/properties.py | 173 |
1 files changed, 154 insertions, 19 deletions
diff --git a/libbe/properties.py b/libbe/properties.py index a8e89fb..956ecc3 100644 --- a/libbe/properties.py +++ b/libbe/properties.py @@ -26,12 +26,17 @@ and for more information on decorators. """ +import copy +import types import unittest class ValueCheckError (ValueError): def __init__(self, name, value, allowed): - msg = "%s not in %s for %s" % (value, allowed, name) + action = "in" # some list of allowed values + if type(allowed) == types.FunctionType: + action = "allowed by" # some allowed-value check function + msg = "%s not %s %s for %s" % (value, action, allowed, name) ValueError.__init__(self, msg) self.name = name self.value = value @@ -66,7 +71,7 @@ def doc_property(doc=None): return funcs return decorator -def local_property(name, null=None): +def local_property(name, null=None, mutable_null=False): """ Define get/set access to per-parent-instance local storage. Uses ._<name>_value to store the value for a particular owner instance. @@ -80,7 +85,11 @@ def local_property(name, null=None): def _fget(self): if fget is not None: fget(self) - value = getattr(self, "_%s_value" % name, null) + if mutable_null == True: + ret_null = copy.deepcopy(null) + else: + ret_null = null + value = getattr(self, "_%s_value" % name, ret_null) return value def _fset(self, value): setattr(self, "_%s_value" % name, value) @@ -118,7 +127,46 @@ def settings_property(name, null=None): return funcs return decorator -def defaulting_property(default=None, null=None): + +# Allow comparison and caching with _original_ values for mutables, +# since +# +# >>> a = [] +# >>> b = a +# >>> b.append(1) +# >>> a +# [1] +# >>> a==b +# True +def _hash_mutable_value(value): + return repr(value) +def _init_mutable_property_cache(self): + if not hasattr(self, "_mutable_property_cache_hash"): + # first call to _fget for any mutable property + self._mutable_property_cache_hash = {} + self._mutable_property_cache_copy = {} +def _set_cached_mutable_property(self, cacher_name, property_name, value): + _init_mutable_property_cache(self) + self._mutable_property_cache_hash[(cacher_name, property_name)] = \ + _hash_mutable_value(value) + self._mutable_property_cache_copy[(cacher_name, property_name)] = \ + copy.deepcopy(value) +def _get_cached_mutable_property(self, cacher_name, property_name, default=None): + _init_mutable_property_cache(self) + if (cacher_name, property_name) not in self._mutable_property_cache_copy: + return default + return self._mutable_property_cache_copy[(cacher_name, property_name)] +def _cmp_cached_mutable_property(self, cacher_name, property_name, value): + _init_mutable_property_cache(self) + if (cacher_name, property_name) not in self._mutable_property_cache_hash: + return 1 # any value > non-existant old hash + old_hash = self._mutable_property_cache_hash[(cacher_name, property_name)] + return cmp(_hash_mutable_value(value), old_hash) + + +def defaulting_property(default=None, null=None, + default_mutable=False, + null_mutable=False): """ Define a default value for get access to a property. If the stored value is null, then default is returned. @@ -127,12 +175,25 @@ def defaulting_property(default=None, null=None): if hasattr(funcs, "__call__"): funcs = funcs() fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "<unknown>") def _fget(self): value = fget(self) if value == null: - return default + if default_mutable == True: + return copy.deepcopy(default) + else: + return default return value + def _fset(self, value): + if value == default: + if null_mutable == True: + value = copy.deepcopy(null) + else: + value = null + fset(self, value) funcs["fget"] = _fget + funcs["fset"] = _fset return funcs return decorator @@ -184,7 +245,7 @@ def checked_property(allowed=[]): return funcs return decorator -def cached_property(generator, initVal=None): +def cached_property(generator, initVal=None, mutable=False): """ Allow caching of values generated by generator(instance), where instance is the instance to which this property belongs. Uses @@ -193,7 +254,7 @@ def cached_property(generator, initVal=None): When the cache flag is True or missing and the stored value is initVal, the first fget call triggers the generator function, - whiose output is stored in _<name>_cached_value. That and + whose output is stored in _<name>_cached_value. That and subsequent calls to fget will return this cached value. If the input value is no longer initVal (e.g. a value has been @@ -205,25 +266,27 @@ def cached_property(generator, initVal=None): The cache flag is missing on initialization. Particular instances may override by setting their own flag. + + In the case that mutable == True, all caching is disabled and the + generator is called whenever the cached value would otherwise be + used. This avoids uncertainties in the value of stored mutables. """ def decorator(funcs): if hasattr(funcs, "__call__"): funcs = funcs() fget = funcs.get("fget") - fset = funcs.get("fset") name = funcs.get("name", "<unknown>") def _fget(self): cache = getattr(self, "_%s_cache" % name, True) value = fget(self) - if cache == True: - if value == initVal: + if value == initVal: + if cache == True and mutable == False: if hasattr(self, "_%s_cached_value" % name): value = getattr(self, "_%s_cached_value" % name) else: value = generator(self) setattr(self, "_%s_cached_value" % name, value) - else: - if value == initVal: + else: value = generator(self) return value funcs["fget"] = _fget @@ -258,12 +321,15 @@ def primed_property(primer, initVal=None): return funcs return decorator -def change_hook_property(hook): +def change_hook_property(hook, mutable=False): """ Call the function hook(instance, old_value, new_value) whenever a value different from the current value is set (instance is a a reference to the class instance to which this property belongs). - This is useful for saving changes to disk, etc. + This is useful for saving changes to disk, etc. This function is + called _after_ the new value has been stored, allowing you to + change the stored value if you want. + """ def decorator(funcs): if hasattr(funcs, "__call__"): @@ -271,15 +337,31 @@ def change_hook_property(hook): fget = funcs.get("fget") fset = funcs.get("fset") name = funcs.get("name", "<unknown>") + def _fget(self, new_value=None, from_fset=False): # only used if mutable == True + value = fget(self) + if _cmp_cached_mutable_property(self, "change hook property", name, value) != 0: + # there has been a change, cache new value + old_value = _get_cached_mutable_property(self, "change hook property", name) + _set_cached_mutable_property(self, "change hook property", name, value) + if from_fset == True: # return previously cached value + value = old_value + else: # the value changed while we weren't looking + hook(self, old_value, value) + return value def _fset(self, value): - old_value = fget(self) + if mutable == True: # get cached previous value + old_value = _fget(self, new_value=value, from_fset=True) + else: + old_value = fget(self) + fset(self, value) if value != old_value: hook(self, old_value, value) - fset(self, value) + if mutable == True: + funcs["fget"] = _fget funcs["fset"] = _fset return funcs return decorator - + class DecoratorTests(unittest.TestCase): def testLocalDoc(self): @@ -320,16 +402,18 @@ class DecoratorTests(unittest.TestCase): class Test(object): @Property @defaulting_property(default='y', null='x') - @local_property(name="DEFAULT") + @local_property(name="DEFAULT", null=5) def x(): return {} t = Test() - self.failUnless(t.x == None, str(t.x)) + self.failUnless(t.x == 5, str(t.x)) t.x = 'x' self.failUnless(t.x == 'y', str(t.x)) t.x = 'y' self.failUnless(t.x == 'y', str(t.x)) t.x = 'z' self.failUnless(t.x == 'z', str(t.x)) + t.x = 5 + self.failUnless(t.x == 5, str(t.x)) def testCheckedLocalProperty(self): class Test(object): @Property @@ -474,6 +558,57 @@ class DecoratorTests(unittest.TestCase): t.x = 2 self.failUnless(t.old == 1, t.old) self.failUnless(t.new == 2, t.new) + def testChangeHookMutableProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + self.hook_calls += 1 + + @Property + @change_hook_property(_hook, mutable=True) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.hook_calls = 0 + t.x = [] + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == [], t.new) + a = t.x + a.append(5) + t.x = a + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5], t.new) + t.x = [] + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [], t.new) + # now append without reassigning. this doesn't trigger the + # change, since we don't ever set t.x, only get it and mess + # with it. It does, however, update our t.new, since t.new = + # t.x and is not a static copy. + t.x.append(5) + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [5], t.new) + # however, the next t.x get _will_ notice the change... + a = t.x + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5], t.new) + self.failUnless(t.hook_calls == 6, t.hook_calls) + t.x.append(6) # this append(6) is not noticed yet + self.failUnless(t.old == [], t.old) + self.failUnless(t.new == [5,6], t.new) + self.failUnless(t.hook_calls == 6, t.hook_calls) + # this append(7) is not noticed, but the t.x get causes the + # append(6) to be noticed + t.x.append(7) + self.failUnless(t.old == [5], t.old) + self.failUnless(t.new == [5,6,7], t.new) + self.failUnless(t.hook_calls == 7, t.hook_calls) + a = t.x # now the append(7) is noticed + self.failUnless(t.old == [5,6], t.old) + self.failUnless(t.new == [5,6,7], t.new) + self.failUnless(t.hook_calls == 8, t.hook_calls) + suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) |