Added decorator-style properties to bugdir. Created settings_object module.
authorW. Trevor King <wking@drexel.edu>
Tue, 2 Dec 2008 15:14:06 +0000 (10:14 -0500)
committerW. Trevor King <wking@drexel.edu>
Tue, 2 Dec 2008 15:14:06 +0000 (10:14 -0500)
settings_object.SavedSettingsObject encapsulates some of the common
settings functionality in the BE BugDir, Bug, and Comment classes.
It's a bit awkward due to the nature of scoping in python subclasses,
but it's better than reproducing this code in each of the above classes.
Now I need to move Bug and Comment over to *this* system ;).

becommands/set.py
libbe/bug.py
libbe/bugdir.py
libbe/comment.py
libbe/properties.py
libbe/settings_object.py [new file with mode: 0644]

index aef5eb33aca40d9c46c695978c0bdb268c706cdd..1103b7bf7c1fcfd89fda05a1900409ecd34ae5ae 100644 (file)
 #    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()
index 5f0429e82093ca163fe71b7c996dbca9e962586e..bb79d1d84971e89856db2430efa621d7d7ea2286 100644 (file)
@@ -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)
index 1142e3d71400bbb6dd19202ab6a15afe5c15fb2b..f93576f7ef6dd3cb7d2b9e5acf03dce564c49e79 100644 (file)
@@ -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()):
index 0fd871c59ecbdf6e83ed067605a538a9f7ea53f2..e3c0a124c646d3c9d0e5e05cc8e799db39bd37be 100644 (file)
@@ -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))
index f55dc0e89ada49e7e255a69163a0b4ce2d0acac5..176e8985629d203762097815c42c9468972d00c1 100644 (file)
@@ -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 (file)
index 0000000..8b0ff47
--- /dev/null
@@ -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()])