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.
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)
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.
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
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
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
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
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__"):
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
funcs["fset"] = _fset
return funcs
return decorator
-
+
class DecoratorTests(unittest.TestCase):
def testLocalDoc(self):
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
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)