Added decorator-style properties to libbe/comment.py.
authorW. Trevor King <wking@drexel.edu>
Mon, 1 Dec 2008 19:42:20 +0000 (14:42 -0500)
committerW. Trevor King <wking@drexel.edu>
Mon, 1 Dec 2008 19:42:20 +0000 (14:42 -0500)
Also some typo corrections and some reworking of bug/bugdir to better
support the lazier loading.

libbe/bug.py
libbe/bugdir.py
libbe/comment.py

index e04d4eea72f769de013a0c861ad3dee49e3879bb..5f0429e82093ca163fe71b7c996dbca9e962586e 100644 (file)
@@ -176,15 +176,15 @@ class Bug(object):
         self.time_string = utility.time_to_str(value)
     time = property(fget=_get_time,
                     fset=_set_time,
-                    doc="An integere version of .time_string")
+                    doc="An integer version of .time_string")
 
     @_versioned_property(name="summary",
                          doc="A one-line bug description")
     def summary(): return {}
 
-    def _get_comment_root(self):
+    def _get_comment_root(self, load_full=False):
         if self.sync_with_disk:
-            return comment.loadComments(self)
+            return comment.loadComments(self, load_full=load_full)
         else:
             return comment.Comment(self, uuid=comment.INVALID_UUID)
 
@@ -212,7 +212,6 @@ class Bug(object):
         self.settings = {}
         if from_disk == True:
             self.sync_with_disk = True
-            #self.load(load_comments=load_comments)
         else:
             self.sync_with_disk = False
             if uuid == None:
@@ -286,13 +285,18 @@ class Bug(object):
                 self.settings[property] = EMPTY
         self._settings_loaded = True
 
-    def load_comments(self):
-        # Clear _comment_root, so _get_comment_root returns a fresh
-        # version.  Turn of syncing temporarily so we don't write our
-        # blank comment tree to disk.
-        self.sync_with_disk = False
-        self._comment_root = None
-        self.sync_with_disk = True
+    def load_comments(self, load_full=True):
+        if load_full == True:
+            # Force a complete load of the whole comment tree
+            self.comment_root = self._get_comment_root(load_full=True)
+        else:
+            # Setup for fresh lazy-loading.  Clear _comment_root, so
+            # _get_comment_root returns a fresh version.  Turn of
+            # syncing temporarily so we don't write our blank comment
+            # tree to disk.
+            self.sync_with_disk = False
+            self.comment_root = None
+            self.sync_with_disk = True
 
     def save_settings(self):
         assert self.summary != None, "Can't save blank bug"
index 6bb6a433486624fc79c19089a8972e9753c65beb..1142e3d71400bbb6dd19202ab6a15afe5c15fb2b 100644 (file)
@@ -409,6 +409,7 @@ only saved if ._save_user_id == True""")
     def _clear_bugs(self):
         while len(self) > 0:
             self.pop()
+        self._bug_map_gen()
 
     def _load_bug(self, uuid):
         bg = bug.Bug(bugdir=self, uuid=uuid, from_disk=True)
@@ -567,6 +568,37 @@ class BugDirTestCase(unittest.TestCase):
         self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime))
         self.bugdir.save()
         self.versionTest()
+    def testComments(self):
+        self.bugdir.new_bug(uuid="a", summary="Ant")
+        bug = self.bugdir.bug_from_uuid("a")
+        comm = bug.comment_root
+        rep = comm.new_reply("Ants are small.")
+        rep.new_reply("And they have six legs.")
+        self.bugdir.save()
+        self.bugdir._clear_bugs()        
+        bug = self.bugdir.bug_from_uuid("a")
+        bug.load_comments()
+        self.failUnless(len(bug.comment_root)==1, len(bug.comment_root))
+        for index,comment in enumerate(bug.comments()):
+            if index == 0:
+                repLoaded = comment
+                self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid)
+                self.failUnless(comment.sync_with_disk == True,
+                                comment.sync_with_disk)
+                #load_settings()
+                self.failUnless(comment.content_type == "text/plain",
+                                comment.content_type)
+                self.failUnless(repLoaded.settings["Content-type"]=="text/plain",
+                                repLoaded.settings)
+                self.failUnless(repLoaded.body == "Ants are small.",
+                                repLoaded.body)
+            elif index == 1:
+                self.failUnless(comment.in_reply_to == repLoaded.uuid,
+                                repLoaded.uuid)
+                self.failUnless(comment.body == "And they have six legs.",
+                                comment.body)
+            else:
+                self.failIf(True, "Invalid comment: %d\n%s" % (index, comment))
 
 unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase)
 suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()])
index ab0973d5fe4619bed00cfa796c87882dc577fd02..0fd871c59ecbdf6e83ed067605a538a9f7ea53f2 100644 (file)
@@ -23,10 +23,22 @@ import textwrap
 import doctest
 
 from beuuid import uuid_gen
+from properties import Property, doc_property, local_property, \
+    defaulting_property, checked_property, cached_property, \
+    primed_property, change_hook_property, settings_property
 import mapfile
 from tree import Tree
 import utility
 
+
+class InvalidShortname(KeyError):
+    def __init__(self, shortname, shortnames):
+        msg = "Invalid shortname %s\n%s" % (shortname, shortnames)
+        KeyError.__init__(self, msg)
+        self.shortname = shortname
+        self.shortnames = shortnames
+
+
 INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!"
 
 def _list_to_root(comments, bug):
@@ -55,7 +67,11 @@ def _list_to_root(comments, bug):
     dummy_root.extend(root_comments)
     return dummy_root
 
-def loadComments(bug):
+def loadComments(bug, load_full=False):
+    """
+    Set load_full=True when you want to load the comment completely
+    from disk *now*, rather than waiting and lazy loading as required.
+    """
     path = bug.get_path("comments")
     if not os.path.isdir(path):
         return Comment(bug, uuid=INVALID_UUID)
@@ -64,6 +80,9 @@ def loadComments(bug):
         if uuid.startswith('.'):
             continue
         comm = Comment(bug, uuid, from_disk=True)
+        if load_full == True:
+            comm.load_settings()
+            dummy = comm.body # force the body to load
         comments.append(comm)
     return _list_to_root(comments, bug)
 
@@ -73,15 +92,108 @@ def saveComments(bug):
     for comment in bug.comment_root.traverse():
         comment.save()
 
-class InvalidShortname(KeyError):
-    def __init__(self, shortname, shortnames):
-        msg = "Invalid shortname %s\n%s" % (shortname, shortnames)
-        KeyError.__init__(self, msg)
-        self.shortname = shortname
-        self.shortnames = shortnames
 
+# 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 Comment(Tree):
+    """
+    >>> c = Comment()
+    >>> c.uuid != None
+    True
+    >>> c.uuid = "some-UUID"
+    >>> print c.content_type
+    text/plain
+    """
+    
+    def _save_settings(self, old, new):
+        if self.sync_with_disk==True:
+            self.save_settings()
+    def _load_settings(self):
+        if self.sync_with_disk==True and self._settings_loaded==False:
+            self.load_settings()
+        else:
+            for property in self.settings_properties:
+                if property not in self.settings:
+                    self.settings[property] = EMPTY
+
+    settings_properties = []
+    required_saved_properties = ['Content-type'] # to protect against future changes in default values
+    def _setting_name_to_attr_name(self, name):
+        "Helper for looking up default vals for required-saved-properties"
+        return name.lower().replace('-', '_')
+
+    def _versioned_property(name, doc, default=None, save=_save_settings, load=_load_settings, setprops=settings_properties, allowed=None):
+        "Combine the common decorators in a single function"
+        setprops.append(name)
+        def decorator(funcs):
+            if allowed != None:
+                checked = checked_property(allowed=allowed)
+            defaulting  = defaulting_property(default=default, null=EMPTY)
+            change_hook = change_hook_property(hook=save)
+            primed      = primed_property(primer=load)
+            settings    = settings_property(name=name)
+            docp        = doc_property(doc=doc)
+            deco = defaulting(change_hook(primed(settings(docp(funcs)))))
+            if allowed != None:
+                deco = checked(deco)
+            return Property(deco)
+        return decorator
+
+    @_versioned_property(name="From",
+                         doc="The author of the comment")
+    def From(): return {}
+
+    @_versioned_property(name="In-reply-to",
+                         doc="UUID for parent comment or bug")
+    def in_reply_to(): return {}
+
+    @_versioned_property(name="Content-type",
+                         doc="Mime type for comment body",
+                         default="text/plain")
+    def content_type(): return {}
+
+    @_versioned_property(name="Date",
+                         doc="An RFC 2822 timestamp for comment creation")
+    def time_string(): return {}
+
+    def _get_time(self):
+        if self.time_string == None:
+            return None
+        return utility.str_to_time(self.time_string)
+    def _set_time(self, value):
+        self.time_string = utility.time_to_str(value)
+    time = property(fget=_get_time,
+                    fset=_set_time,
+                    doc="An integer version of .time_string")
+
+    def _get_comment_body(self):
+        if self.rcs != None and self.sync_with_disk == True:
+            import rcs
+            return self.rcs.get_file_contents(self.get_path("body"))
+    def _set_comment_body(self, value, force=False):
+        if (self.rcs != None and self.sync_with_disk == True) or force==True:
+            assert value != None, "Can't save empty comment"
+            self.rcs.set_file_contents(self.get_path("body"), value)
+
+    @Property
+    @change_hook_property(hook=_set_comment_body)
+    @cached_property(generator=_get_comment_body)
+    @local_property("body")
+    @doc_property(doc="The meat of the comment")
+    def body(): return {}
+
+    def _get_rcs(self):
+        if hasattr(self.bug, "rcs"):
+            return self.bug.rcs
+
+    @Property
+    @cached_property(generator=_get_rcs)
+    @local_property("rcs")
+    @doc_property(doc="A revision control system instance.")
+    def rcs(): return {}
+
     def __init__(self, bug=None, uuid=None, from_disk=False,
                  in_reply_to=None, body=None):
         """
@@ -98,25 +210,19 @@ class Comment(Tree):
         """
         Tree.__init__(self)
         self.bug = bug
-        if bug != None:
-            self.rcs = bug.rcs
-        else:
-            self.rcs = None
+        self.uuid = uuid 
+        self._settings_loaded = False
+        self.settings = {}
         if from_disk == True: 
-            self.uuid = uuid 
-            self.load()
+            self.sync_with_disk = True
         else:
-            if uuid != None:
-                self.uuid = uuid
-            else:
+            self.sync_with_disk = False
+            if uuid == None:
                 self.uuid = uuid_gen()
-            self.time = time.time()
+            self.time = int(time.time()) # only save to second precision
             if self.rcs != None:
                 self.From = self.rcs.get_user_id()
-            else:
-                self.From = None
             self.in_reply_to = in_reply_to
-            self.content_type = "text/plain"
             self.body = body
 
     def traverse(self, *args, **kwargs):
@@ -126,22 +232,10 @@ class Comment(Tree):
                 continue
             yield comment
 
-    def _clean_string(self, value):
-        """
-        >>> comm = Comment()
-        >>> comm._clean_string(None)
-        ''
-        >>> comm._clean_string("abc")
-        'abc'
-        """
-        if value == None:
-            return ""
-        return value
-
     def string(self, indent=0, shortname=None):
         """
         >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
-        >>> comm.time = utility.str_to_time("Thu, 01 Jan 1970 00:00:00 +0000")
+        >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000"
         >>> print comm.string(indent=2, shortname="com-1")
           --------- Comment ---------
           Name: com-1
@@ -157,12 +251,12 @@ class Comment(Tree):
         lines = []
         lines.append("--------- Comment ---------")
         lines.append("Name: %s" % shortname)
-        lines.append("From: %s" % self._clean_string(self.From))
-        lines.append("Date: %s" % utility.time_to_str(self.time))
+        lines.append("From: %s" % (self.From or ""))
+        lines.append("Date: %s" % self.time_string)
         lines.append("")
-        #lines.append(textwrap.fill(self._clean_string(self.body),
+        #lines.append(textwrap.fill(self.body or "",
         #                           width=(79-indent)))
-        lines.extend(self._clean_string(self.body).splitlines())
+        lines.extend((self.body or "").splitlines())
         # some comments shouldn't be wrapped...
         
         istring = ' '*indent
@@ -173,7 +267,7 @@ class Comment(Tree):
         """
         >>> comm = Comment(bug=None, body="Some insightful remarks")
         >>> comm.uuid = "com-1"
-        >>> comm.time = utility.str_to_time("Thu, 20 Nov 2008 15:55:11 +0000")
+        >>> comm.time_string = "Thu, 20 Nov 2008 15:55:11 +0000"
         >>> comm.From = "Jane Doe <jdoe@example.com>"
         >>> print comm
         --------- Comment ---------
@@ -192,36 +286,34 @@ class Comment(Tree):
         assert name in ["values", "body"]
         return os.path.join(my_dir, name)
 
-    def load(self):
-        map = mapfile.map_load(self.rcs, self.get_path("values"))
-        self.time = utility.str_to_time(map["Date"])
-        self.From = map["From"]
-        self.in_reply_to = map.get("In-reply-to")
-        self.content_type = map.get("Content-type", "text/plain")
-        self.body = self.rcs.get_file_contents(self.get_path("body"))
+    def load_settings(self):
+        self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
+        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
+        self._settings_loaded = True
 
-    def save(self):
-        assert self.rcs != None
-        map_file = {"Date": utility.time_to_str(self.time)}
-        self._add_headers(map_file, ("From", "in_reply_to", "content_type"))
+    def save_settings(self):
+        map = {}
+        for k,v in self.settings.items():
+            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))
+        
         self.rcs.mkdir(self.get_path())
-        mapfile.map_save(self.rcs, self.get_path("values"), map_file)
-        self.rcs.set_file_contents(self.get_path("body"), self.body)
-    def _add_headers(self, map, names):
-        map_names = {}
-        for name in names:
-            map_names[name] = self._pyname_to_header(name)
-        self._add_attrs(map, map_names)
-
-    def _pyname_to_header(self, name):
-        return name.capitalize().replace('_', '-')
-
-    def _add_attrs(self, map, map_names):
-        for name in map_names.keys():
-            value = getattr(self, name)
-            if value is not None:
-                map[map_names[name]] = value
+        path = self.get_path("values")
+        mapfile.map_save(self.rcs, path, map)
+
+    def save(self):
+        assert self.body != None, "Can't save blank comment"
+        #if self.in_reply_to == None:
+        #    raise Exception, str(self)+'\n'+str(self.settings)+'\n'+str(self._settings_loaded)
+        #assert self.in_reply_to != None, "Comment must be a reply to something"
+        self.save_settings()
+        self._set_comment_body(self.body, force=True)
 
     def remove(self):
         for comment in self.traverse():
@@ -232,15 +324,19 @@ class Comment(Tree):
         if self.uuid != INVALID_UUID:
             reply.in_reply_to = self.uuid
         self.append(reply)
+        #raise Exception, "adding reply \n%s\n%s" % (self, reply)
 
     def new_reply(self, body=None):
         """
         >>> comm = Comment(bug=None, body="Some insightful remarks")
         >>> repA = comm.new_reply("Critique original comment")
         >>> repB = repA.new_reply("Begin flamewar :p")
+        >>> repB.in_reply_to == repA.uuid
+        True
         """
         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,