Merged Thomas Habets 2009-01-07 XML output for "be show".
authorW. Trevor King <wking@drexel.edu>
Fri, 19 Jun 2009 18:42:15 +0000 (14:42 -0400)
committerW. Trevor King <wking@drexel.edu>
Fri, 19 Jun 2009 18:42:15 +0000 (14:42 -0400)
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 ;).

1  2 
becommands/show.py
libbe/bug.py
libbe/comment.py
libbe/properties.py
libbe/settings_object.py

index 87b890f4fa94bcb57b8b62b9e2d02eb3ca5461b9,1ee354cf770c144e50e8b937a223dc357e83e459..7c4825781ab821463ef2c1c743cb5967676517ac
@@@ -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 f871c7ac4a5457310b2fb8ce5bdc4f2a2cbc5981,afa9e09b4fb39c834c24316bbf062cc2c5fe3d21..fe059fa91a4998fa9193e36f20a09ec47a3c3fad
@@@ -228,13 -128,43 +228,49 @@@ class Bug(settings_object.SavedSettings
      def __repr__(self):
          return "Bug(uuid=%r)" % self.uuid
  
 -        if show_comments:
 -            if self._comments_loaded == False:
 -                self.load_comments()
 +    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 == 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
index cb5ea59f7243e8fa217b56a1f815dd17f010537c,87c1de044b051c8098e661f714fd61e113437204..e5c86c7003bb6851084ef0bbcf2249efe04b4be2
@@@ -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)
  
 -        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
+     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
++        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")
          """
          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")
                  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)
  
 -        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)
 -        
+     def xml_thread(self, name_map={}, indent=0,
+                    auto_name_map=False, bug_shortname=None):
 -    def comment_shortnames(self, bug_shortname=""):
++        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=None):
          """
          Iterate through (id, comment) pairs, in time order.
          (This is a user-friendly id, not the comment uuid).
index 176e8985629d203762097815c42c9468972d00c1,0000000000000000000000000000000000000000..a8e89fb62656301bf8084037339292592a0eb101
mode 100644,000000..100644
--- /dev/null
@@@ -1,477 -1,0 +1,479 @@@
- def local_property(name):
 +# 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
 +
-             value = getattr(self, "_%s_value" % name, None)
++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)
- def settings_property(name):
++            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
 +
-             value = self.settings.get(name, None)
++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, 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)
 +
index 8b0ff47c0b5cab352d1a3106ff05bd0cc1a403e9,0000000000000000000000000000000000000000..1df3e6ba72a92563b14abb4cc0c2a632d5b3818e
mode 100644,000000..100644
--- /dev/null
@@@ -1,267 -1,0 +1,353 @@@
- # Define an invalid value for our properties, distinct from None,
- # which shows that a property has been initialized but has no value.
- EMPTY = -1
 +# 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
 +
-     determine a valid default at run time.
++
++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
-         primed      = primed_property(primer=primer)
-         settings    = settings_property(name=name)
++    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)
-         """To be run after setting self.settings up from disk."""
++        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):
-             elif self.settings[property] == None:
++        """
++        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] == 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()])