diff options
Diffstat (limited to 'libbe/comment.py')
-rw-r--r-- | libbe/comment.py | 306 |
1 files changed, 194 insertions, 112 deletions
diff --git a/libbe/comment.py b/libbe/comment.py index 3249e8b..41bc7e6 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -16,6 +16,11 @@ # 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. + +""" +Define the Comment class for representing bug comments. +""" + import base64 import os import os.path @@ -61,6 +66,11 @@ 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 list_to_root(comments, bug, root=None, @@ -115,8 +125,10 @@ def loadComments(bug, load_full=False): Set load_full=True when you want to load the comment completely from disk *now*, rather than waiting and lazy loading as required. """ + if bug.sync_with_disk == False: + raise DiskAccessRequired("load comments") path = bug.get_path("comments") - if not os.path.isdir(path): + if not os.path.exists(path): return Comment(bug, uuid=INVALID_UUID) comments = [] for uuid in os.listdir(path): @@ -131,6 +143,8 @@ def loadComments(bug, load_full=False): return list_to_root(comments, bug) def saveComments(bug): + if bug.sync_with_disk == False: + raise DiskAccessRequired("save comments") for comment in bug.comment_root.traverse(): comment.save() @@ -162,9 +176,9 @@ class Comment(Tree, settings_object.SavedSettingsObject): doc="Alternate ID for linking imported comments. Internally comments are linked (via In-reply-to) to the parent's UUID. However, these UUIDs are generated internally, so Alt-id is provided as a user-controlled linking target.") def alt_id(): return {} - @_versioned_property(name="From", + @_versioned_property(name="Author", doc="The author of the comment") - def From(): return {} + def author(): return {} @_versioned_property(name="In-reply-to", doc="UUID for parent comment or bug") @@ -178,28 +192,28 @@ class Comment(Tree, settings_object.SavedSettingsObject): @_versioned_property(name="Date", doc="An RFC 2822 timestamp for comment creation") - def time_string(): return {} + def date(): return {} def _get_time(self): - if self.time_string == None: + if self.date == None: return None - return utility.str_to_time(self.time_string) + return utility.str_to_time(self.date) def _set_time(self, value): - self.time_string = utility.time_to_str(value) + self.date = utility.time_to_str(value) time = property(fget=_get_time, fset=_set_time, - doc="An integer version of .time_string") + doc="An integer version of .date") def _get_comment_body(self): - if self.rcs != None and self.sync_with_disk == True: - import rcs + if self.vcs != None and self.sync_with_disk == True: + import vcs binary = not self.content_type.startswith("text/") - return self.rcs.get_file_contents(self.get_path("body"), binary=binary) + return self.vcs.get_file_contents(self.get_path("body"), binary=binary) def _set_comment_body(self, old=None, new=None, force=False): - if (self.rcs != None and self.sync_with_disk == True) or force==True: + if (self.vcs != None and self.sync_with_disk == True) or force==True: assert new != None, "Can't save empty comment" binary = not self.content_type.startswith("text/") - self.rcs.set_file_contents(self.get_path("body"), new, binary=binary) + self.vcs.set_file_contents(self.get_path("body"), new, binary=binary) @Property @change_hook_property(hook=_set_comment_body) @@ -208,15 +222,15 @@ class Comment(Tree, settings_object.SavedSettingsObject): @doc_property(doc="The meat of the comment") def body(): return {} - def _get_rcs(self): - if hasattr(self.bug, "rcs"): - return self.bug.rcs + def _get_vcs(self): + if hasattr(self.bug, "vcs"): + return self.bug.vcs @Property - @cached_property(generator=_get_rcs) - @local_property("rcs") + @cached_property(generator=_get_vcs) + @local_property("vcs") @doc_property(doc="A revision control system instance.") - def rcs(): return {} + def vcs(): return {} def _extra_strings_check_fn(value): return utility.iterable_full_of_strings(value, \ @@ -257,13 +271,29 @@ class Comment(Tree, settings_object.SavedSettingsObject): if uuid == None: self.uuid = uuid_gen() self.time = int(time.time()) # only save to second precision - if self.rcs != None: - self.From = self.rcs.get_user_id() + if self.vcs != None: + self.author = self.vcs.get_user_id() self.in_reply_to = in_reply_to self.body = body - def set_sync_with_disk(self, value): - self.sync_with_disk = True + def __cmp__(self, other): + return cmp_full(self, other) + + def __str__(self): + """ + >>> comm = Comment(bug=None, body="Some insightful remarks") + >>> comm.uuid = "com-1" + >>> comm.date = "Thu, 20 Nov 2008 15:55:11 +0000" + >>> comm.author = "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 traverse(self, *args, **kwargs): """Avoid working with the possible dummy root comment""" @@ -272,6 +302,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): continue yield comment + # serializing methods + def _setting_attr_string(self, setting): value = getattr(self, setting) if value == None: @@ -282,12 +314,12 @@ class Comment(Tree, settings_object.SavedSettingsObject): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> comm.uuid = "0123" - >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000" >>> print comm.xml(indent=2, shortname="com-1") <comment> <uuid>0123</uuid> <short-name>com-1</short-name> - <from></from> + <author></author> <date>Thu, 01 Jan 1970 00:00:00 +0000</date> <content-type>text/plain</content-type> <body>Some @@ -309,8 +341,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): ("alt-id", self.alt_id), ("short-name", shortname), ("in-reply-to", self.in_reply_to), - ("from", self._setting_attr_string("From")), - ("date", self.time_string), + ("author", self._setting_attr_string("author")), + ("date", self.date), ("content-type", self.content_type), ("body", body)] lines = ["<comment>"] @@ -328,11 +360,11 @@ class Comment(Tree, settings_object.SavedSettingsObject): <alt-id> fields. >>> commA = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> commA.uuid = "0123" - >>> commA.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> commA.date = "Thu, 01 Jan 1970 00:00:00 +0000" >>> xml = commA.xml(shortname="com-1") >>> commB = Comment() >>> commB.from_xml(xml) - >>> attrs=['uuid','alt_id','in_reply_to','From','time_string','content_type','body'] + >>> 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'" @@ -342,15 +374,15 @@ class Comment(Tree, settings_object.SavedSettingsObject): Mismatch on alt_id: '0123' should be 'None' >>> print commB.alt_id 0123 - >>> commA.From - >>> commB.From + >>> commA.author + >>> commB.author """ if type(xml_string) == types.UnicodeType: 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','from','date','content-type','body'] + tags=['uuid','alt-id','in-reply-to','author','date','content-type','body'] uuid = None body = None for child in comment.getchildren(): @@ -368,10 +400,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): if child.tag == "body": body = text continue # don't set the bug's body yet. - elif child.tag == 'from': - attr_name = "From" - elif child.tag == 'date': - attr_name = 'time_string' else: attr_name = child.tag.replace('-','_') setattr(self, attr_name, text) @@ -389,7 +417,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): def string(self, indent=0, shortname=None): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") - >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000" >>> print comm.string(indent=2, shortname="com-1") --------- Comment --------- Name: com-1 @@ -405,8 +433,8 @@ class Comment(Tree, settings_object.SavedSettingsObject): lines = [] lines.append("--------- Comment ---------") lines.append("Name: %s" % shortname) - lines.append("From: %s" % (self._setting_attr_string("From"))) - lines.append("Date: %s" % self.time_string) + lines.append("From: %s" % (self._setting_attr_string("author"))) + lines.append("Date: %s" % self.date) lines.append("") if self.content_type.startswith("text/"): lines.extend((self.body or "").splitlines()) @@ -417,78 +445,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): 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_string = "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_settings(self): - self.settings = mapfile.map_load(self.rcs, self.get_path("values")) - self._setup_saved_settings() - - def save_settings(self): - self.rcs.mkdir(self.get_path()) - path = self.get_path("values") - mapfile.map_save(self.rcs, path, self._get_saved_settings()) - - def save(self): - """ - Save any loaded contents to disk. - - However, if self.sync_with_disk = True, then any changes are - automatically written to disk as soon as they happen, so - calling this method will just waste time (unless something - else has been messing with your on-disk files). - """ - assert self.body != None, "Can't save blank comment" - self.save_settings() - self._set_comment_body(new=self.body, force=True) - - def remove(self): - for comment in self.traverse(): - path = comment.get_path() - self.rcs.recursive_remove(path) - - def add_reply(self, reply, allow_time_inversion=False): - if self.uuid != INVALID_UUID: - reply.in_reply_to = self.uuid - self.append(reply) - #raise Exception, "adding reply \n%s\n%s" % (self, 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") - >>> repB.in_reply_to == repA.uuid - True - """ - reply = Comment(self.bug, body=body) - if self.bug != None: - reply.set_sync_with_disk(self.bug.sync_with_disk) - if reply.sync_with_disk == True: - reply.save() - self.add_reply(reply) - return reply - def string_thread(self, string_method_name="string", name_map={}, indent=0, flatten=True, auto_name_map=False, bug_shortname=None): @@ -506,7 +462,7 @@ class Comment(Tree, settings_object.SavedSettingsObject): name_map = {} for shortname,comment in comm.comment_shortnames(bug_shortname): name_map[comment.uuid] = shortname - comm.sort(key=lambda c : c.From) # your sort + 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") @@ -593,6 +549,77 @@ class Comment(Tree, settings_object.SavedSettingsObject): indent=indent, auto_name_map=auto_name_map, bug_shortname=bug_shortname) + # methods for saving/loading/acessing settings and properties. + + def get_path(self, *args): + dir = os.path.join(self.bug.get_path("comments"), self.uuid) + if len(args) == 0: + return dir + assert args[0] in ["values", "body"], str(args) + return os.path.join(dir, *args) + + def set_sync_with_disk(self, value): + self.sync_with_disk = value + + def load_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("load settings") + self.settings = mapfile.map_load(self.vcs, self.get_path("values")) + self._setup_saved_settings() + + def save_settings(self): + if self.sync_with_disk == False: + raise DiskAccessRequired("save settings") + self.vcs.mkdir(self.get_path()) + path = self.get_path("values") + mapfile.map_save(self.vcs, path, self._get_saved_settings()) + + def save(self): + """ + Save any loaded contents to disk. + + However, if self.sync_with_disk = True, then any changes are + automatically written to disk as soon as they happen, so + calling this method will just waste time (unless something + else has been messing with your on-disk files). + """ + sync_with_disk = self.sync_with_disk + if sync_with_disk == False: + self.set_sync_with_disk(True) + assert self.body != None, "Can't save blank comment" + self.save_settings() + self._set_comment_body(new=self.body, force=True) + if sync_with_disk == False: + self.set_sync_with_disk(False) + + def remove(self): + if self.sync_with_disk == False and self.uuid != INVALID_UUID: + raise DiskAccessRequired("remove") + for comment in self.traverse(): + path = comment.get_path() + self.vcs.recursive_remove(path) + + def add_reply(self, reply, allow_time_inversion=False): + 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") + >>> repB.in_reply_to == repA.uuid + True + """ + reply = Comment(self.bug, body=body) + if self.bug != None: + reply.set_sync_with_disk(self.bug.sync_with_disk) + if reply.sync_with_disk == True: + reply.save() + self.add_reply(reply) + return reply + def comment_shortnames(self, bug_shortname=None): """ Iterate through (id, comment) pairs, in time order. @@ -659,4 +686,59 @@ class Comment(Tree, settings_object.SavedSettingsObject): return comment raise KeyError(uuid) +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() + >>> commentA.author = "John Doe" + >>> commentB.author = "Jane Doe" + >>> cmp_attr(commentA, commentB, attr) > 0 + True + >>> cmp_attr(commentA, commentB, attr, invert=True) < 0 + True + >>> commentB.author = "John Doe" + >>> cmp_attr(commentA, commentB, attr) == 0 + True + """ + if not hasattr(comment_2, attr) : + return 1 + val_1 = getattr(comment_1, attr) + val_2 = getattr(comment_2, attr) + if val_1 == None: val_1 = None + if val_2 == None: val_2 = None + + if invert == True : + return -cmp(val_1, val_2) + else : + return cmp(val_1, val_2) + +# alphabetical rankings (a < z) +cmp_uuid = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "uuid") +cmp_author = lambda comment_1, comment_2 : cmp_attr(comment_1, comment_2, "author") +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") +# 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) + +class CommentCompoundComparator (object): + def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST): + self.cmp_list = cmp_list + def __call__(self, comment_1, comment_2): + for comparison in self.cmp_list : + val = comparison(comment_1, comment_2) + if val != 0 : + return val + return 0 + +cmp_full = CommentCompoundComparator() + suite = doctest.DocTestSuite() |