Transition to libbe.LOG for logging
[be.git] / libbe / comment.py
index 21118f0a10b297ff7fb35ade88b919eb39f53498..9bef50af850a07379a788f3e42f8adaf783c68c7 100644 (file)
@@ -1,23 +1,25 @@
-# Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
+# Copyright (C) 2008-2012 Chris Ball <cjb@laptop.org>
+#                         Gianluca Montecchi <gian@grys.it>
+#                         Niall Douglas (s_sourceforge@nedprod.com) <spam@spamtrap.com>
 #                         Thomas Habets <thomas@habets.pp.se>
-#                         W. Trevor King <wking@drexel.edu>
+#                         W. Trevor King <wking@tremily.us>
 #
-# 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 2 of the License, or
-# (at your option) any later version.
+# This file is part of Bugs Everywhere.
 #
-# 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.
+# Bugs Everywhere 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 2 of the License, or (at your option) any
+# later version.
 #
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# Bugs Everywhere 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
+# Bugs Everywhere.  If not, see <http://www.gnu.org/licenses/>.
 
-"""
-Define the Comment class for representing bug comments.
+"""Define :py:class:`Comment` for representing bug comments.
 """
 
 import base64
@@ -53,13 +55,6 @@ if libbe.TESTING == True:
     import doctest
 
 
-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
-
 class MissingReference(ValueError):
     def __init__(self, comment):
         msg = "Missing reference to %s" % (comment.in_reply_to)
@@ -67,11 +62,6 @@ class MissingReference(ValueError):
         self.reference = comment.in_reply_to
         self.comment = comment
 
-class DiskAccessRequired (Exception):
-    def __init__(self, goal):
-        msg = "Cannot %s without accessing the disk" % goal
-        Exception.__init__(self, msg)
-
 INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!"
 
 def load_comments(bug, load_full=False):
@@ -97,11 +87,16 @@ def load_comments(bug, load_full=False):
 
 def save_comments(bug):
     for comment in bug.comment_root.traverse():
+        comment.bug = bug
+        comment.storage = bug.storage
         comment.save()
 
 
 class Comment (Tree, settings_object.SavedSettingsObject):
-    """
+    """Comments are a notes that attach to :py:class:`~libbe.bug.Bug`\s in
+    threaded trees.  In mailing-list terms, a comment is analogous to
+    a single part of an email.
+
     >>> c = Comment()
     >>> c.uuid != None
     True
@@ -162,8 +157,10 @@ class Comment (Tree, settings_object.SavedSettingsObject):
                 decode=self.content_type.startswith("text/"))
     def _set_comment_body(self, old=None, new=None, force=False):
         assert self.uuid != INVALID_UUID, self
-        if self.bug != None and self.bug.bugdir != None:
-            new = libbe.util.id.short_to_long_text([self.bug.bugdir], new)
+        if self.content_type.startswith('text/') \
+                and self.bug != None and self.bug.bugdir != None:
+            new = libbe.util.id.short_to_long_text(
+                {self.bug.bugdir.uuid: self.bug.bugdir}, new)
         if (self.storage != None and self.storage.writeable == True) \
                 or force==True:
             assert new != None, "Can't save empty comment"
@@ -191,18 +188,21 @@ class Comment (Tree, settings_object.SavedSettingsObject):
     def extra_strings(): return {}
 
     def __init__(self, bug=None, uuid=None, from_storage=False,
-                 in_reply_to=None, body=None):
+                 in_reply_to=None, body=None, content_type=None):
         """
-        Set from_storage=True to load an old comment.
-        Set from_storage=False to create a new comment.
+        Set ``from_storage=True`` to load an old comment.
+        Set ``from_storage=False`` to create a new comment.
 
-        The uuid option is required when from_storage==True.
+        The ``uuid`` option is required when ``from_storage==True``.
 
-        The in_reply_to and body options are only used if
-        from_storage==False (the default).  When from_storage==True,
-        they are loaded from the bug database.
+        The in_reply_to, body, and content_type options are only used
+        if ``from_storage==False`` (the default).  When
+        ``from_storage==True``, they are loaded from the bug database.
+        ``content_type`` decides if the body should be run through
+        :py:func:`util.id.short_to_long_text` before saving.  See
+        :py:meth:`_set_comment_body` for details.
 
-        in_reply_to should be the uuid string of the parent comment.
+        ``in_reply_to`` should be the uuid string of the parent comment.
         """
         Tree.__init__(self)
         settings_object.SavedSettingsObject.__init__(self)
@@ -215,6 +215,8 @@ class Comment (Tree, settings_object.SavedSettingsObject):
                 self.uuid = libbe.util.id.uuid_gen()
             self.time = int(time.time()) # only save to second precision
             self.in_reply_to = in_reply_to
+            if content_type != None:
+                self.content_type = content_type
             self.body = body
         if self.bug != None:
             self.storage = self.bug.storage
@@ -261,6 +263,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
     def safe_in_reply_to(self):
         """
         Return self.in_reply_to, except...
+
           * if no comment matches that id, in which case return None.
           * if that id matches another comments .alt_id, in which case
             return the matching comments .uuid.
@@ -291,6 +294,17 @@ class Comment (Tree, settings_object.SavedSettingsObject):
         insightful
         remarks</body>
           </comment>
+        >>> comm.content_type = 'image/png'
+        >>> print comm.xml()
+        <comment>
+          <uuid>0123</uuid>
+          <short-name>//012</short-name>
+          <author></author>
+          <date>Thu, 01 Jan 1970 00:00:00 +0000</date>
+          <content-type>image/png</content-type>
+          <body>U29tZQppbnNpZ2h0ZnVsCnJlbWFya3MK
+        </body>
+        </comment>
         """
         if self.content_type.startswith('text/'):
             body = (self.body or '').rstrip('\n')
@@ -319,8 +333,8 @@ class Comment (Tree, settings_object.SavedSettingsObject):
         sep = '\n' + istring
         return istring + sep.join(lines).rstrip('\n')
 
-    def from_xml(self, xml_string, verbose=True):
-        """
+    def from_xml(self, xml_string, preserve_uuids=False):
+        u"""
         Note: If alt-id is not given, translates any <uuid> fields to
         <alt-id> fields.
         >>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
@@ -330,7 +344,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
         >>> commA.extra_strings += ['TAG: very helpful']
         >>> xml = commA.xml()
         >>> commB = Comment()
-        >>> commB.from_xml(xml, verbose=True)
+        >>> commB.from_xml(xml)
         >>> commB.explicit_attrs
         ['author', 'date', 'content_type', 'body', 'alt_id']
         >>> commB.xml() == xml
@@ -339,6 +353,10 @@ class Comment (Tree, settings_object.SavedSettingsObject):
         >>> commB.alt_id = None
         >>> commB.xml() == xml
         True
+        >>> commC = Comment()
+        >>> commC.from_xml(xml, preserve_uuids=True)
+        >>> commC.uuid == commA.uuid
+        True
         """
         if type(xml_string) == types.UnicodeType:
             xml_string = xml_string.strip().encode('unicode_escape')
@@ -363,8 +381,10 @@ class Comment (Tree, settings_object.SavedSettingsObject):
                     text = settings_object.EMPTY
                 else:
                     text = xml.sax.saxutils.unescape(child.text)
-                    text = text.decode('unicode_escape').strip()
-                if child.tag == 'uuid':
+                    if not isinstance(text, unicode):
+                        text = text.decode('unicode_escape')
+                    text = text.strip()
+                if child.tag == 'uuid' and not preserve_uuids:
                     uuid = text
                     continue # don't set the comment's uuid tag.
                 elif child.tag == 'body':
@@ -377,9 +397,10 @@ class Comment (Tree, settings_object.SavedSettingsObject):
                 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)
+            else:
+                libbe.LOG.warning(
+                    'ignoring unknown tag {0} in {1}'.format(
+                        child.tag, comment.tag))
         if uuid != self.uuid and self.alt_id == None:
             self.explicit_attrs.append('alt_id')
             self.alt_id = uuid
@@ -395,6 +416,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
         """
         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'
@@ -441,16 +463,17 @@ class Comment (Tree, settings_object.SavedSettingsObject):
           <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 accept_changes == True:
-                    setattr(self, attr, new)
-                elif change_exception == True:
-                    raise ValueError, \
-                        'Merge would change %s "%s"->"%s" for comment %s' \
-                        % (attr, old, new, self.uuid)
+        if hasattr(other, 'explicit_attrs'):
+            for attr in other.explicit_attrs:
+                old = getattr(self, attr)
+                new = getattr(other, attr)
+                if old != new:
+                    if accept_changes:
+                        setattr(self, attr, new)
+                    elif change_exception:
+                        raise ValueError(
+                            ('Merge would change {} "{}"->"{}" for comment {}'
+                             ).format(attr, old, new, self.uuid))
         if self.alt_id == self.uuid:
             self.alt_id = None
         for estr in other.extra_strings:
@@ -486,7 +509,8 @@ class Comment (Tree, settings_object.SavedSettingsObject):
         if self.content_type.startswith("text/"):
             body = (self.body or "")
             if self.bug != None and self.bug.bugdir != None:
-                body = libbe.util.id.long_to_short_text([self.bug.bugdir], body)
+                body = libbe.util.id.long_to_short_text(
+                    {self.bug.bugdir.uuid: self.bug.bugdir}, body)
             lines.extend(body.splitlines())
         else:
             lines.append("Content type %s not printable.  Try XML output instead" % self.content_type)
@@ -506,14 +530,6 @@ class Comment (Tree, settings_object.SavedSettingsObject):
         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.author) # 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")
         >>> b = a.new_reply("Critique original comment")
@@ -590,9 +606,11 @@ class Comment (Tree, settings_object.SavedSettingsObject):
     # methods for saving/loading/acessing settings and properties.
 
     def load_settings(self, settings_mapfile=None):
+        if self.uuid == INVALID_UUID:
+            return
         if settings_mapfile == None:
-            settings_mapfile = \
-                self.storage.get(self.id.storage("values"), default="\n")
+            settings_mapfile = self.storage.get(
+                self.id.storage('values'), '{}\n')
         try:
             settings = mapfile.parse(settings_mapfile)
         except mapfile.InvalidMapfileContents, e:
@@ -601,6 +619,8 @@ class Comment (Tree, settings_object.SavedSettingsObject):
         self._setup_saved_settings(settings)
 
     def save_settings(self):
+        if self.uuid == INVALID_UUID:
+            return
         mf = mapfile.generate(self._get_saved_settings())
         self.storage.set(self.id.storage("values"), mf)
 
@@ -608,7 +628,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
         """
         Save any loaded contents to storage.
 
-        However, if self.storage.is_writeable() == True, then any
+        However, if ``self.storage.is_writeable() == True``, then any
         changes are automatically written to storage as soon as they
         happen, so calling this method will just waste time (unless
         something else has been messing with your stored files).
@@ -640,7 +660,7 @@ class Comment (Tree, settings_object.SavedSettingsObject):
             reply.in_reply_to = self.uuid
         self.append(reply)
 
-    def new_reply(self, body=None):
+    def new_reply(self, body=None, content_type=None):
         """
         >>> comm = Comment(bug=None, body="Some insightful remarks")
         >>> repA = comm.new_reply("Critique original comment")
@@ -648,13 +668,13 @@ class Comment (Tree, settings_object.SavedSettingsObject):
         >>> repB.in_reply_to == repA.uuid
         True
         """
-        reply = Comment(self.bug, body=body)
+        reply = Comment(self.bug, body=body, content_type=content_type)
         self.add_reply(reply)
         return reply
 
     def comment_from_uuid(self, uuid, match_alt_id=True):
-        """
-        Use a uuid to look up a comment.
+        """Use a uuid to look up a comment.
+
         >>> a = Comment(bug=None, uuid="a")
         >>> b = a.new_reply()
         >>> b.uuid = "b"
@@ -695,6 +715,7 @@ def cmp_attr(comment_1, comment_2, attr, invert=False):
     Compare a general attribute between two comments using the conventional
     comparison rule for that attribute type.  If invert == True, sort
     *against* that convention.
+
     >>> attr="author"
     >>> commentA = Comment()
     >>> commentB = Comment()