Added Bug.from_xml() + some .from_xml() fixups.
authorW. Trevor King <wking@drexel.edu>
Fri, 20 Nov 2009 22:09:08 +0000 (17:09 -0500)
committerW. Trevor King <wking@drexel.edu>
Fri, 20 Nov 2009 22:09:08 +0000 (17:09 -0500)
Moved comment.InvalidXML to utility.InvalidXML, so that bug and
comment can share it.  Added docstring explaining the __init__
arguments.

Added indent and shortname options to Bug.xml() to match
Comment.xml().

Added .extra_strings export to Comment.xml().

Converted Bug.xml() from string addition to list joining, which avoids
a bunch of memory allocation/deallocation.

Assorted " -> ' replacements.

Elaborated doctests to check UTF-8, extra_strings, ...

Added new comparison cmp_extra_strings for both bug. and
comment.DEFAULT_CMP_FULL_CMP_LIST.

libbe/bug.py
libbe/comment.py
libbe/utility.py

index 48f8358f5d3144afb4208fd0aeab0dbf5408d1e4..fecb9b70db6da1c921e30685eeaeba00a0977e3b 100644 (file)
@@ -25,6 +25,10 @@ import os.path
 import errno
 import time
 import types
+try: # import core module, Python >= 2.5
+    from xml.etree import ElementTree
+except ImportError: # look for non-core module
+    from elementtree import ElementTree
 import xml.sax.saxutils
 import doctest
 
@@ -271,40 +275,100 @@ class Bug(settings_object.SavedSettingsObject):
             return str(value)
         return value
 
-    def xml(self, show_comments=False):
-        if self.bugdir == None:
-            shortname = self.uuid
-        else:
-            shortname = self.bugdir.bug_shortname(self)
+    def xml(self, indent=0, shortname=None, show_comments=False):
+        if shortname == None:
+            if self.bugdir == None:
+                shortname = self.uuid
+            else:
+                shortname = self.bugdir.bug_shortname(self)
 
         if self.time == None:
             timestring = ""
         else:
             timestring = utility.time_to_str(self.time)
 
-        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'
+        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)]
+        lines = ['<bug>']
         for (k,v) in info:
             if v is not None:
-                ret += '  <%s>%s</%s>\n' % (k,xml.sax.saxutils.escape(v),k)
+                lines.append('  <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
         for estr in self.extra_strings:
-            ret += '  <extra-string>%s</extra-string>\n' % estr
+            lines.append('  <extra-string>%s</extra-string>\n' % estr)
         if show_comments == True:
-            comout = self.comment_root.xml_thread(auto_name_map=True,
+            comout = self.comment_root.xml_thread(indent=indent+2,
+                                                  auto_name_map=True,
                                                   bug_shortname=shortname)
             if len(comout) > 0:
-                ret += comout+'\n'
-        ret += '</bug>'
-        return ret
+                lines.append(comout)
+        lines.append('</bug>')
+        istring = ' '*indent
+        sep = '\n' + istring
+        return istring + sep.join(lines).rstrip('\n')
+
+    def from_xml(self, xml_string, verbose=True):
+        """
+        Note: If a bug uuid is given, set .alt_id to it's value.
+        >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
+        >>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000"
+        >>> bugA.creator = u'Fran\xe7ois'
+        >>> bugA.extra_strings += ['TAG: very helpful']
+        >>> commA = bugA.comment_root.new_reply(body='comment A')
+        >>> commB = bugA.comment_root.new_reply(body='comment B')
+        >>> commC = commA.new_reply(body='comment C')
+        >>> xml = bugA.xml(shortname="bug-1")
+        >>> bugB = Bug()
+        >>> bugB.from_xml(xml, verbose=True)
+        >>> bugB.xml(shortname="bug-1") == xml
+        False
+        >>> bugB.uuid = bugB.alt_id
+        >>> bugB.xml(shortname="bug-1") == xml
+        True
+        """
+        if type(xml_string) == types.UnicodeType:
+            xml_string = xml_string.strip().encode('unicode_escape')
+        bug = ElementTree.XML(xml_string)
+        if bug.tag != 'bug':
+            raise utility.InvalidXML( \
+                'bug', bug, 'root element must be <comment>')
+        tags=['uuid','short-name','severity','status','assigned','target',
+              'reporter', 'creator', 'created', 'summary', 'extra-string',
+              'comment']
+        uuid = None
+        estrs = []
+        for child in bug.getchildren():
+            if child.tag == 'short-name':
+                pass
+            elif child.tag in tags:
+                if child.text == None or len(child.text) == 0:
+                    text = settings_object.EMPTY
+                else:
+                    text = xml.sax.saxutils.unescape(child.text)
+                    text = text.decode('unicode_escape').strip()
+                if child.tag == "uuid":
+                    uuid = text
+                    continue # don't set the bug's uuid tag.
+                if child.tag == 'extra-string':
+                    estrs.append(text)
+                    continue # don't set the bug's extra_string yet.
+                else:
+                    attr_name = child.tag.replace('-','_')
+                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 not hasattr(self, 'alt_id') or self.alt_id == None:
+                self.alt_id = uuid
+        self.extra_strings = estrs
 
     def string(self, shortlist=False, show_comments=False):
         if self.bugdir == None:
@@ -525,6 +589,7 @@ cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
 cmp_target = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "target")
 cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter")
 cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary")
+cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings")
 # chronological rankings (newer < older)
 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
 
@@ -547,7 +612,8 @@ def cmp_comments(bug_1, bug_2):
 
 DEFAULT_CMP_FULL_CMP_LIST = \
     (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator,
-     cmp_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid)
+     cmp_reporter, cmp_target, cmp_comments, cmp_summary, cmp_uuid,
+     cmp_extra_strings)
 
 class BugCompoundComparator (object):
     def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
index 5f67878f5fe40ad8f97e7458ae66d4aea77ce3df..678d8badb8ffd71e8c6519f9f4068d839f2d69f4 100644 (file)
@@ -51,14 +51,6 @@ class InvalidShortname(KeyError):
         self.shortname = shortname
         self.shortnames = shortnames
 
-class InvalidXML(ValueError):
-    def __init__(self, element, comment):
-        msg = "Invalid comment xml: %s\n  %s\n" \
-            % (comment, ElementTree.tostring(element))
-        ValueError.__init__(self, msg)
-        self.element = element
-        self.comment = comment
-
 class MissingReference(ValueError):
     def __init__(self, comment):
         msg = "Missing reference to %s" % (comment.in_reply_to)
@@ -331,27 +323,29 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         """
         if shortname == None:
             shortname = self.uuid
-        if self.content_type.startswith("text/"):
-            body = (self.body or "").rstrip('\n')
+        if self.content_type.startswith('text/'):
+            body = (self.body or '').rstrip('\n')
         else:
             maintype,subtype = self.content_type.split('/',1)
             msg = email.mime.base.MIMEBase(maintype, subtype)
-            msg.set_payload(self.body or "")
+            msg.set_payload(self.body or '')
             email.encoders.encode_base64(msg)
-            body = base64.encodestring(self.body or "")
-        info = [("uuid", self.uuid),
-                ("alt-id", self.alt_id),
-                ("short-name", shortname),
-                ("in-reply-to", self.in_reply_to),
-                ("author", self._setting_attr_string("author")),
-                ("date", self.date),
-                ("content-type", self.content_type),
-                ("body", body)]
-        lines = ["<comment>"]
+            body = base64.encodestring(self.body or '')
+        info = [('uuid', self.uuid),
+                ('alt-id', self.alt_id),
+                ('short-name', shortname),
+                ('in-reply-to', self.in_reply_to),
+                ('author', self._setting_attr_string('author')),
+                ('date', self.date),
+                ('content-type', self.content_type),
+                ('body', body)]
+        lines = ['<comment>']
         for (k,v) in info:
             if v != None:
                 lines.append('  <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
-        lines.append("</comment>")
+        for estr in self.extra_strings:
+            lines.append('  <extra-string>%s</extra-string>\n' % estr)
+        lines.append('</comment>')
         istring = ' '*indent
         sep = '\n' + istring
         return istring + sep.join(lines).rstrip('\n')
@@ -363,58 +357,61 @@ class Comment(Tree, settings_object.SavedSettingsObject):
         >>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
         >>> commA.uuid = "0123"
         >>> commA.date = "Thu, 01 Jan 1970 00:00:00 +0000"
+        >>> commA.author = u'Fran\xe7ois'
+        >>> commA.extra_strings += ['TAG: very helpful']
         >>> xml = commA.xml(shortname="com-1")
         >>> commB = Comment()
-        >>> commB.from_xml(xml)
-        >>> attrs=['uuid','alt_id','in_reply_to','author','date','content_type','body']
-        >>> for attr in attrs: # doctest: +ELLIPSIS
-        ...     if getattr(commB, attr) != getattr(commA, attr):
-        ...         estr = "Mismatch on %s: '%s' should be '%s'"
-        ...         args = (attr, getattr(commB, attr), getattr(commA, attr))
-        ...         print estr % args
-        Mismatch on uuid: '...' should be '0123'
-        Mismatch on alt_id: '0123' should be 'None'
-        >>> print commB.alt_id
-        0123
-        >>> commA.author
-        >>> commB.author
+        >>> commB.from_xml(xml, verbose=True)
+        >>> commB.xml(shortname="com-1") == xml
+        False
+        >>> commB.uuid = commB.alt_id
+        >>> commB.alt_id = None
+        >>> commB.xml(shortname="com-1") == xml
+        True
         """
         if type(xml_string) == types.UnicodeType:
-            xml_string = xml_string.strip().encode("unicode_escape")
+            xml_string = xml_string.strip().encode('unicode_escape')
         comment = ElementTree.XML(xml_string)
-        if comment.tag != "comment":
-            raise InvalidXML(comment, "root element must be <comment>")
-        tags=['uuid','alt-id','in-reply-to','author','date','content-type','body']
+        if comment.tag != 'comment':
+            raise utility.InvalidXML( \
+                'comment', comment, 'root element must be <comment>')
+        tags=['uuid','alt-id','in-reply-to','author','date','content-type',
+              'body','extra-string']
         uuid = None
         body = None
+        estrs = []
         for child in comment.getchildren():
-            if child.tag == "short-name":
+            if child.tag == 'short-name':
                 pass
             elif child.tag in tags:
                 if child.text == None or len(child.text) == 0:
                     text = settings_object.EMPTY
                 else:
                     text = xml.sax.saxutils.unescape(child.text)
-                    text = unicode(text).decode("unicode_escape").strip()
-                if child.tag == "uuid":
+                    text = text.decode('unicode_escape').strip()
+                if child.tag == 'uuid':
                     uuid = text
-                    continue # don't set the bug's uuid tag.
-                if child.tag == "body":
+                    continue # don't set the comment's uuid tag.
+                if child.tag == 'body':
                     body = text
-                    continue # don't set the bug's body yet.
+                    continue # don't set the comment's body yet.
+                if child.tag == 'extra-string':
+                    estrs.append(text)
+                    continue # don't set the comment's extra_string yet.
                 else:
                     attr_name = child.tag.replace('-','_')
                 setattr(self, attr_name, text)
             elif verbose == True:
-                print >> sys.stderr, "Ignoring unknown tag %s in %s" \
+                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]:
             self.alt_id = uuid
         if body != None:
-            if self.content_type.startswith("text/"):
-                self.body = body+"\n" # restore trailing newline
+            if self.content_type.startswith('text/'):
+                self.body = body+'\n' # restore trailing newline
             else:
                 self.body = base64.decodestring(body)
+        self.extra_strings = estrs
 
     def string(self, indent=0, shortname=None):
         """
@@ -726,12 +723,14 @@ cmp_author = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "autho
 cmp_in_reply_to = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "in_reply_to")
 cmp_content_type = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "content_type")
 cmp_body = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "body")
+cmp_extra_strings = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "extra_strings")
 # chronological rankings (newer < older)
 cmp_time = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "time", invert=True)
 
+
 DEFAULT_CMP_FULL_CMP_LIST = \
     (cmp_time, cmp_author, cmp_content_type, cmp_body, cmp_in_reply_to,
-     cmp_uuid)
+     cmp_uuid, cmp_extra_strings)
 
 class CommentCompoundComparator (object):
     def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
index 4126913432b0118a5ee6e1e3d4f0eca708673aac..7510b1626e9d48d50bb6cf71f900c9a1876e67a7 100644 (file)
@@ -29,6 +29,21 @@ import time
 import types
 import doctest
 
+class InvalidXML(ValueError):
+    """
+    Invalid XML while parsing for a *.from_xml() method.
+    type    - string identifying *, e.g. "bug", "comment", ...
+    element - ElementTree.Element instance which caused the error
+    error   - string describing the error
+    """
+    def __init__(self, type, element, error):
+        msg = 'Invalid %s xml: %s\n  %s\n' \
+            % (type, error, ElementTree.tostring(element))
+        ValueError.__init__(self, msg)
+        self.type = type
+        self.element = element
+        self.error = error
+
 def search_parent_directories(path, filename):
     """
     Find the file (or directory) named filename in path or in any