aboutsummaryrefslogtreecommitdiffstats
path: root/libbe
diff options
context:
space:
mode:
Diffstat (limited to 'libbe')
-rw-r--r--libbe/bug.py8
-rw-r--r--libbe/properties.py143
-rw-r--r--libbe/settings_object.py6
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)