aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--becommands/set.py31
-rw-r--r--libbe/bug.py2
-rw-r--r--libbe/bugdir.py258
-rw-r--r--libbe/comment.py2
-rw-r--r--libbe/properties.py95
-rw-r--r--libbe/settings_object.py267
6 files changed, 487 insertions, 168 deletions
diff --git a/becommands/set.py b/becommands/set.py
index aef5eb3..1103b7b 100644
--- a/becommands/set.py
+++ b/becommands/set.py
@@ -15,9 +15,19 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Change tree settings"""
-from libbe import cmdutil, bugdir
+from libbe import cmdutil, bugdir, settings_object
__desc__ = __doc__
+def _value_string(bd, setting):
+ val = bd.settings.get(setting, settings_object.EMPTY)
+ if val == settings_object.EMPTY:
+ default = getattr(bd, bd._setting_name_to_attr_name(setting))
+ if default != settings_object.EMPTY:
+ val = "None (%s)" % default
+ else:
+ val = None
+ return str(val)
+
def execute(args, test=False):
"""
>>> import os
@@ -39,23 +49,22 @@ def execute(args, test=False):
raise cmdutil.UsageError, "Too many arguments"
bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test)
if len(args) == 0:
- keys = bd.settings.keys()
+ keys = bd.settings_properties
keys.sort()
for key in keys:
- print "%16s: %s" % (key, bd.settings[key])
+ print "%16s: %s" % (key, _value_string(bd, key))
elif len(args) == 1:
- print bd.settings.get(args[0])
+ print _value_string(bd, args[0])
else:
if args[1] != "none":
+ if args[0] not in bd.settings_properties:
+ msg = "Invalid setting %s\n" % args[0]
+ msg += 'Allowed settings:\n '
+ msg += '\n '.join(bd.settings_properties)
+ raise cmdutil.UserError(msg)
old_setting = bd.settings.get(args[0])
- bd.settings[args[0]] = args[1]
- if args[0] == "user_id":
- bd.save_user_id()
-
- # attempt to get the new value
- bd.save()
try:
- bd.load()
+ setattr(bd, args[0], args[1])
except bugdir.InvalidValue, e:
bd.settings[args[0]] = old_setting
bd.save()
diff --git a/libbe/bug.py b/libbe/bug.py
index 5f0429e..bb79d1d 100644
--- a/libbe/bug.py
+++ b/libbe/bug.py
@@ -302,7 +302,7 @@ class Bug(object):
assert self.summary != None, "Can't save blank bug"
map = {}
for k,v in self.settings.items():
- if (v != None and v != EMPTY):
+ if v != None and v != EMPTY:
map[k] = v
for k in self.required_saved_properties:
map[k] = getattr(self, k)
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index 1142e3d..f93576f 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -22,6 +22,11 @@ import copy
import unittest
import doctest
+from properties import Property, doc_property, local_property, \
+ defaulting_property, checked_property, fn_checked_property, \
+ cached_property, primed_property, change_hook_property, \
+ settings_property
+import settings_object
import mapfile
import bug
import rcs
@@ -65,31 +70,7 @@ class MultipleBugMatches(ValueError):
TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n"
-def setting_property(name, valid=None, default=None, doc=None):
- if default != None:
- raise NotImplementedError
- def getter(self):
- value = self.settings.get(name)
- if valid is not None:
- if value not in valid and value != None:
- raise InvalidValue(name, value)
- return value
-
- def setter(self, value):
- if value != getter(self):
- if valid is not None:
- if value not in valid and value != None:
- raise InvalidValue(name, value)
- if value is None:
- del self.settings[name]
- else:
- self.settings[name] = value
- self._save_settings(self.get_path("settings"), self.settings)
-
- return property(getter, setter, doc=doc)
-
-
-class BugDir (list):
+class BugDir (list, settings_object.SavedSettingsObject):
"""
Sink to existing root
======================
@@ -143,14 +124,108 @@ class BugDir (list):
using BugDirs, set manipulate_encodings=False, and stick to ASCII
in your tests.
"""
+
+ settings_properties = []
+ required_saved_properties = []
+ _prop_save_settings = settings_object.prop_save_settings
+ _prop_load_settings = settings_object.prop_load_settings
+ def _versioned_property(settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ **kwargs):
+ if "settings_properties" not in kwargs:
+ kwargs["settings_properties"] = settings_properties
+ if "required_saved_properties" not in kwargs:
+ kwargs["required_saved_properties"]=required_saved_properties
+ return settings_object.versioned_property(**kwargs)
+
+ @_versioned_property(name="target",
+ doc="The current project development target")
+ def target(): return {}
+
+ def _guess_encoding(self):
+ return encoding.get_encoding()
+ def _check_encoding(value):
+ if value != None and value != settings_object.EMPTY:
+ return encoding.known_encoding(value)
+ def _setup_encoding(self, new_encoding):
+ if new_encoding != None and new_encoding != settings_object.EMPTY:
+ if self._manipulate_encodings == True:
+ encoding.set_IO_stream_encodings(new_encoding)
+ def _set_encoding(self, old_encoding, new_encoding):
+ self._setup_encoding(new_encoding)
+ self._prop_save_settings(old_encoding, new_encoding)
+
+ @_versioned_property(name="encoding",
+ doc="""The default input/output encoding to use (e.g. "utf-8").""",
+ change_hook=_set_encoding,
+ generator=_guess_encoding,
+ check_fn=_check_encoding)
+ def encoding(): return {}
+
+ def _guess_user_id(self):
+ return self.rcs.get_user_id()
+ def _set_user_id(self, old_user_id, new_user_id):
+ self.rcs.user_id = new_user_id
+ self._prop_save_settings(old_user_id, new_user_id)
+
+ @_versioned_property(name="user_id",
+ doc=
+"""The user's prefered name, e.g 'John Doe <jdoe@example.com>'. Note
+that the Arch RCS backend *enforces* ids with this format.""",
+ change_hook=_set_user_id,
+ generator=_guess_user_id)
+ def user_id(): return {}
+
+ @_versioned_property(name="rcs_name",
+ doc="""The name of the current RCS. Kept seperate to make saving/loading
+settings easy. Don't set this attribute. Set .rcs instead, and
+.rcs_name will be automatically adjusted.""",
+ default="None",
+ allowed=["None", "Arch", "bzr", "git", "hg"])
+ def rcs_name(): return {}
+
+ def _get_rcs(self, rcs_name=None):
+ """Get and root a new revision control system"""
+ if rcs_name == None:
+ rcs_name = self.rcs_name
+ new_rcs = rcs.rcs_by_name(rcs_name)
+ self._change_rcs(None, new_rcs)
+ return new_rcs
+ def _change_rcs(self, old_rcs, new_rcs):
+ new_rcs.encoding = self.encoding
+ new_rcs.root(self.root)
+ self.rcs_name = new_rcs.name
+
+ @Property
+ @change_hook_property(hook=_change_rcs)
+ @cached_property(generator=_get_rcs)
+ @local_property("rcs")
+ @doc_property(doc="A revision control system instance.")
+ def rcs(): return {}
+
+ def _bug_map_gen(self):
+ map = {}
+ for bug in self:
+ map[bug.uuid] = bug
+ for uuid in self.list_uuids():
+ if uuid not in map:
+ map[uuid] = None
+ self._bug_map_value = map # ._bug_map_value used by @local_property
+
+ @Property
+ @primed_property(primer=_bug_map_gen)
+ @local_property("bug_map")
+ @doc_property(doc="A dict of (bug-uuid, bug-instance) pairs.")
+ def _bug_map(): return {}
+
+
def __init__(self, root=None, sink_to_existing_root=True,
assert_new_BugDir=False, allow_rcs_init=False,
manipulate_encodings=True,
from_disk=False, rcs=None):
list.__init__(self)
- self._save_user_id = False
+ settings_object.SavedSettingsObject.__init__(self)
self._manipulate_encodings = manipulate_encodings
- self.settings = {}
if root == None:
root = os.getcwd()
if sink_to_existing_root == True:
@@ -159,9 +234,15 @@ class BugDir (list):
if not os.path.exists(root):
raise NoRootEntry(root)
self.root = root
+ # get a temporary rcs until we've loaded settings
+ self.sync_with_disk = False
+ self.rcs = self._guess_rcs()
+
if from_disk == True:
+ self.sync_with_disk = True
self.load()
else:
+ self.sync_with_disk = False
if assert_new_BugDir == True:
if os.path.exists(self.get_path()):
raise AlreadyInitialized, self.get_path()
@@ -206,75 +287,6 @@ class BugDir (list):
self.rcs.set_file_contents(self.get_path("version"),
TREE_VERSION_STRING)
- def _get_encoding(self):
- if self._encoding == None:
- return encoding.get_encoding()
- else:
- return self._encoding
- def _set_encoding(self, new_encoding):
- if new_encoding != None:
- if encoding.known_encoding(new_encoding) == False:
- raise InvalidValue("encoding", new_encoding)
- self._encoding = new_encoding
- if self._manipulate_encodings == True:
- encoding.set_IO_stream_encodings(self.encoding)
- if hasattr(self, "rcs"):
- if self.rcs != None:
- self.rcs.encoding = self.encoding
- _encoding = setting_property("encoding",
- doc=
-"""The default input/output encoding to use (e.g. "utf-8").
-Dont' set this attribute, set .encoding instead.""")
- encoding = property(_get_encoding, _set_encoding, doc=
-"""The default input/output encoding to use (e.g. "utf-8").""")
-
- def _get_rcs(self):
- return self._rcs
- def _set_rcs(self, new_rcs):
- if new_rcs == None:
- new_rcs = rcs.rcs_by_name("None")
- new_rcs.encoding = self.encoding
- self._rcs = new_rcs
- new_rcs.root(self.root)
- self.rcs_name = new_rcs.name
- _rcs = None
- rcs = property(_get_rcs, _set_rcs,
- doc="A revision control system (RCS) instance")
- rcs_name = setting_property("rcs_name",
- ("None", "bzr", "git", "Arch", "hg"),
- doc=
-"""The name of the current RCS. Kept seperate to make saving/loading
-settings easy. Don't set this attribute. Set .rcs instead, and
-.rcs_name will be automatically adjusted.""")
-
-
- def _get_user_id(self):
- if self._user_id == None and self.rcs != None:
- self._user_id = self.rcs.get_user_id()
- return self._user_id
- def _set_user_id(self, user_id):
- if self.rcs != None:
- self.rcs.user_id = user_id
- self._user_id = user_id
- user_id = property(_get_user_id, _set_user_id, doc=
-"""The user's prefered name, e.g 'John Doe <jdoe@example.com>'. Note
-that the Arch RCS backend *enforces* ids with this format.""")
- _user_id = setting_property("user_id", doc=
-"""The user's prefered name. Kept seperate to make saving/loading
-settings easy. Don't set this attribute. Set .user_id instead,
-and ._user_id will be automatically adjusted. This setting is
-only saved if ._save_user_id == True""")
-
-
- target = setting_property("target",
- doc="The current project development target")
-
- def save_user_id(self, user_id=None):
- if user_id == None:
- user_id = self.user_id
- self._save_user_id = True
- self.user_id = user_id
-
def get_path(self, *args):
my_dir = os.path.join(self.root, ".be")
if len(args) == 0:
@@ -292,7 +304,6 @@ only saved if ._save_user_id == True""")
if allow_rcs_init == True:
new_rcs = rcs.installed_rcs()
new_rcs.init(self.root)
- self.rcs = new_rcs
return new_rcs
def load(self):
@@ -303,14 +314,10 @@ only saved if ._save_user_id == True""")
else:
if not os.path.exists(self.get_path()):
raise NoBugDir(self.get_path())
- self.settings = self._get_settings(self.get_path("settings"))
+ self.load_settings()
self.rcs = rcs.rcs_by_name(self.rcs_name)
- self.encoding = self.encoding # setup encoding, IO_stream_encoding...
- if self.settings.get("user_id") != None:
- self.save_user_id() # was a user name in the settings file
-
- self._bug_map_gen()
+ self._setup_encoding(self.encoding)
def load_all_bugs(self):
"Warning: this could take a while."
@@ -321,45 +328,31 @@ only saved if ._save_user_id == True""")
def save(self):
self.rcs.mkdir(self.get_path())
self.set_version()
- self._save_settings(self.get_path("settings"), self.settings)
+ self.save_settings()
self.rcs.mkdir(self.get_path("bugs"))
for bug in self:
bug.save()
+ def load_settings(self):
+ self.settings = self._get_settings(self.get_path("settings"))
+ self._setup_saved_settings()
+
def _get_settings(self, settings_path):
- if self.rcs_name == None:
- # Use a temporary RCS to loading settings the first time
- RCS = rcs.rcs_by_name("None")
- RCS.root(self.root)
- else:
- RCS = self.rcs
-
- allow_no_rcs = not RCS.path_in_root(settings_path)
+ allow_no_rcs = not self.rcs.path_in_root(settings_path)
# allow_no_rcs=True should only be for the special case of
# configuring duplicate bugdir settings
try:
- settings = mapfile.map_load(RCS, settings_path, allow_no_rcs)
+ settings = mapfile.map_load(self.rcs, settings_path, allow_no_rcs)
except rcs.NoSuchFile:
settings = {"rcs_name": "None"}
return settings
+ def save_settings(self):
+ settings = self._get_saved_settings()
+ self._save_settings(self.get_path("settings"), settings)
+
def _save_settings(self, settings_path, settings):
- this_dir_path = os.path.realpath(self.get_path("settings"))
- if os.path.realpath(settings_path) == this_dir_path:
- if not os.path.exists(self.get_path()):
- # don't save settings until the bug directory has been
- # initialized. this initialization happens the first time
- # a bug directory is saved (BugDir.save()). If the user
- # is just working with a BugDir in memory, we don't want
- # to go cluttering up his file system with settings files.
- return
- if self._save_user_id == False:
- if "user_id" in settings:
- settings = copy.copy(settings)
- del settings["user_id"]
- if settings.get("encoding") == encoding.get_encoding():
- del settings["encoding"] # don't duplicate system default
allow_no_rcs = not self.rcs.path_in_root(settings_path)
# allow_no_rcs=True should only be for the special case of
# configuring duplicate bugdir settings
@@ -383,15 +376,6 @@ only saved if ._save_user_id == True""")
def remove_duplicate_bugdir(self):
self.rcs.remove_duplicate_repo()
- def _bug_map_gen(self):
- map = {}
- for bug in self:
- map[bug.uuid] = bug
- for uuid in self.list_uuids():
- if uuid not in map:
- map[uuid] = None
- self._bug_map = map
-
def list_uuids(self):
uuids = []
if os.path.exists(self.get_path()):
diff --git a/libbe/comment.py b/libbe/comment.py
index 0fd871c..e3c0a12 100644
--- a/libbe/comment.py
+++ b/libbe/comment.py
@@ -298,7 +298,7 @@ class Comment(Tree):
def save_settings(self):
map = {}
for k,v in self.settings.items():
- if (v != None and v != EMPTY):
+ if v != None and v != EMPTY:
map[k] = v
for k in self.required_saved_properties:
map[k] = getattr(self, self._setting_name_to_attr_name(k))
diff --git a/libbe/properties.py b/libbe/properties.py
index f55dc0e..176e898 100644
--- a/libbe/properties.py
+++ b/libbe/properties.py
@@ -134,6 +134,30 @@ def defaulting_property(default=None, null=None):
return funcs
return decorator
+def fn_checked_property(value_allowed_fn):
+ """
+ Define allowed values for get/set access to a property.
+ """
+ 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):
+ value = fget(self)
+ if value_allowed_fn(value) != True:
+ raise ValueCheckError(name, value, value_allowed_fn)
+ return value
+ def _fset(self, value):
+ if value_allowed_fn(value) != True:
+ raise ValueCheckError(name, value, value_allowed_fn)
+ fset(self, value)
+ funcs["fget"] = _fget
+ funcs["fset"] = _fset
+ return funcs
+ return decorator
+
def checked_property(allowed=[]):
"""
Define allowed values for get/set access to a property.
@@ -163,14 +187,22 @@ def cached_property(generator, initVal=None):
Allow caching of values generated by generator(instance), where
instance is the instance to which this property belongs. Uses
._<name>_cache to store a cache flag for a particular owner
- instance. When the cache flag is True (or missing), the normal
- value is returned. Otherwise the generator is called (and it's
- output stored) for every get. The cache flag is missing on
- initialization. Particular instances may override by setting
- their own flag.
+ instance.
+
+ 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
+ subsequent calls to fget will return this cached value.
+
+ If the input value is no longer initVal (e.g. a value has been
+ loaded from disk or set with fset), that value overrides any
+ cached value, and this property has no effect.
- If caching is True, but the stored value == initVal, the parameter
- is considered 'uninitialized', and the generator is called anyway.
+ When the cache flag is False and the stored value is initVal, the
+ generator is not cached, but is called on every fget.
+
+ The cache flag is missing on initialization. Particular instances
+ may override by setting their own flag.
"""
def decorator(funcs):
if hasattr(funcs, "__call__"):
@@ -180,11 +212,17 @@ def cached_property(generator, initVal=None):
name = funcs.get("name", "<unknown>")
def _fget(self):
cache = getattr(self, "_%s_cache" % name, True)
+ value = fget(self)
if cache == True:
- value = fget(self)
- if cache == False or (cache == True and value == initVal):
- value = generator(self)
- fset(self, value)
+ if value == initVal:
+ 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:
+ value = generator(self)
return value
funcs["fget"] = _fget
return funcs
@@ -339,6 +377,22 @@ class DecoratorTests(unittest.TestCase):
t.a = 'a'
t.a = 'b'
t.a = 'c'
+ def testFnCheckedLocalProperty(self):
+ class Test(object):
+ @Property
+ @fn_checked_property(lambda v : v in ['x', 'y', 'z'])
+ @local_property(name="CHECKED")
+ def x(): return {}
+ def __init__(self):
+ self._CHECKED_value = 'x'
+ t = Test()
+ self.failUnless(t.x == 'x', str(t.x))
+ try:
+ t.x = None
+ e = None
+ except ValueCheckError, e:
+ pass
+ self.failUnless(type(e) == ValueCheckError, type(e))
def testCachedLocalProperty(self):
class Gen(object):
def __init__(self):
@@ -359,17 +413,22 @@ class DecoratorTests(unittest.TestCase):
t.x = 8
self.failUnless(t.x == 8, t.x)
self.failUnless(t.x == 8, t.x)
- t._CACHED_cache = False
- val = t.x
- self.failUnless(val == 2, val)
+ t._CACHED_cache = False # Caching is off, but the stored value
+ val = t.x # is 8, not the initVal (None), so we
+ self.failUnless(val == 8, val) # get 8.
+ t._CACHED_value = None # Now we've set the stored value to None
+ val = t.x # so future calls to fget (like this)
+ self.failUnless(val == 2, val) # will call the generator every time...
val = t.x
self.failUnless(val == 3, val)
val = t.x
self.failUnless(val == 4, val)
- t._CACHED_cache = True
- self.failUnless(t.x == 4, str(t.x))
- self.failUnless(t.x == 4, str(t.x))
- self.failUnless(t.x == 4, str(t.x))
+ t._CACHED_cache = True # We turn caching back on, and get
+ self.failUnless(t.x == 1, str(t.x)) # the original cached value.
+ del t._CACHED_cached_value # Removing that value forces a
+ self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call
+ self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which
+ self.failUnless(t.x == 5, str(t.x)) # we get the new cached value.
def testPrimedLocalProperty(self):
class Test(object):
def prime(self):
diff --git a/libbe/settings_object.py b/libbe/settings_object.py
new file mode 100644
index 0000000..8b0ff47
--- /dev/null
+++ b/libbe/settings_object.py
@@ -0,0 +1,267 @@
+# Bugs Everywhere - a distributed bugtracker
+# Copyright (C) 2008 W. Trevor King <wking@drexel.edu>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+This module provides a base class implementing settings-dict based
+property storage useful for BE objects with saved properties
+(e.g. BugDir, Bug, Comment). For example usage, consider the
+unittests at the end of the module.
+"""
+
+import doctest
+import unittest
+
+from properties import Property, doc_property, local_property, \
+ defaulting_property, checked_property, fn_checked_property, \
+ cached_property, primed_property, change_hook_property, \
+ settings_property
+
+# Define an invalid value for our properties, distinct from None,
+# which shows that a property has been initialized but has no value.
+EMPTY = -1
+
+
+def prop_save_settings(self, old, new):
+ if self.sync_with_disk==True:
+ self.save_settings()
+def prop_load_settings(self):
+ if self.sync_with_disk==True and self._settings_loaded==False:
+ self.load_settings()
+ else:
+ self._setup_saved_settings(flag_as_loaded=False)
+
+def setting_name_to_attr_name(self, name):
+ """
+ Convert keys to the .settings dict into their associated
+ SavedSettingsObject attribute names.
+ >>> print setting_name_to_attr_name(None,"User-id")
+ user_id
+ """
+ return name.lower().replace('-', '_')
+
+def attr_name_to_setting_name(self, name):
+ """
+ The inverse of setting_name_to_attr_name.
+ >>> print attr_name_to_setting_name(None, "user_id")
+ User-id
+ """
+ return name.capitalize().replace('_', '-')
+
+def versioned_property(name, doc,
+ default=None, generator=None,
+ change_hook=prop_save_settings,
+ primer=prop_load_settings,
+ allowed=None, check_fn=None,
+ settings_properties=[],
+ required_saved_properties=[],
+ require_save=False):
+ """
+ Combine the common decorators in a single function.
+
+ Use zero or one (but not both) of default or generator, since a
+ working default will keep the generator from functioning. Use the
+ default if you know what you want the default value to be at
+ 'coding time'. Use the generator if you can write a function to
+ determine a valid default at run time.
+
+ allowed and check_fn have a similar relationship, although you can
+ use both of these if you want. allowed compares the proposed
+ value against a list determined at 'coding time' and check_fn
+ allows more flexible comparisons to take place at run time.
+
+ Set require_save to True if you want to save the default/generated
+ value for a property, to protect against future changes. E.g., we
+ currently expect all comments to be 'text/plain' but in the future
+ we may want to default to 'text/html'. If we don't want the old
+ comments to be interpreted as 'text/html', we would require that
+ the content type be saved.
+
+ change_hook, primer, settings_properties, and
+ required_saved_properties are only options to get their defaults
+ into our local scope. Don't mess with them.
+ """
+ settings_properties.append(name)
+ if require_save == True:
+ required_saved_properties.append(name)
+ def decorator(funcs):
+ fulldoc = doc
+ if default != None:
+ defaulting = defaulting_property(default=default, null=EMPTY)
+ fulldoc += "\n\nThis property defaults to %s" % default
+ if generator != None:
+ cached = cached_property(generator=generator, initVal=EMPTY)
+ fulldoc += "\n\nThis property is generated with %s" % generator
+ if check_fn != None:
+ fn_checked = fn_checked_property(value_allowed_fn=check_fn)
+ fulldoc += "\n\nThis property is checked with %s" % check_fn
+ if allowed != None:
+ checked = checked_property(allowed=allowed)
+ fulldoc += "\n\nThe allowed values for this property are: %s." \
+ % (', '.join(allowed))
+ hooked = change_hook_property(hook=change_hook)
+ primed = primed_property(primer=primer)
+ settings = settings_property(name=name)
+ docp = doc_property(doc=fulldoc)
+ deco = hooked(primed(settings(docp(funcs))))
+ if default != None:
+ deco = defaulting(deco)
+ if generator != None:
+ deco = cached(deco)
+ if default != None:
+ deco = defaulting(deco)
+ if allowed != None:
+ deco = checked(deco)
+ if check_fn != None:
+ deco = fn_checked(deco)
+ return Property(deco)
+ return decorator
+
+class SavedSettingsObject(object):
+
+ # Keep a list of properties that may be stored in the .settings dict.
+ #settings_properties = []
+
+ # A list of properties that we save to disk, even if they were
+ # never set (in which case we save the default value). This
+ # protects against future changes in default values.
+ #required_saved_properties = []
+
+ _setting_name_to_attr_name = setting_name_to_attr_name
+ _attr_name_to_setting_name = attr_name_to_setting_name
+
+ def __init__(self):
+ self._settings_loaded = False
+ self.sync_with_disk = False
+ self.settings = {}
+
+ def load_settings(self):
+ """Load the settings from disk."""
+ # Override. Must call ._setup_saved_settings() after loading.
+ self.settings = {}
+ self._setup_saved_settings()
+
+ def _setup_saved_settings(self, flag_as_loaded=True):
+ """To be run after setting self.settings up from disk."""
+ for property in self.settings_properties:
+ if property not in self.settings:
+ self.settings[property] = EMPTY
+ elif self.settings[property] == None:
+ self.settings[property] = EMPTY
+ if flag_as_loaded == True:
+ self._settings_loaded = True
+
+ def save_settings(self):
+ """Load the settings from disk."""
+ # Override. Should save the dict output of ._get_saved_settings()
+ settings = self._get_saved_settings()
+ pass # write settings to disk....
+
+ def _get_saved_settings(self):
+ settings = {}
+ for k,v in self.settings.items():
+ if v != None and v != EMPTY:
+ settings[k] = v
+ for k in self.required_saved_properties:
+ settings[k] = getattr(self, self._setting_name_to_attr_name(k))
+ return settings
+
+ def clear_cached_setting(self, setting=None):
+ "If setting=None, clear *all* cached settings"
+ if setting != None:
+ if hasattr(self, "_%s_cached_value" % setting):
+ delattr(self, "_%s_cached_value" % setting)
+ else:
+ for setting in settings_properties:
+ self.clear_cached_setting(setting)
+
+
+class SavedSettingsObjectTests(unittest.TestCase):
+ def testDefaultingProperty(self):
+ class Test(SavedSettingsObject):
+ settings_properties = []
+ required_saved_properties = []
+ @versioned_property(name="Content-type",
+ doc="A test property",
+ default="text/plain",
+ settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties)
+ def content_type(): return {}
+ def __init__(self):
+ SavedSettingsObject.__init__(self)
+ t = Test()
+ self.failUnless(t._settings_loaded == False, t._settings_loaded)
+ self.failUnless(t.content_type == "text/plain", t.content_type)
+ self.failUnless(t._settings_loaded == False, t._settings_loaded)
+ t.load_settings()
+ self.failUnless(t._settings_loaded == True, t._settings_loaded)
+ self.failUnless(t.content_type == "text/plain", t.content_type)
+ self.failUnless(t.settings["Content-type"] == EMPTY,
+ t.settings["Content-type"])
+ self.failUnless(t._get_saved_settings() == {}, t._get_saved_settings())
+ t.content_type = "text/html"
+ self.failUnless(t.content_type == "text/html",
+ t.content_type)
+ self.failUnless(t.settings["Content-type"] == "text/html",
+ t.settings["Content-type"])
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"},
+ t._get_saved_settings())
+ def testRequiredDefaultingProperty(self):
+ class Test(SavedSettingsObject):
+ settings_properties = []
+ required_saved_properties = []
+ @versioned_property(name="Content-type",
+ doc="A test property",
+ default="text/plain",
+ settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ require_save=True)
+ def content_type(): return {}
+ def __init__(self):
+ SavedSettingsObject.__init__(self)
+ t = Test()
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"},
+ t._get_saved_settings())
+ t.content_type = "text/html"
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"},
+ t._get_saved_settings())
+ def testClassVersionedPropertyDefinition(self):
+ class Test(SavedSettingsObject):
+ settings_properties = []
+ required_saved_properties = []
+ def _versioned_property(settings_properties=settings_properties,
+ required_saved_properties=required_saved_properties,
+ **kwargs):
+ if "settings_properties" not in kwargs:
+ kwargs["settings_properties"] = settings_properties
+ if "required_saved_properties" not in kwargs:
+ kwargs["required_saved_properties"]=required_saved_properties
+ return versioned_property(**kwargs)
+ @_versioned_property(name="Content-type",
+ doc="A test property",
+ default="text/plain",
+ require_save=True)
+ def content_type(): return {}
+ def __init__(self):
+ SavedSettingsObject.__init__(self)
+ t = Test()
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"},
+ t._get_saved_settings())
+ t.content_type = "text/html"
+ self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"},
+ t._get_saved_settings())
+
+unitsuite=unittest.TestLoader().loadTestsFromTestCase(SavedSettingsObjectTests)
+suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])