From: W. Trevor King <wking@drexel.edu> Date: Fri, 19 Jun 2009 18:42:15 +0000 (-0400) Subject: Merged Thomas Habets 2009-01-07 XML output for "be show". X-Git-Tag: 1.0.0~84 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=89b2ba997492895328203c3d80cfd8a66dd17363;p=be.git Merged Thomas Habets 2009-01-07 XML output for "be show". I rewrote a few of his routines, e.g. generalizing Comment.string_thread to run a caller-specified method avoided the need for some duplicate code in Comment.xml_thread. There was also a reasonable reorganization of libbe.settings_object.versioned_property because the <in_reply_to> field of the Comment.xml output was giving me `-1' (= old settings_object.EMPTY) instead of None, even after I had set comm.in_reply_to to None. The rewritten versioned_property avoids the ambiguity of UNPRIMED vs EMPTY, and avoids the stupididy of my using EMPTY=-1 ;). --- 89b2ba997492895328203c3d80cfd8a66dd17363 diff --cc becommands/show.py index 87b890f,1ee354c..7c48257 --- a/becommands/show.py +++ b/becommands/show.py @@@ -35,22 -34,22 +35,40 @@@ def execute(args, test=False) Created : Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000) Bug A <BLANKLINE> ++ >>> execute (["--xml", "a"], test=True) ++ <bug> ++ <uuid>a</uuid> ++ <short-name>a</short-name> ++ <severity>minor</severity> ++ <status>open</status> ++ <assigned><class 'libbe.settings_object.EMPTY'></assigned> ++ <target><class 'libbe.settings_object.EMPTY'></target> ++ <reporter><class 'libbe.settings_object.EMPTY'></reporter> ++ <creator>John Doe <jdoe@example.com></creator> ++ <created>Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000)</created> ++ <summary>Bug A</summary> ++ </bug> """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) if len(args) == 0: - raise cmdutil.UserError("Please specify a bug id.") - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) for bugid in args: bug = bd.bug_from_shortname(bugid) - print bug.string(show_comments=True) - if bugid != args[-1]: - print "" # add a blank line between bugs + if options.dumpXML: + print bug.xml(show_comments=True) + else: + print bug.string(show_comments=True) ++ if bugid != args[-1]: ++ print "" # add a blank line between bugs def get_parser(): - parser = cmdutil.CmdOptionParser("be show BUG-ID [BUG-ID ...]") + parser = cmdutil.CmdOptionParser("be show [options] BUG-ID [BUG-ID ...]") + parser.add_option("-x", "--xml", action="store_true", + dest='dumpXML', help="Dump as XML") return parser longhelp=""" diff --cc libbe/bug.py index f871c7a,afa9e09..fe059fa --- a/libbe/bug.py +++ b/libbe/bug.py @@@ -228,13 -128,43 +228,49 @@@ class Bug(settings_object.SavedSettings def __repr__(self): return "Bug(uuid=%r)" % self.uuid + def _setting_attr_string(self, setting): + value = getattr(self, setting) + if value == settings_object.EMPTY: + return "" + else: + return str(value) + + def xml(self, show_comments=False): + if self.bugdir == None: + shortname = self.uuid + else: + shortname = self.bugdir.bug_shortname(self) + + if self.time == None: + timestring = "" + else: + htime = utility.handy_time(self.time) + ftime = utility.time_to_str(self.time) + timestring = "%s (%s)" % (htime, ftime) + + info = [("uuid", self.uuid), + ("short-name", shortname), + ("severity", self.severity), + ("status", self.status), + ("assigned", self.assigned), + ("target", self.target), ++ ("reporter", self.reporter), + ("creator", self.creator), + ("created", timestring), + ("summary", self.summary)] + ret = '<bug>\n' + for (k,v) in info: + if v is not None: + ret += ' <%s>%s</%s>\n' % (k,v,k) + - if show_comments: - if self._comments_loaded == False: - self.load_comments() ++ if show_comments == True: + comout = self.comment_root.xml_thread(auto_name_map=True, + bug_shortname=shortname) + ret += comout + + ret += '</bug>' + return ret + def string(self, shortlist=False, show_comments=False): if self.bugdir == None: shortname = self.uuid diff --cc libbe/comment.py index cb5ea59,87c1de0..e5c86c7 --- a/libbe/comment.py +++ b/libbe/comment.py @@@ -209,13 -118,32 +209,45 @@@ class Comment(Tree, settings_object.Sav continue yield comment - def _clean_string(self, value): - """ - >>> comm = Comment() - >>> comm._clean_string(None) - '' - >>> comm._clean_string("abc") - 'abc' - """ - if value == None: + def _setting_attr_string(self, setting): + value = getattr(self, setting) + if value == settings_object.EMPTY: return "" - return value + else: + return str(value) + def xml(self, indent=0, shortname=None): ++ """ ++ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") ++ >>> comm.uuid = "0123" ++ >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" ++ >>> print comm.xml(indent=2, shortname="com-1") ++ <comment> ++ <name>com-1</name> ++ <uuid>0123</uuid> ++ <from></from> ++ <date>Thu, 01 Jan 1970 00:00:00 +0000</date> ++ <body>Some ++ insightful ++ remarks</body> ++ </comment> ++ """ + if shortname == None: + shortname = self.uuid - ret = """<comment> - <name>%s</name> - <from>%s</from> - <date>%s</date> - <body>%s</body> -</comment>\n""" % (shortname, - self._clean_string(self.From), - utility.time_to_str(self.time), - self.body.rstrip('\n')) - return ret ++ lines = ["<comment>", ++ " <name>%s</name>" % (shortname,), ++ " <uuid>%s</uuid>" % self.uuid,] ++ if self.in_reply_to != None: ++ lines.append(" <in_reply_to>%s</in_reply_to>" % self.in_reply_to) ++ lines.extend([ ++ " <from>%s</from>" % self._setting_attr_string("From"), ++ " <date>%s</date>" % self.time_string, ++ " <body>%s</body>" % (self.body or "").rstrip('\n'), ++ "</comment>\n"]) ++ istring = ' '*indent ++ sep = '\n' + istring ++ return istring + sep.join(lines).rstrip('\n') + def string(self, indent=0, shortname=None): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") @@@ -310,22 -249,13 +342,28 @@@ """ reply = Comment(self.bug, body=body) self.add_reply(reply) + #raise Exception, "new reply added (%s),\n%s\n%s\n\t--%s--" % (body, self, reply, reply.in_reply_to) return reply - def string_thread(self, name_map={}, indent=0, flatten=True, - def string_thread(self, name_map={}, indent=0, ++ def string_thread(self, string_method_name="string", name_map={}, ++ indent=0, flatten=True, auto_name_map=False, bug_shortname=None): """ -- Return a sting displaying a thread of comments. ++ Return a string displaying a thread of comments. bug_shortname is only used if auto_name_map == True. + ++ string_method_name (defaults to "string") is the name of the ++ Comment method used to generate the output string for each ++ Comment in the thread. The method must take the arguments ++ indent and shortname. ++ + SIDE-EFFECT: if auto_name_map==True, calls comment_shortnames() + which will sort the tree by comment.time. Avoid by calling + name_map = {} + for shortname,comment in comm.comment_shortnames(bug_shortname): + name_map[comment.uuid] = shortname + comm.sort(key=lambda c : c.From) # your sort + comm.string_thread(name_map=name_map) >>> a = Comment(bug=None, uuid="a", body="Insightful remarks") >>> a.time = utility.str_to_time("Thu, 20 Nov 2008 01:00:00 +0000") @@@ -401,10 -331,27 +439,17 @@@ sname = name_map[comment.uuid] else: sname = None -- stringlist.append(comment.string(indent=ind, shortname=sname)) ++ string_fn = getattr(comment, string_method_name) ++ stringlist.append(string_fn(indent=ind, shortname=sname)) return '\n'.join(stringlist) + def xml_thread(self, name_map={}, indent=0, + auto_name_map=False, bug_shortname=None): - if auto_name_map == True: - name_map = {} - for shortname,comment in self.comment_shortnames(bug_shortname): - name_map[comment.uuid] = shortname - stringlist = [] - for depth,comment in self.thread(flatten=True): - ind = 2*depth+indent - if comment.uuid in name_map: - sname = name_map[comment.uuid] - else: - sname = None - stringlist.append(comment.xml(indent=ind, shortname=sname)) - return '\n'.join(stringlist) - ++ return self.string_thread(string_method_name="xml", name_map=name_map, ++ indent=indent, auto_name_map=auto_name_map, ++ bug_shortname=bug_shortname) + - def comment_shortnames(self, bug_shortname=""): + def comment_shortnames(self, bug_shortname=None): """ Iterate through (id, comment) pairs, in time order. (This is a user-friendly id, not the comment uuid). diff --cc libbe/properties.py index 176e898,0000000..a8e89fb mode 100644,000000..100644 --- a/libbe/properties.py +++ b/libbe/properties.py @@@ -1,477 -1,0 +1,479 @@@ +# 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 series of useful decorators for defining +various types of properties. For example usage, consider the +unittests at the end of the module. + +See + http://www.python.org/dev/peps/pep-0318/ +and + http://www.phyast.pitt.edu/~micheles/python/documentation.html +for more information on decorators. +""" + +import unittest + ++ +class ValueCheckError (ValueError): + def __init__(self, name, value, allowed): + msg = "%s not in %s for %s" % (value, allowed, name) + ValueError.__init__(self, msg) + self.name = name + self.value = value + self.allowed = allowed + +def Property(funcs): + """ + End a chain of property decorators, returning a property. + """ + args = {} + args["fget"] = funcs.get("fget", None) + args["fset"] = funcs.get("fset", None) + args["fdel"] = funcs.get("fdel", None) + args["doc"] = funcs.get("doc", None) + + #print "Creating a property with" + #for key, val in args.items(): print key, value + return property(**args) + +def doc_property(doc=None): + """ + Add a docstring to a chain of property decorators. + """ + def decorator(funcs=None): + """ + Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...} + or a function fn() returning such a dict. + """ + if hasattr(funcs, "__call__"): + funcs = funcs() # convert from function-arg to dict + funcs["doc"] = doc + return funcs + return decorator + - def local_property(name): ++def local_property(name, null=None): + """ + Define get/set access to per-parent-instance local storage. Uses + ._<name>_value to store the value for a particular owner instance. ++ If the ._<name>_value attribute does not exist, returns null. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget", None) + fset = funcs.get("fset", None) + def _fget(self): + if fget is not None: + fget(self) - value = getattr(self, "_%s_value" % name, None) ++ value = getattr(self, "_%s_value" % name, null) + return value + def _fset(self, value): + setattr(self, "_%s_value" % name, value) + if fset is not None: + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + funcs["name"] = name + return funcs + return decorator + - def settings_property(name): ++def settings_property(name, null=None): + """ + Similar to local_property, except where local_property stores the + value in instance._<name>_value, settings_property stores the + value in instance.settings[name]. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget", None) + fset = funcs.get("fset", None) + def _fget(self): + if fget is not None: + fget(self) - value = self.settings.get(name, None) ++ value = self.settings.get(name, null) + return value + def _fset(self, value): + self.settings[name] = value + if fset is not None: + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + funcs["name"] = name + return funcs + return decorator + +def defaulting_property(default=None, null=None): + """ + Define a default value for get access to a property. + If the stored value is null, then default is returned. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + def _fget(self): + value = fget(self) + if value == null: + return default + return value + funcs["fget"] = _fget + 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. + """ + 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 not in allowed: + raise ValueCheckError(name, value, allowed) + return value + def _fset(self, value): + if value not in allowed: + raise ValueCheckError(name, value, allowed) + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +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 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. + + 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__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "<unknown>") + def _fget(self): + cache = getattr(self, "_%s_cache" % name, True) + value = fget(self) + if cache == True: + if value == initVal: + if 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 + return decorator + +def primed_property(primer, initVal=None): + """ + Just like a generator_property, except that instead of returning a + new value and running fset to cache it, the primer performs some + background manipulation (e.g. loads data into instance.settings) + such that a _second_ pass through fget succeeds. + + The 'cache' flag becomes a 'prime' flag, with priming taking place + whenever ._<name>_prime is True, or is False or missing and + value == initVal. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + name = funcs.get("name", "<unknown>") + def _fget(self): + prime = getattr(self, "_%s_prime" % name, False) + if prime == False: + value = fget(self) + if prime == True or (prime == False and value == initVal): + primer(self) + value = fget(self) + return value + funcs["fget"] = _fget + return funcs + return decorator + +def change_hook_property(hook): + """ + Call the function hook(instance, old_value, new_value) whenever a + value different from the current value is set (instance is a a + reference to the class instance to which this property belongs). + This is useful for saving changes to disk, etc. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "<unknown>") + def _fset(self, value): + old_value = fget(self) + if value != old_value: + hook(self, old_value, value) + fset(self, value) + funcs["fset"] = _fset + return funcs + return decorator + + +class DecoratorTests(unittest.TestCase): + def testLocalDoc(self): + class Test(object): + @Property + @doc_property("A fancy property") + def x(): + return {} + self.failUnless(Test.x.__doc__ == "A fancy property", + Test.x.__doc__) + def testLocalProperty(self): + class Test(object): + @Property + @local_property(name="LOCAL") + def x(): + return {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("_LOCAL_value" in dir(t), dir(t)) + self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value) + def testSettingsProperty(self): + class Test(object): + @Property + @settings_property(name="attr") + def x(): + return {} + def __init__(self): + self.settings = {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("attr" in t.settings, t.settings) + self.failUnless(t.settings["attr"] == 'z', t.settings["attr"]) + def testDefaultingLocalProperty(self): + class Test(object): + @Property + @defaulting_property(default='y', null='x') + @local_property(name="DEFAULT") + def x(): return {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'x' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'y' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'z' + self.failUnless(t.x == 'z', str(t.x)) + def testCheckedLocalProperty(self): + class Test(object): + @Property + @checked_property(allowed=['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 testTwoCheckedLocalProperties(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="X") + def x(): return {} + + @Property + @checked_property(allowed=['a', 'b', 'c']) + @local_property(name="A") + def a(): return {} + def __init__(self): + self._A_value = 'a' + self._X_value = 'x' + t = Test() + try: + t.x = 'a' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.x = 'x' + t.x = 'y' + t.x = 'z' + try: + t.a = 'x' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + 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): + self.i = 0 + def __call__(self, owner): + self.i += 1 + return self.i + class Test(object): + @Property + @cached_property(generator=Gen(), initVal=None) + @local_property(name="CACHED") + def x(): return {} + t = Test() + self.failIf("_CACHED_cache" in dir(t), getattr(t, "_CACHED_cache", None)) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + t.x = 8 + self.failUnless(t.x == 8, t.x) + self.failUnless(t.x == 8, t.x) + 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 # 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): + self.settings["PRIMED"] = "initialized" + @Property + @primed_property(primer=prime, initVal=None) + @settings_property(name="PRIMED") + def x(): return {} + def __init__(self): + self.settings={} + t = Test() + self.failIf("_PRIMED_prime" in dir(t), getattr(t, "_PRIMED_prime", None)) + self.failUnless(t.x == "initialized", t.x) + t.x = 1 + self.failUnless(t.x == 1, t.x) + t.x = None + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = True + t.x = 3 + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = False + t.x = 3 + self.failUnless(t.x == 3, t.x) + def testChangeHookLocalProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + + @Property + @change_hook_property(_hook) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 2 + self.failUnless(t.old == 1, t.old) + self.failUnless(t.new == 2, t.new) + +suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) + diff --cc libbe/settings_object.py index 8b0ff47,0000000..1df3e6b mode 100644,000000..100644 --- a/libbe/settings_object.py +++ b/libbe/settings_object.py @@@ -1,267 -1,0 +1,353 @@@ +# 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 ++ ++class _Token (object): ++ """ ++ `Control' value class for properties. We want values that only ++ mean something to the settings_object module. ++ """ ++ pass ++ ++class UNPRIMED (_Token): ++ "Property has not been primed." ++ pass ++ ++class EMPTY (_Token): ++ """ ++ Property has been primed but has no user-set value, so use ++ default/generator value. ++ """ ++ pass + + +def prop_save_settings(self, old, new): ++ """ ++ The default action undertaken when a property changes. ++ """ + if self.sync_with_disk==True: + self.save_settings() ++ +def prop_load_settings(self): ++ """ ++ The default action undertaken when an UNPRIMED property is accessed. ++ """ + if self.sync_with_disk==True and self._settings_loaded==False: + self.load_settings() + else: + self._setup_saved_settings(flag_as_loaded=False) + ++# Some name-mangling routines for pretty printing setting names +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. ++ determine a valid default at run time. If both default and ++ generator are None, then the property will be a defaulting ++ property which defaults to None. + + 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) ++ primed = primed_property(primer=primer, initVal=UNPRIMED) ++ settings = settings_property(name=name, null=UNPRIMED) + 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.""" ++ """ ++ To be run after setting self.settings up from disk. Marks all ++ settings as primed. ++ """ + for property in self.settings_properties: + if property not in self.settings: + self.settings[property] = EMPTY - elif self.settings[property] == None: ++ elif self.settings[property] == UNPRIMED: + 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 testSimpleProperty(self): ++ class Test(SavedSettingsObject): ++ settings_properties = [] ++ required_saved_properties = [] ++ @versioned_property(name="Content-type", ++ doc="A test property", ++ settings_properties=settings_properties, ++ required_saved_properties=required_saved_properties) ++ def content_type(): return {} ++ def __init__(self): ++ SavedSettingsObject.__init__(self) ++ t = Test() ++ # access missing setting ++ self.failUnless(t._settings_loaded == False, t._settings_loaded) ++ self.failUnless(len(t.settings) == 0, len(t.settings)) ++ self.failUnless(t.content_type == EMPTY, t.content_type) ++ # accessing t.content_type triggers the priming, which runs ++ # t._setup_saved_settings, which fills out t.settings with ++ # EMPTY data. t._settings_loaded is still false though, since ++ # the default priming does not do any of the `official' loading ++ # that occurs in t.load_settings. ++ self.failUnless(len(t.settings) == 1, len(t.settings)) ++ self.failUnless(t.settings["Content-type"] == EMPTY, ++ t.settings["Content-type"]) ++ self.failUnless(t._settings_loaded == False, t._settings_loaded) ++ # load settings creates an EMPTY value in the settings array ++ t.load_settings() ++ self.failUnless(t._settings_loaded == True, t._settings_loaded) ++ self.failUnless(t.settings["Content-type"] == EMPTY, ++ t.settings["Content-type"]) ++ self.failUnless(t.content_type == EMPTY, t.content_type) ++ self.failUnless(len(t.settings) == 1, len(t.settings)) ++ self.failUnless(t.settings["Content-type"] == EMPTY, ++ t.settings["Content-type"]) ++ # now we set a value ++ t.content_type = None ++ self.failUnless(t.settings["Content-type"] == None, ++ t.settings["Content-type"]) ++ self.failUnless(t.content_type == None, t.content_type) ++ self.failUnless(t.settings["Content-type"] == None, ++ t.settings["Content-type"]) ++ # now we set another value ++ t.content_type = "text/plain" ++ self.failUnless(t.content_type == "text/plain", t.content_type) ++ self.failUnless(t.settings["Content-type"] == "text/plain", ++ t.settings["Content-type"]) ++ self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, ++ t._get_saved_settings()) ++ # now we clear to the post-primed value ++ t.content_type = EMPTY ++ self.failUnless(t._settings_loaded == True, t._settings_loaded) ++ self.failUnless(t.settings["Content-type"] == EMPTY, ++ t.settings["Content-type"]) ++ self.failUnless(t.content_type == EMPTY, t.content_type) ++ self.failUnless(len(t.settings) == 1, len(t.settings)) ++ self.failUnless(t.settings["Content-type"] == EMPTY, ++ t.settings["Content-type"]) + 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()])