From 93801606303d79cfe9c1483cd6627cb1b93dedc7 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sat, 22 Nov 2008 16:14:04 -0500 Subject: Oops, these new submods are used by the new, classified Bug & BugDir. I'd forgotten tell bzr... --- libbe/comment.py | 382 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 libbe/comment.py (limited to 'libbe/comment.py') diff --git a/libbe/comment.py b/libbe/comment.py new file mode 100644 index 0000000..95dade6 --- /dev/null +++ b/libbe/comment.py @@ -0,0 +1,382 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc. +# +# +# 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 + + 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 " + >>> print comm + --------- Comment --------- + Name: com-1 + From: Jane Doe + Date: Thu, 20 Nov 2008 15:55:11 +0000 + + 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 + + Insightful remarks + --------- Comment --------- + Name: b + From: + Date: Thu, 20 Nov 2008 02:00:00 +0000 + + Critique original comment + --------- Comment --------- + Name: c + From: + Date: Thu, 20 Nov 2008 03:00:00 +0000 + + Begin flamewar :p + --------- Comment --------- + Name: d + From: + Date: Thu, 20 Nov 2008 04:00:00 +0000 + + 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 + + Insightful remarks + --------- Comment --------- + Name: bug-1:2 + From: + Date: Thu, 20 Nov 2008 02:00:00 +0000 + + Critique original comment + --------- Comment --------- + Name: bug-1:3 + From: + Date: Thu, 20 Nov 2008 03:00:00 +0000 + + Begin flamewar :p + --------- Comment --------- + Name: bug-1:4 + From: + Date: Thu, 20 Nov 2008 04:00:00 +0000 + + 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() -- cgit From 9e0a846ff4fdaac45665e5a1e085aa37e3fa135b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 23 Nov 2008 06:51:30 -0500 Subject: Added archive/project init code for `./test_usage.sh arch`. Also some minor cleanups. --- libbe/comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'libbe/comment.py') diff --git a/libbe/comment.py b/libbe/comment.py index 95dade6..0bfc12d 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -218,7 +218,7 @@ class Comment(Tree): def add_reply(self, reply): if reply.time != None and self.time != None: - assert reply.time > self.time + assert reply.time >= self.time if self.uuid != INVALID_UUID: reply.in_reply_to = self.uuid self.append(reply) -- cgit From 510c9f33393c1f222ee56732c026f229ed8ae49d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 23 Nov 2008 09:50:56 -0500 Subject: Go back to lazy bug loading to get execution speed back up. Fixes bug b3c6da51-3a30-42c9-8c75-587c7a1705c5 --- libbe/comment.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'libbe/comment.py') diff --git a/libbe/comment.py b/libbe/comment.py index 0bfc12d..9b87f18 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -63,7 +63,7 @@ def loadComments(bug): for uuid in os.listdir(path): if uuid.startswith('.'): continue - comm = Comment(bug, uuid, loadNow=True) + comm = Comment(bug, uuid, from_disk=True) comments.append(comm) return _list_to_root(comments, bug) @@ -74,16 +74,16 @@ def saveComments(bug): comment.save() class Comment(Tree): - def __init__(self, bug=None, uuid=None, loadNow=False, + def __init__(self, bug=None, uuid=None, from_disk=False, in_reply_to=None, body=None): """ - Set loadNow=True to load an old bug. - Set loadNow=False to create a new bug. + Set from_disk=True to load an old bug. + Set from_disk=False to create a new bug. - The uuid option is required when loadNow==True. + The uuid option is required when from_disk==True. The in_reply_to and body options are only used if - loadNow==False (the default). When loadNow==True, they are + from_disk==False (the default). When from_disk==True, they are loaded from the bug database. in_reply_to should be the uuid string of the parent comment. @@ -94,7 +94,7 @@ class Comment(Tree): self.rcs = bug.rcs else: self.rcs = None - if loadNow == True: + if from_disk == True: self.uuid = uuid self.load() else: -- cgit From 63a7726eba738fe2ed340027039ba655ff91898a Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 24 Nov 2008 07:09:03 -0500 Subject: Added 'allow_no_rcs' flag to RCS file system access methods. Now mapfile access has fewer special cases, and there is less redundant rcs.add/update code. --- libbe/comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'libbe/comment.py') diff --git a/libbe/comment.py b/libbe/comment.py index 9b87f18..710c773 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -181,7 +181,7 @@ class Comment(Tree): return os.path.join(my_dir, name) def load(self): - map = mapfile.map_load(self.get_path("values")) + 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") -- cgit From 116c356bdb9b8e6d32af2419dda1d423b238efe1 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 24 Nov 2008 08:11:08 -0500 Subject: Removed auto-wrapping from comment.Comment.string(). It makes tracebacks almost illegible. I doubt markup/markdown systax or auto-formatting is really useful, since bugs-reports are ususally a short comment and a traceback. I also closed a4d38ba7-ec28-4096-a4f3-eb8c9790ffb2 and 7bfc591e-584a-476e-8e11-b548f1afcaa6, which have probably been fixed for a long time... --- libbe/comment.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'libbe/comment.py') diff --git a/libbe/comment.py b/libbe/comment.py index 710c773..7a90cca 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -150,8 +150,10 @@ class Comment(Tree): 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))) + #lines.append(textwrap.fill(self._clean_string(self.body), + # width=(79-indent))) + lines.append(self._clean_string(self.body)) + # some comments shouldn't be wrapped... istring = ' '*indent sep = '\n' + istring -- cgit From 442e5b00c8b5bf65041d0a1dafbf8c7eac0d5356 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 24 Nov 2008 16:30:52 -0500 Subject: Fixed broken doctest and inconsitent indentation from not wrapping comments. Also emptied becommands/__init__.py. I didn't understand the plugin interface when I wrote it. --- libbe/comment.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'libbe/comment.py') diff --git a/libbe/comment.py b/libbe/comment.py index 7a90cca..c89fd9d 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -140,7 +140,9 @@ class Comment(Tree): From: Date: Thu, 20 Nov 2008 15:55:11 +0000 - Some insightful remarks + Some + insightful + remarks """ if shortname == None: shortname = self.uuid @@ -152,7 +154,7 @@ class Comment(Tree): lines.append("") #lines.append(textwrap.fill(self._clean_string(self.body), # width=(79-indent))) - lines.append(self._clean_string(self.body)) + lines.extend(self._clean_string(self.body).splitlines()) # some comments shouldn't be wrapped... istring = ' '*indent -- cgit