diff options
author | W. Trevor King <wking@drexel.edu> | 2008-12-02 10:14:06 -0500 |
---|---|---|
committer | W. Trevor King <wking@drexel.edu> | 2008-12-02 10:14:06 -0500 |
commit | 6a94d050bbd72b3812fd7cb05445a66484103214 (patch) | |
tree | 55e8a3cd1d605132ccca46c3c5155e94ae0a6336 | |
parent | a98aafc8572bb826a0fda1b6bca0011fc4ef126a (diff) | |
download | bugseverywhere-6a94d050bbd72b3812fd7cb05445a66484103214.tar.gz |
Added decorator-style properties to bugdir. Created settings_object module.
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 ;).
-rw-r--r-- | becommands/set.py | 31 | ||||
-rw-r--r-- | libbe/bug.py | 2 | ||||
-rw-r--r-- | libbe/bugdir.py | 258 | ||||
-rw-r--r-- | libbe/comment.py | 2 | ||||
-rw-r--r-- | libbe/properties.py | 95 | ||||
-rw-r--r-- | libbe/settings_object.py | 267 |
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()]) |