Oops, these new submods are used by the new, classified Bug & BugDir.
authorW. Trevor King <wking@drexel.edu>
Sat, 22 Nov 2008 21:14:04 +0000 (16:14 -0500)
committerW. Trevor King <wking@drexel.edu>
Sat, 22 Nov 2008 21:14:04 +0000 (16:14 -0500)
I'd forgotten tell bzr...

libbe/beuuid.py [new file with mode: 0644]
libbe/comment.py [new file with mode: 0644]
libbe/tree.py [new file with mode: 0644]

diff --git a/libbe/beuuid.py b/libbe/beuuid.py
new file mode 100644 (file)
index 0000000..e2435ea
--- /dev/null
@@ -0,0 +1,62 @@
+# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
+# <abentley@panoramicfeedback.com>
+#
+#    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 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, write to the Free Software
+#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+"""
+Backwards compatibility support for Python 2.4.  Once people give up
+on 2.4 ;), the uuid call should be merged into bugdir.py
+"""
+
+import unittest
+
+try:
+    from uuid import uuid4 # Python >= 2.5
+    def uuid_gen():
+        id = uuid4()
+        idstr = id.urn
+        start = "urn:uuid:"
+        assert idstr.startswith(start)
+        return idstr[len(start):]
+except ImportError:
+    import os
+    import sys
+    from subprocess import Popen, PIPE
+
+    def uuid_gen():
+        # Shell-out to system uuidgen
+        args = ['uuidgen', 'r']
+        try:
+            if sys.platform != "win32":
+                q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+            else:
+                # win32 don't have os.execvp() so have to run command in a shell
+                q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, 
+                          shell=True, cwd=cwd)
+        except OSError, e :
+            strerror = "%s\nwhile executing %s" % (e.args[1], args)
+            raise OSError, strerror
+        output, error = q.communicate()
+        status = q.wait()
+        if status != 0:
+            strerror = "%s\nwhile executing %s" % (status, args)
+            raise Exception, strerror
+        return output.rstrip('\n')
+
+class UUIDtestCase(unittest.TestCase):
+    def testUUID_gen(self):
+        id = uuid_gen()
+        self.failUnless(len(id) == 36, "invalid UUID '%s'" % id)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase)
diff --git a/libbe/comment.py b/libbe/comment.py
new file mode 100644 (file)
index 0000000..95dade6
--- /dev/null
@@ -0,0 +1,382 @@
+# Bugs Everywhere, a distributed bugtracker
+# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
+# <abentley@panoramicfeedback.com>
+#
+#    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 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, write to the Free Software
+#    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+#    MA 02110-1301, USA
+import os
+import os.path
+import time
+import textwrap
+import doctest
+
+from beuuid import uuid_gen
+import mapfile
+from tree import Tree
+import utility
+
+INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!"
+
+def _list_to_root(comments, bug):
+    """
+    Convert a raw list of comments to single (dummy) root comment.  We
+    use a dummy root comment, because there can be several comment
+    threads rooted on the same parent bug.  To simplify comment
+    interaction, we condense these threads into a single thread with a
+    Comment dummy root.
+    
+    No Comment method should use the dummy comment.
+    """
+    root_comments = []
+    uuid_map = {}
+    for comment in comments:
+        assert comment.uuid != None
+        uuid_map[comment.uuid] = comment
+    for comm in comments:
+        if comm.in_reply_to == None:
+            root_comments.append(comm)
+        else:
+            parentUUID = comm.in_reply_to
+            parent = uuid_map[parentUUID]
+            parent.add_reply(comm)
+    dummy_root = Comment(bug, uuid=INVALID_UUID)
+    dummy_root.extend(root_comments)
+    return dummy_root
+
+def loadComments(bug):
+    path = bug.get_path("comments")
+    if not os.path.isdir(path):
+        return Comment(bug, uuid=INVALID_UUID)
+    comments = []
+    for uuid in os.listdir(path):
+        if uuid.startswith('.'):
+            continue
+        comm = Comment(bug, uuid, loadNow=True)
+        comments.append(comm)
+    return _list_to_root(comments, bug)
+
+def saveComments(bug):
+    path = bug.get_path("comments")
+    bug.rcs.mkdir(path)
+    for comment in bug.comment_root.traverse():
+        comment.save()
+
+class Comment(Tree):
+    def __init__(self, bug=None, uuid=None, loadNow=False,
+                 in_reply_to=None, body=None):
+        """
+        Set loadNow=True to load an old bug.
+        Set loadNow=False to create a new bug.
+
+        The uuid option is required when loadNow==True.
+        
+        The in_reply_to and body options are only used if
+        loadNow==False (the default).  When loadNow==True, they are
+        loaded from the bug database.
+        
+        in_reply_to should be the uuid string of the parent comment.
+        """
+        Tree.__init__(self)
+        self.bug = bug
+        if bug != None:
+            self.rcs = bug.rcs
+        else:
+            self.rcs = None
+        if loadNow == True: 
+            self.uuid = uuid 
+            self.load()
+        else:
+            if uuid != None:
+                self.uuid = uuid
+            else:
+                self.uuid = uuid_gen()
+            self.time = time.time()
+            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):
+        """Avoid working with the possible dummy root comment"""
+        for comment in Tree.traverse(self, *args, **kwargs):
+            if comment.uuid == INVALID_UUID:
+                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, 20 Nov 2008 15:55:11 +0000")
+        >>> print comm.string(indent=2, shortname="com-1")
+          --------- Comment ---------
+          Name: com-1
+          From: 
+          Date: Thu, 20 Nov 2008 15:55:11 +0000
+        <BLANKLINE>
+          Some insightful remarks
+        """
+        if shortname == None:
+            shortname = self.uuid
+        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("")
+        lines.append(textwrap.fill(self._clean_string(self.body),
+                                   width=(79-indent)))
+        
+        istring = ' '*indent
+        sep = '\n' + istring
+        return istring + sep.join(lines).rstrip('\n')
+
+    def __str__(self):
+        """
+        >>> 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.From = "Jane Doe <jdoe@example.com>"
+        >>> print comm
+        --------- Comment ---------
+        Name: com-1
+        From: Jane Doe <jdoe@example.com>
+        Date: Thu, 20 Nov 2008 15:55:11 +0000
+        <BLANKLINE>
+        Some insightful remarks
+        """
+        return self.string()
+
+    def get_path(self, name=None):
+        my_dir = os.path.join(self.bug.get_path("comments"), self.uuid)
+        if name is None:
+            return my_dir
+        assert name in ["values", "body"]
+        return os.path.join(my_dir, name)
+
+    def load(self):
+        map = mapfile.map_load(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 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"))
+        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
+
+    def remove(self):
+        for comment in self.traverse():
+            path = comment.get_path()
+            self.rcs.recursive_remove(path)
+
+    def add_reply(self, reply):
+        if reply.time != None and self.time != None:
+            assert reply.time > self.time
+        if self.uuid != INVALID_UUID:
+            reply.in_reply_to = self.uuid
+        self.append(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")
+        """
+        reply = Comment(self.bug, body=body)
+        self.add_reply(reply)
+        return reply
+
+    def string_thread(self, name_map={}, indent=0,
+                      auto_name_map=False, bug_shortname=None):
+        """
+        Return a sting displaying a thread of comments.
+        bug_shortname is only used if auto_name_map == True.
+
+        >>> 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")
+        >>> b.uuid = "b"
+        >>> b.time = utility.str_to_time("Thu, 20 Nov 2008 02:00:00 +0000")
+        >>> c = b.new_reply("Begin flamewar :p")
+        >>> c.uuid = "c"
+        >>> c.time = utility.str_to_time("Thu, 20 Nov 2008 03:00:00 +0000")
+        >>> d = a.new_reply("Useful examples")
+        >>> d.uuid = "d"
+        >>> d.time = utility.str_to_time("Thu, 20 Nov 2008 04:00:00 +0000")
+        >>> a.sort(key=lambda comm : comm.time)
+        >>> print a.string_thread()
+        --------- Comment ---------
+        Name: a
+        From: 
+        Date: Thu, 20 Nov 2008 01:00:00 +0000
+        <BLANKLINE>
+        Insightful remarks
+          --------- Comment ---------
+          Name: b
+          From: 
+          Date: Thu, 20 Nov 2008 02:00:00 +0000
+        <BLANKLINE>
+          Critique original comment
+          --------- Comment ---------
+          Name: c
+          From: 
+          Date: Thu, 20 Nov 2008 03:00:00 +0000
+        <BLANKLINE>
+          Begin flamewar :p
+        --------- Comment ---------
+        Name: d
+        From: 
+        Date: Thu, 20 Nov 2008 04:00:00 +0000
+        <BLANKLINE>
+        Useful examples
+        >>> print a.string_thread(auto_name_map=True, bug_shortname="bug-1")
+        --------- Comment ---------
+        Name: bug-1:1
+        From: 
+        Date: Thu, 20 Nov 2008 01:00:00 +0000
+        <BLANKLINE>
+        Insightful remarks
+          --------- Comment ---------
+          Name: bug-1:2
+          From: 
+          Date: Thu, 20 Nov 2008 02:00:00 +0000
+        <BLANKLINE>
+          Critique original comment
+          --------- Comment ---------
+          Name: bug-1:3
+          From: 
+          Date: Thu, 20 Nov 2008 03:00:00 +0000
+        <BLANKLINE>
+          Begin flamewar :p
+        --------- Comment ---------
+        Name: bug-1:4
+        From: 
+        Date: Thu, 20 Nov 2008 04:00:00 +0000
+        <BLANKLINE>
+        Useful examples
+        """
+        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.string(indent=ind, shortname=sname))
+        return '\n'.join(stringlist)
+
+    def comment_shortnames(self, bug_shortname=""):
+        """
+        Iterate through (id, comment) pairs, in time order.
+        (This is a user-friendly id, not the comment uuid).
+
+        SIDE-EFFECT : will sort the comment tree by comment.time
+
+        >>> a = Comment(bug=None, uuid="a")
+        >>> b = a.new_reply()
+        >>> b.uuid = "b"
+        >>> c = b.new_reply()
+        >>> c.uuid = "c"
+        >>> d = a.new_reply()
+        >>> d.uuid = "d"
+        >>> for id,name in a.comment_shortnames("bug-1"):
+        ...     print id, name.uuid
+        bug-1:1 a
+        bug-1:2 b
+        bug-1:3 c
+        bug-1:4 d
+        """
+        self.sort(key=lambda comm : comm.time)
+        for num,comment in enumerate(self.traverse()):
+            yield ("%s:%d" % (bug_shortname, num+1), comment)
+
+    def comment_from_shortname(self, comment_shortname, *args, **kwargs):
+        """
+        Use a comment shortname to look up a comment.
+        >>> a = Comment(bug=None, uuid="a")
+        >>> b = a.new_reply()
+        >>> b.uuid = "b"
+        >>> c = b.new_reply()
+        >>> c.uuid = "c"
+        >>> d = a.new_reply()
+        >>> d.uuid = "d"
+        >>> comm = a.comment_from_shortname("bug-1:3", bug_shortname="bug-1")
+        >>> id(comm) == id(c)
+        True
+        """
+        for cur_name, comment in self.comment_shortnames(*args, **kwargs):
+            if comment_shortname == cur_name:
+                return comment
+        raise KeyError(comment_shortname)
+
+    def comment_from_uuid(self, uuid):
+        """
+        Use a comment shortname to look up a comment.
+        >>> a = Comment(bug=None, uuid="a")
+        >>> b = a.new_reply()
+        >>> b.uuid = "b"
+        >>> c = b.new_reply()
+        >>> c.uuid = "c"
+        >>> d = a.new_reply()
+        >>> d.uuid = "d"
+        >>> comm = a.comment_from_uuid("d")
+        >>> id(comm) == id(d)
+        True
+        """
+        for comment in self.traverse():
+            if comment.uuid == uuid:
+                return comment
+        raise KeyError(uuid)
+
+suite = doctest.DocTestSuite()
diff --git a/libbe/tree.py b/libbe/tree.py
new file mode 100644 (file)
index 0000000..e6f144e
--- /dev/null
@@ -0,0 +1,158 @@
+# Bugs Everywhere, a distributed bugtracker
+# Copyright (C) 2008 W. Trevor King
+#
+#  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 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, write to the Free Software
+#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+#  02110-1301, USA
+
+import doctest
+
+class Tree(list):
+    """
+    Construct
+               +-b---d-g
+             a-+   +-e
+               +-c-+-f-h-i
+    with
+    >>> i = Tree();       i.n = "i"
+    >>> h = Tree([i]);    h.n = "h"
+    >>> f = Tree([h]);    f.n = "f"
+    >>> e = Tree();       e.n = "e"
+    >>> c = Tree([f,e]);  c.n = "c"
+    >>> g = Tree();       g.n = "g"
+    >>> d = Tree([g]);    d.n = "d"
+    >>> b = Tree([d]);    b.n = "b"
+    >>> a = Tree();       a.n = "a"
+    >>> a.append(c)
+    >>> a.append(b)
+    
+    >>> a.branch_len()
+    5
+    >>> a.sort(key=lambda node : node.branch_len())
+    >>> "".join([node.n for node in a.traverse()])
+    'abdgcefhi'
+    >>> "".join([node.n for node in a.traverse(depthFirst=False)])
+    'abcdefghi'
+    >>> for depth,node in a.thread():
+    ...     print "%*s" % (2*depth+1, node.n)
+    a
+      b
+        d
+          g
+      c
+        e
+        f
+          h
+            i
+    >>> for depth,node in a.thread(flatten=True):
+    ...     print "%*s" % (2*depth+1, node.n)
+    a
+      b
+      d
+      g
+    c
+      e
+    f
+    h
+    i
+    """
+    def branch_len(self):
+        """
+        Exhaustive search every time == SLOW.
+
+        Use only on small trees, or reimplement by overriding
+        child-addition methods to allow accurate caching.
+
+        For the tree
+               +-b---d-g
+             a-+   +-e
+               +-c-+-f-h-i
+        this method returns 5.
+        """
+        if len(self) == 0:
+            return 1
+        else:
+            return 1 + max([child.branch_len() for child in self])
+
+    def sort(self, *args, **kwargs):
+        """
+        This method can be slow, e.g. on a branch_len() sort, since a
+        node at depth N from the root has it's branch_len() method
+        called N times.
+        """
+        list.sort(self, *args, **kwargs)
+        for child in self:
+            child.sort()
+
+    def traverse(self, depthFirst=True):
+        """
+        Note: you might want to sort() your tree first.
+        """
+        if depthFirst == True:
+            yield self
+            for child in self:
+                for descendant in child.traverse():
+                    yield descendant
+        else: # breadth first, Wikipedia algorithm
+            # http://en.wikipedia.org/wiki/Breadth-first_search
+            queue = [self]
+            while len(queue) > 0:
+                node = queue.pop(0)
+                yield node
+                queue.extend(node)
+
+    def thread(self, flatten=False):
+        """
+        When flatten==False, the depth of any node is one greater than
+        the depth of its parent.  That way the inheritance is
+        explicit, but you can end up with highly indented threads.
+        
+        When flatten==True, the depth of any node is only greater than
+        the depth of its parent when there is a branch, and the node
+        is not the last child.  This can lead to ancestry ambiguity,
+        but keeps the total indentation down.  E.g.
+                      +-b                  +-b-c
+                    a-+-c        and     a-+
+                      +-d-e-f              +-d-e-f
+        would both produce (after sorting by branch_len())
+        (0, a)
+        (1, b)
+        (1, c)
+        (0, d)
+        (0, e)
+        (0, f)
+        """
+        stack = [] # ancestry of the current node
+        if flatten == True:
+            depthDict = {}
+        
+        for node in self.traverse(depthFirst=True):
+            while len(stack) > 0 \
+                    and id(node) not in [id(c) for c in stack[-1]]:
+                stack.pop(-1)
+            if flatten == False:
+                depth = len(stack)
+            else:
+                if len(stack) == 0:
+                    depth = 0
+                else:
+                    parent = stack[-1]
+                    depth = depthDict[id(parent)]
+                    if len(parent) > 1 and node != parent[-1]:
+                        depth += 1
+                depthDict[id(node)] = depth
+            yield (depth,node)
+            stack.append(node)
+
+suite = doctest.DocTestSuite()