diff options
Diffstat (limited to 'libbe')
-rw-r--r-- | libbe/bug.py | 8 | ||||
-rw-r--r-- | libbe/properties.py | 143 | ||||
-rw-r--r-- | libbe/settings_object.py | 6 |
3 files changed, 89 insertions, 68 deletions
diff --git a/libbe/bug.py b/libbe/bug.py index 0f912cf..7418933 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -185,11 +185,11 @@ class Bug(settings_object.SavedSettingsObject): fset=_set_time, doc="An integer version of .time_string") - def _extra_strings_generator(self): - return [] def _extra_strings_check_fn(value): "Require an iterable full of strings" - if not hasattr(value, "__iter__"): + if value == settings_object.EMPTY: + return True + elif not hasattr(value, "__iter__"): return False for x in value: if type(x) not in types.StringTypes: @@ -200,7 +200,7 @@ class Bug(settings_object.SavedSettingsObject): self._prop_save_settings(old, new) @_versioned_property(name="extra_strings", doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.", - generator=_extra_strings_generator, + default=[], check_fn=_extra_strings_check_fn, change_hook=_extra_strings_change_hook, mutable=True) diff --git a/libbe/properties.py b/libbe/properties.py index 2ccc50b..956ecc3 100644 --- a/libbe/properties.py +++ b/libbe/properties.py @@ -71,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. @@ -85,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) @@ -123,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. @@ -133,14 +176,21 @@ def defaulting_property(default=None, null=None): 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: - value = null + if null_mutable == True: + value = copy.deepcopy(null) + else: + value = null fset(self, value) funcs["fget"] = _fget funcs["fset"] = _fset @@ -195,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 @@ -204,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 @@ -216,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 @@ -278,20 +330,6 @@ def change_hook_property(hook, mutable=False): called _after_ the new value has been stored, allowing you to change the stored value if you want. - If mutable=True, store a string-representation of the old_value - for use in comparisions, since - - >>> a = [] - >>> b = a - >>> b.append(1) - >>> a - [1] - >>> a==b - True - - The string-value-changed test may miss the first write, since - there will not have been an opportunity to cache a string version - of the old value. """ def decorator(funcs): if hasattr(funcs, "__call__"): @@ -299,37 +337,16 @@ def change_hook_property(hook, mutable=False): fget = funcs.get("fget") fset = funcs.get("fset") name = funcs.get("name", "<unknown>") - def hash_value(value): # only used if mutable == True - return repr(value) def _fget(self, new_value=None, from_fset=False): # only used if mutable == True value = fget(self) - if not hasattr(self, "_change_hook_property_mutable_cache_hash"): - # first call to _fget for any mutable property - self._change_hook_property_mutable_cache_hash = {} - self._change_hook_property_mutable_cache_copy = {} - if name not in self._change_hook_property_mutable_cache_hash: - # first call to _fget for this particular mutable property - self._change_hook_property_mutable_cache_hash[name] = \ - hash_value(new_value) - self._change_hook_property_mutable_cache_copy[name] = \ - copy.deepcopy(new_value) - elif from_fset == True: # return cached value, and cache new value - old_hash = self._change_hook_property_mutable_cache_hash[name] - if hash_value(value) != old_hash: - value = self._change_hook_property_mutable_cache_copy[name] - self._change_hook_property_mutable_cache_hash[name] = \ - hash_value(new_value) - self._change_hook_property_mutable_cache_copy[name] = \ - copy.deepcopy(new_value) - else: # check for a change in value while we weren't looking - old_hash = self._change_hook_property_mutable_cache_hash[name] - if hash_value(value) != old_hash: - old_val=self._change_hook_property_mutable_cache_copy[name] - self._change_hook_property_mutable_cache_hash[name] = \ - hash_value(value) - self._change_hook_property_mutable_cache_copy[name] = \ - copy.deepcopy(value) - hook(self, old_val, value) + 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): if mutable == True: # get cached previous value @@ -344,7 +361,7 @@ def change_hook_property(hook, mutable=False): funcs["fset"] = _fset return funcs return decorator - + class DecoratorTests(unittest.TestCase): def testLocalDoc(self): @@ -385,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 @@ -574,21 +593,21 @@ class DecoratorTests(unittest.TestCase): a = t.x self.failUnless(t.old == [], t.old) self.failUnless(t.new == [5], t.new) - self.failUnless(t.hook_calls == 5, t.hook_calls) + 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 == 5, t.hook_calls) + 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 == 6, t.hook_calls) + 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 == 7, t.hook_calls) + self.failUnless(t.hook_calls == 8, t.hook_calls) suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) diff --git a/libbe/settings_object.py b/libbe/settings_object.py index a854ce5..60fddb9 100644 --- a/libbe/settings_object.py +++ b/libbe/settings_object.py @@ -126,10 +126,12 @@ def versioned_property(name, doc, def decorator(funcs): fulldoc = doc if default != None: - defaulting = defaulting_property(default=default, null=EMPTY) + defaulting = defaulting_property(default=default, null=EMPTY, + default_mutable=mutable) fulldoc += "\n\nThis property defaults to %s" % default if generator != None: - cached = cached_property(generator=generator, initVal=EMPTY) + cached = cached_property(generator=generator, initVal=EMPTY, + mutable=mutable) fulldoc += "\n\nThis property is generated with %s" % generator if check_fn != None: fn_checked = fn_checked_property(value_allowed_fn=check_fn) |