Added Bug.merge() and Comment.merge().
authorW. Trevor King <wking@drexel.edu>
Sat, 28 Nov 2009 12:41:13 +0000 (07:41 -0500)
committerW. Trevor King <wking@drexel.edu>
Sat, 28 Nov 2009 12:41:13 +0000 (07:41 -0500)
Added *.explicit_attrs list creation to Bug and Comment.from_xml().

Added match_alt_id keyword argumennt to .comment_from_uuid().

Removed extra enline following '</extra-string>' tag in Bug and
Comment.xml().

libbe/bug.py
libbe/comment.py

index fecb9b70db6da1c921e30685eeaeba00a0977e3b..23d5488198cdf28f59a31a94753eddee4ac5c8f6 100644 (file)
@@ -20,6 +20,7 @@
 Define the Bug class for representing bugs.
 """
 
+import copy
 import os
 import os.path
 import errno
@@ -302,7 +303,7 @@ class Bug(settings_object.SavedSettingsObject):
             if v is not None:
                 lines.append('  <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
         for estr in self.extra_strings:
-            lines.append('  <extra-string>%s</extra-string>\n' % estr)
+            lines.append('  <extra-string>%s</extra-string>' % estr)
         if show_comments == True:
             comout = self.comment_root.xml_thread(indent=indent+2,
                                                   auto_name_map=True,
@@ -332,6 +333,8 @@ class Bug(settings_object.SavedSettingsObject):
         >>> bugB.uuid = bugB.alt_id
         >>> bugB.xml(shortname="bug-1") == xml
         True
+        >>> bugB.explicit_attrs  # doctest: +NORMALIZE_WHITESPACE
+        ['uuid', 'severity', 'status', 'creator', 'created', 'summary']
         """
         if type(xml_string) == types.UnicodeType:
             xml_string = xml_string.strip().encode('unicode_escape')
@@ -340,8 +343,9 @@ class Bug(settings_object.SavedSettingsObject):
             raise utility.InvalidXML( \
                 'bug', bug, 'root element must be <comment>')
         tags=['uuid','short-name','severity','status','assigned','target',
-              'reporter', 'creator', 'created', 'summary', 'extra-string',
+              'reporter', 'creator','created','summary','extra-string',
               'comment']
+        self.explicit_attrs = []
         uuid = None
         estrs = []
         for child in bug.getchildren():
@@ -353,23 +357,120 @@ class Bug(settings_object.SavedSettingsObject):
                 else:
                     text = xml.sax.saxutils.unescape(child.text)
                     text = text.decode('unicode_escape').strip()
-                if child.tag == "uuid":
+                if child.tag == 'uuid':
                     uuid = text
                     continue # don't set the bug's uuid tag.
-                if child.tag == 'extra-string':
+                elif child.tag == 'extra-string':
                     estrs.append(text)
                     continue # don't set the bug's extra_string yet.
-                else:
-                    attr_name = child.tag.replace('-','_')
+                attr_name = child.tag.replace('-','_')
+                self.explicit_attrs.append(attr_name)
                 setattr(self, attr_name, text)
             elif verbose == True:
                 print >> sys.stderr, "Ignoring unknown tag %s in %s" \
                     % (child.tag, comment.tag)
-        if uuid not in [None, self.uuid]:
+        if uuid != self.uuid:
             if not hasattr(self, 'alt_id') or self.alt_id == None:
                 self.alt_id = uuid
         self.extra_strings = estrs
 
+    def merge(self, other, allow_changes=True, allow_new_comments=True):
+        """
+        Merge info from other into this bug.  Overrides any attributes
+        in self that are listed in other.explicit_attrs.
+        >>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()')
+        >>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
+        >>> bugA.creator = 'Frank'
+        >>> bugA.extra_strings += ['TAG: very helpful']
+        >>> bugA.extra_strings += ['TAG: favorite']
+        >>> commA = bugA.comment_root.new_reply(body='comment A')
+        >>> commA.uuid = 'uuid-commA'
+        >>> bugB = Bug(uuid='3210', summary='More tests for Bug.merge()')
+        >>> bugB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
+        >>> bugB.creator = 'John'
+        >>> bugB.explicit_attrs = ['creator', 'summary']
+        >>> bugB.extra_strings += ['TAG: very helpful']
+        >>> bugB.extra_strings += ['TAG: useful']
+        >>> commB = bugB.comment_root.new_reply(body='comment B')
+        >>> commB.uuid = 'uuid-commB'
+        >>> bugA.merge(bugB, allow_changes=False)
+        Traceback (most recent call last):
+          ...
+        ValueError: Merge would change creator "Frank"->"John" for bug 0123
+        >>> bugA.merge(bugB, allow_new_comments=False)
+        Traceback (most recent call last):
+          ...
+        ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123
+        >>> bugA.merge(bugB)
+        >>> print bugA.xml(show_comments=True)  # doctest: +ELLIPSIS
+        <bug>
+          <uuid>0123</uuid>
+          <short-name>0123</short-name>
+          <severity>minor</severity>
+          <status>open</status>
+          <creator>John</creator>
+          <created>...</created>
+          <summary>More tests for Bug.merge()</summary>
+          <extra-string>TAG: favorite</extra-string>
+          <extra-string>TAG: useful</extra-string>
+          <extra-string>TAG: very helpful</extra-string>
+          <comment>
+            <uuid>uuid-commA</uuid>
+            <short-name>0123:1</short-name>
+            <author></author>
+            <date>...</date>
+            <content-type>text/plain</content-type>
+            <body>comment A</body>
+          </comment>
+          <comment>
+            <uuid>uuid-commB</uuid>
+            <short-name>0123:2</short-name>
+            <author></author>
+            <date>...</date>
+            <content-type>text/plain</content-type>
+            <body>comment B</body>
+          </comment>
+        </bug>
+        """
+        for attr in other.explicit_attrs:
+            old = getattr(self, attr)
+            new = getattr(other, attr)
+            if old != new:
+                if allow_changes == True:
+                    setattr(self, attr, new)
+                else:
+                    raise ValueError, \
+                        'Merge would change %s "%s"->"%s" for bug %s' \
+                        % (attr, old, new, self.uuid)
+        if allow_changes == False and len(other.extra_strings) > 0:
+            raise ValueError, \
+                'Merge would change extra_strings for bug %s' % self.uuid
+        for estr in other.extra_strings:
+            if not estr in self.extra_strings:
+                self.extra_strings.append(estr)
+        import sys
+        for o_comm in other.comments():
+            s_comm = None
+            try:
+                s_comm = self.comment_root.comment_from_uuid(o_comm.uuid)
+            except KeyError, e:
+                try:
+                    s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id)
+                except KeyError, e:
+                    pass
+            if s_comm == None:
+                if allow_new_comments == False:
+                    raise ValueError, \
+                        'Merge would add comment %s (alt: %s) to bug %s' \
+                        % (o_comm.uuid, o_comm.alt_id, self.uuid)
+                o_comm_copy = copy.copy(o_comm)
+                o_comm_copy.bug = self
+                print >> sys.stderr, "add comment %s" % o_comm.uuid
+                self.comment_root.add_reply(o_comm_copy)
+            else:
+                print >> sys.stderr, "merge comment %s into %s" % (o_comm.uuid, s_comm.uuid)
+                s_comm.merge(o_comm, allow_changes=allow_changes)
+
     def string(self, shortlist=False, show_comments=False):
         if self.bugdir == None:
             shortname = self.uuid
@@ -493,8 +594,8 @@ class Bug(settings_object.SavedSettingsObject):
         return self.comment_root.comment_from_shortname(shortname,
                                                         *args, **kwargs)
 
-    def comment_from_uuid(self, uuid):
-        return self.comment_root.comment_from_uuid(uuid)
+    def comment_from_uuid(self, uuid, *args, **kwargs):
+        return self.comment_root.comment_from_uuid(uuid, *args, **kwargs)
 
     def comment_shortnames(self, shortname=None):
         """
index 5cc43c402e7e23f2c6d0f2509c0cd176250ed185..1adb6f4db27f7da587bfcfbd985aeb699f46b29d 100644 (file)
@@ -344,7 +344,7 @@ class Comment(Tree, settings_object.SavedSettingsObject):
             if v != None:
                 lines.append('  <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
         for estr in self.extra_strings:
-            lines.append('  <extra-string>%s</extra-string>\n' % estr)
+            lines.append('  <extra-string>%s</extra-string>' % estr)
         lines.append('</comment>')
         istring = ' '*indent
         sep = '\n' + istring
@@ -362,6 +362,8 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         >>> xml = commA.xml(shortname="com-1")
         >>> commB = Comment()
         >>> commB.from_xml(xml, verbose=True)
+        >>> commB.explicit_attrs
+        ['author', 'date', 'content_type', 'body', 'alt_id']
         >>> commB.xml(shortname="com-1") == xml
         False
         >>> commB.uuid = commB.alt_id
@@ -377,6 +379,7 @@ class Comment(Tree, settings_object.SavedSettingsObject):
                 'comment', comment, 'root element must be <comment>')
         tags=['uuid','alt-id','in-reply-to','author','date','content-type',
               'body','extra-string']
+        self.explicit_attrs = []
         uuid = None
         body = None
         estrs = []
@@ -392,19 +395,21 @@ class Comment(Tree, settings_object.SavedSettingsObject):
                 if child.tag == 'uuid':
                     uuid = text
                     continue # don't set the comment's uuid tag.
-                if child.tag == 'body':
+                elif child.tag == 'body':
                     body = text
+                    self.explicit_attrs.append(child.tag)
                     continue # don't set the comment's body yet.
-                if child.tag == 'extra-string':
+                elif child.tag == 'extra-string':
                     estrs.append(text)
                     continue # don't set the comment's extra_string yet.
-                else:
-                    attr_name = child.tag.replace('-','_')
+                attr_name = child.tag.replace('-','_')
+                self.explicit_attrs.append(attr_name)
                 setattr(self, attr_name, text)
             elif verbose == True:
                 print >> sys.stderr, 'Ignoring unknown tag %s in %s' \
                     % (child.tag, comment.tag)
-        if self.alt_id == None and uuid not in [None, self.uuid]:
+        if self.alt_id == None:
+            self.explicit_attrs.append('alt_id')
             self.alt_id = uuid
         if body != None:
             if self.content_type.startswith('text/'):
@@ -413,6 +418,58 @@ class Comment(Tree, settings_object.SavedSettingsObject):
                 self.body = base64.decodestring(body)
         self.extra_strings = estrs
 
+    def merge(self, other, allow_changes=True):
+        """
+        Merge info from other into this comment.  Overrides any
+        attributes in self that are listed in other.explicit_attrs.
+        >>> commA = Comment(bug=None, body='Some insightful remarks')
+        >>> commA.uuid = '0123'
+        >>> commA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
+        >>> commA.author = 'Frank'
+        >>> commA.extra_strings += ['TAG: very helpful']
+        >>> commA.extra_strings += ['TAG: favorite']
+        >>> commB = Comment(bug=None, body='More insightful remarks')
+        >>> commB.uuid = '3210'
+        >>> commB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
+        >>> commB.author = 'John'
+        >>> commB.explicit_attrs = ['author', 'body']
+        >>> commB.extra_strings += ['TAG: very helpful']
+        >>> commB.extra_strings += ['TAG: useful']
+        >>> commA.merge(commB, allow_changes=False)
+        Traceback (most recent call last):
+          ...
+        ValueError: Merge would change author "Frank"->"John" for comment 0123
+        >>> commA.merge(commB)
+        >>> print commA.xml()
+        <comment>
+          <uuid>0123</uuid>
+          <short-name>0123</short-name>
+          <author>John</author>
+          <date>Thu, 01 Jan 1970 00:00:00 +0000</date>
+          <content-type>text/plain</content-type>
+          <body>More insightful remarks</body>
+          <extra-string>TAG: favorite</extra-string>
+          <extra-string>TAG: useful</extra-string>
+          <extra-string>TAG: very helpful</extra-string>
+        </comment>
+        """
+        for attr in other.explicit_attrs:
+            old = getattr(self, attr)
+            new = getattr(other, attr)
+            if old != new:
+                if allow_changes == True:
+                    setattr(self, attr, new)
+                else:
+                    raise ValueError, \
+                        'Merge would change %s "%s"->"%s" for comment %s' \
+                        % (attr, old, new, self.uuid)
+        if allow_changes == False and len(other.extra_strings) > 0:
+            raise ValueError, \
+                'Merge would change extra_strings for comment %s' % self.uuid
+        for estr in other.extra_strings:
+            if not estr in self.extra_strings:
+                self.extra_strings.append(estr)
+
     def string(self, indent=0, shortname=None):
         """
         >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
@@ -674,7 +731,7 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         raise InvalidShortname(comment_shortname,
                                list(self.comment_shortnames(*args, **kwargs)))
 
-    def comment_from_uuid(self, uuid):
+    def comment_from_uuid(self, uuid, match_alt_id=True):
         """
         Use a comment shortname to look up a comment.
         >>> a = Comment(bug=None, uuid="a")
@@ -684,13 +741,24 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         >>> c.uuid = "c"
         >>> d = a.new_reply()
         >>> d.uuid = "d"
+        >>> d.alt_id = "d-alt"
         >>> comm = a.comment_from_uuid("d")
         >>> id(comm) == id(d)
         True
+        >>> comm = a.comment_from_uuid("d-alt")
+        >>> id(comm) == id(d)
+        True
+        >>> comm = a.comment_from_uuid(None, match_alt_id=False)
+        Traceback (most recent call last):
+          ...
+        KeyError: None
         """
         for comment in self.traverse():
             if comment.uuid == uuid:
                 return comment
+            if match_alt_id == True and uuid != None \
+                    and comment.alt_id == uuid:
+                return comment
         raise KeyError(uuid)
 
 def cmp_attr(comment_1, comment_2, attr, invert=False):