diff options
Diffstat (limited to 'libbe/comment.py')
-rw-r--r-- | libbe/comment.py | 250 |
1 files changed, 138 insertions, 112 deletions
diff --git a/libbe/comment.py b/libbe/comment.py index ca5f884..17dbab6 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -23,6 +23,7 @@ """ import base64 +import functools import time try: from email.mime.base import MIMEBase @@ -37,8 +38,7 @@ import xml.sax.saxutils import libbe import libbe.util.id from libbe.storage.util.properties import Property, doc_property, \ - local_property, defaulting_property, checked_property, cached_property, \ - primed_property, change_hook_property, settings_property + local_property, cached_property, change_hook_property import libbe.storage.util.settings_object as settings_object import libbe.storage.util.mapfile as mapfile from libbe.util.tree import Tree @@ -48,11 +48,6 @@ if libbe.TESTING: import doctest -# https://stackoverflow.com/a/56719588/164233 -def cmp(a, b): - return (int(a) > int(b)) - (int(a) < int(b)) - - class MissingReference(ValueError): def __init__(self, comment): msg = "Missing reference to %s" % (comment.in_reply_to) @@ -60,8 +55,12 @@ class MissingReference(ValueError): self.reference = comment.in_reply_to self.comment = comment + INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" +DEFAULT_IDX_FULL_IDX_LIST = None # To be redefined later + + def load_comments(bug, load_full=False): """ Set load_full=True when you want to load the comment completely @@ -77,12 +76,13 @@ def load_comments(bug, load_full=False): comm = Comment(bug, uuid, from_storage=True) if load_full: comm.load_settings() - dummy = comm.body # force the body to load + _ = comm.body # force the body to load comments.append(comm) bug.comment_root = Comment(bug, uuid=INVALID_UUID) bug.add_comments(comments, ignore_missing_references=True) return bug.comment_root + def save_comments(bug): for comment in bug.comment_root.traverse(): comment.bug = bug @@ -90,16 +90,17 @@ def save_comments(bug): comment.save() +@functools.total_ordering class Comment (Tree, settings_object.SavedSettingsObject): - """Comments are a notes that attach to :py:class:`~libbe.bug.Bug`\s in + """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 + >>> c.uuid is not None True >>> c.uuid = "some-UUID" - >>> print c.content_type + >>> print(c.content_type) text/plain """ @@ -107,17 +108,22 @@ class Comment (Tree, settings_object.SavedSettingsObject): required_saved_properties = [] _prop_save_settings = settings_object.prop_save_settings _prop_load_settings = settings_object.prop_load_settings + def _versioned_property(settings_properties=settings_properties, required_saved_properties=required_saved_properties, **kwargs): if "settings_properties" not in kwargs: kwargs["settings_properties"] = settings_properties if "required_saved_properties" not in kwargs: - kwargs["required_saved_properties"]=required_saved_properties + kwargs["required_saved_properties"] = required_saved_properties return settings_object.versioned_property(**kwargs) @_versioned_property(name="Alt-id", - 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.") + 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="Author", @@ -142,6 +148,7 @@ class Comment (Tree, settings_object.SavedSettingsObject): if self.date is None: return None return utility.str_to_time(self.date) + def _set_time(self, value): self.date = utility.time_to_str(value) time = property(fget=_get_time, @@ -149,19 +156,21 @@ class Comment (Tree, settings_object.SavedSettingsObject): doc="An integer version of .date") def _get_comment_body(self): - if self.storage != None and self.storage.is_readable() \ + if self.storage is not None and self.storage.is_readable() \ and self.uuid != INVALID_UUID: - return self.storage.get(self.id.storage("body"), + return self.storage.get( + self.id.storage("body"), 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.content_type.startswith('text/') \ - and self.bug != None and self.bug.bugdir != None: + and self.bug is not None and self.bug.bugdir is not 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" + if (self.storage is not None and self.storage.writeable) \ + or force: + assert new is not None, "Can't save empty comment" self.storage.set(self.id.storage("body"), new) @Property @@ -172,11 +181,13 @@ class Comment (Tree, settings_object.SavedSettingsObject): def body(): return {} def _extra_strings_check_fn(value): - return utility.iterable_full_of_strings(value, \ - alternative=settings_object.EMPTY) + return utility.iterable_full_of_strings( + value, alternative=settings_object.EMPTY) + def _extra_strings_change_hook(self, old, new): - self.extra_strings.sort() # to make merging easier + self.extra_strings.sort() # to make merging easier self._prop_save_settings(old, new) + @_versioned_property(name="extra_strings", doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.", default=[], @@ -208,22 +219,35 @@ class Comment (Tree, settings_object.SavedSettingsObject): self.storage = None self.uuid = uuid self.id = libbe.util.id.ID(self, 'comment') - if from_storage == False: + if not from_storage: if uuid is None: self.uuid = libbe.util.id.uuid_gen() - self.time = int(time.time()) # only save to second precision + self.time = int(time.time()) # only save to second precision self.in_reply_to = in_reply_to - if content_type != None: + if content_type is not None: self.content_type = content_type self.body = body - if self.bug != None: + if self.bug is not None: self.storage = self.bug.storage - if from_storage == False: - if self.storage != None and self.storage.is_writeable(): + if not from_storage: + if self.storage is not None and self.storage.is_writeable(): self.save() - def __cmp__(self, other): - return cmp_full(self, other) + def __eq__(self, other, idx_list=None): + if idx_list is None: + idx_list = DEFAULT_IDX_FULL_IDX_LIST + for idx in idx_list: + if idx(self) != idx(other): + return False + return True + + def __lt__(self, other, idx_list=None): + if idx_list is None: + idx_list = DEFAULT_IDX_FULL_IDX_LIST + for idx in idx_list: + if idx(self) != idx(other): + return idx(self) < idx(other) + return False def __str__(self): """ @@ -231,7 +255,7 @@ class Comment (Tree, settings_object.SavedSettingsObject): >>> comm.uuid = "com-1" >>> comm.date = "Thu, 20 Nov 2008 15:55:11 +0000" >>> comm.author = "Jane Doe <jdoe@example.com>" - >>> print comm + >>> print(comm) --------- Comment --------- Name: //com From: Jane Doe <jdoe@example.com> @@ -281,7 +305,7 @@ class Comment (Tree, settings_object.SavedSettingsObject): >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> comm.uuid = "0123" >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000" - >>> print comm.xml(indent=2) + >>> print(comm.xml(indent=2)) <comment> <uuid>0123</uuid> <short-name>//012</short-name> @@ -293,7 +317,7 @@ class Comment (Tree, settings_object.SavedSettingsObject): remarks</body> </comment> >>> comm.content_type = 'image/png' - >>> print comm.xml() + >>> print(comm.xml()) <comment> <uuid>0123</uuid> <short-name>//012</short-name> @@ -307,11 +331,11 @@ class Comment (Tree, settings_object.SavedSettingsObject): if self.content_type.startswith('text/'): body = (self.body or '').rstrip('\n') else: - maintype,subtype = self.content_type.split('/',1) + maintype, subtype = self.content_type.split('/', 1) msg = MIMEBase(maintype, subtype) msg.set_payload(self.body or '') encode_base64(msg) - body = base64.encodestring(self.body or '') + body = base64.encodebytes(self.body.encode() or b'') info = [('uuid', self.uuid), ('alt-id', self.alt_id), ('short-name', self.id.user()), @@ -321,9 +345,10 @@ class Comment (Tree, settings_object.SavedSettingsObject): ('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)) + for (k, v) in info: + if v is not None: + lines.append(' <%s>%s</%s>' + % (k, xml.sax.saxutils.escape(v), k)) for estr in self.extra_strings: lines.append(' <extra-string>%s</extra-string>' % estr) lines.append('</comment>') @@ -358,24 +383,24 @@ class Comment (Tree, settings_object.SavedSettingsObject): """ if type(xml_string) == str: xml_string = xml_string.strip().encode('unicode_escape') - if hasattr(xml_string, 'getchildren'): # already an ElementTree Element + if isinstance(xml_string, ElementTree.Element): comment = xml_string else: comment = ElementTree.XML(xml_string) if comment.tag != 'comment': - raise utility.InvalidXML( \ + raise utility.InvalidXML( 'comment', comment, 'root element must be <comment>') - tags=['uuid','alt-id','in-reply-to','author','date','content-type', - 'body','extra-string'] + tags = ['uuid', 'alt-id', 'in-reply-to', 'author', 'date', + 'content-type', 'body', 'extra-string'] self.explicit_attrs = [] uuid = None body = None estrs = [] - for child in comment.getchildren(): + for child in comment: if child.tag == 'short-name': pass elif child.tag in tags: - if child.text == None or len(child.text) == 0: + if child.text is None or len(child.text) == 0: text = settings_object.EMPTY else: text = xml.sax.saxutils.unescape(child.text) @@ -384,27 +409,27 @@ class Comment (Tree, settings_object.SavedSettingsObject): text = text.strip() if child.tag == 'uuid' and not preserve_uuids: uuid = text - continue # don't set the comment's uuid tag. + continue # don't set the comment's uuid tag. elif child.tag == 'body': body = text self.explicit_attrs.append(child.tag) - continue # don't set the comment's body yet. + continue # don't set the comment's body yet. elif child.tag == 'extra-string': estrs.append(text) - continue # don't set the comment's extra_string yet. - attr_name = child.tag.replace('-','_') + continue # don't set the comment's extra_string yet. + attr_name = child.tag.replace('-', '_') self.explicit_attrs.append(attr_name) setattr(self, attr_name, text) else: libbe.LOG.warning( 'ignoring unknown tag {0} in {1}'.format( child.tag, comment.tag)) - if uuid != self.uuid and self.alt_id == None: + if uuid != self.uuid and self.alt_id is None: self.explicit_attrs.append('alt_id') self.alt_id = uuid - if body != None: + if body is not None: if self.content_type.startswith('text/'): - self.body = body+'\n' # restore trailing newline + self.body = body+'\n' # restore trailing newline else: self.body = base64.decodestring(body) self.extra_strings = estrs @@ -440,15 +465,18 @@ class Comment (Tree, settings_object.SavedSettingsObject): Traceback (most recent call last): ... ValueError: Merge would add extra string "TAG: useful" to comment 0123 - >>> print commA.author + >>> print(commA.author) John - >>> print commA.extra_strings + >>> print(commA.extra_strings) ['TAG: favorite', 'TAG: very helpful'] >>> commA.merge(commB, accept_changes=True, ... accept_extra_strings=True, change_exception=True) - >>> print commA.extra_strings + >>> print(commA.extra_strings) ['TAG: favorite', 'TAG: useful', 'TAG: very helpful'] - >>> print commA.xml() + >>> print(type(commA)) + >>> print(dir(commA)) + >>> print(commA.xml()) + >>> print(commA.xml()) <comment> <uuid>0123</uuid> <short-name>//012</short-name> @@ -475,11 +503,12 @@ class Comment (Tree, settings_object.SavedSettingsObject): if self.alt_id == self.uuid: self.alt_id = None for estr in other.extra_strings: - if not estr in self.extra_strings: + if estr not in self.extra_strings: if accept_extra_strings: self.extra_strings.append(estr) elif change_exception: - raise ValueError('Merge would add extra string "%s" to comment %s' \ + raise ValueError( + 'Merge would add extra string "%s" to comment %s' % (estr, self.uuid)) def string(self, indent=0): @@ -487,7 +516,7 @@ class Comment (Tree, settings_object.SavedSettingsObject): >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") >>> comm.uuid = 'abcdef' >>> comm.date = "Thu, 01 Jan 1970 00:00:00 +0000" - >>> print comm.string(indent=2) + >>> print(comm.string(indent=2)) --------- Comment --------- Name: //abc From: @@ -505,12 +534,13 @@ class Comment (Tree, settings_object.SavedSettingsObject): lines.append("") if self.content_type.startswith("text/"): body = (self.body or "") - if self.bug != None and self.bug.bugdir != None: + if self.bug is not None and self.bug.bugdir is not None: 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) + lines.append(("Content type %s not printable. " + + "Try XML output instead") % self.content_type) istring = ' '*indent sep = '\n' + istring @@ -539,7 +569,7 @@ class Comment (Tree, settings_object.SavedSettingsObject): >>> 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(flatten=True) + >>> print(a.string_thread(flatten=True)) --------- Comment --------- Name: //a From: @@ -564,7 +594,7 @@ class Comment (Tree, settings_object.SavedSettingsObject): Date: Thu, 20 Nov 2008 04:00:00 +0000 <BLANKLINE> Useful examples - >>> print a.string_thread() + >>> print(a.string_thread()) --------- Comment --------- Name: //a From: @@ -591,7 +621,7 @@ class Comment (Tree, settings_object.SavedSettingsObject): Useful examples """ stringlist = [] - for depth,comment in self.thread(flatten=flatten): + for depth, comment in self.thread(flatten=flatten): ind = 2*depth+indent string_fn = getattr(comment, string_method_name) stringlist.append(string_fn(indent=ind)) @@ -610,7 +640,7 @@ class Comment (Tree, settings_object.SavedSettingsObject): self.id.storage('values'), '{}\n') try: settings = mapfile.parse(settings_mapfile) - except mapfile.InvalidMapfileContents as e: + except mapfile.InvalidMapfileContents: raise Exception('Invalid settings file for comment %s\n' '(BE version missmatch?)' % self.id.user()) self._setup_saved_settings(settings) @@ -632,9 +662,9 @@ class Comment (Tree, settings_object.SavedSettingsObject): """ if self.uuid == INVALID_UUID: return - assert self.storage != None, "Can't save without storage" - assert self.body != None, "Can't save blank comment" - if self.bug != None: + assert self.storage is not None, "Can't save without storage" + assert self.body is not None, "Can't save blank comment" + if self.bug is not None: parent = self.bug.id.storage() else: parent = None @@ -694,7 +724,7 @@ class Comment (Tree, settings_object.SavedSettingsObject): for comment in self.traverse(): if comment.uuid == uuid: return comment - if match_alt_id == True and uuid != None \ + if match_alt_id and uuid is not None \ and comment.alt_id == uuid: return comment raise KeyError(uuid) @@ -702,68 +732,64 @@ class Comment (Tree, settings_object.SavedSettingsObject): # methods for id generation def sibling_uuids(self): - if self.bug != None: + if self.bug is not None: return self.bug.uuids() return [] -def cmp_attr(comment_1, comment_2, attr, invert=False): +def idx_attr(comment, attr): """ - Compare a general attribute between two comments using the conventional - comparison rule for that attribute type. If invert == True, sort - *against* that convention. + Generate an index for sorting of comments based on the given + attribute. >>> 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 + >>> idx_attr(commentA, attr) + 'John Doe' + >>> commentB = Comment() >>> commentB.author = "John Doe" - >>> cmp_attr(commentA, commentB, attr) == 0 + >>> idx_attr(commentA, attr) == idx_attr(commentB, attr) True """ - if not hasattr(comment_2, attr) : - return 1 - val_1 = getattr(comment_1, attr) - val_2 = getattr(comment_2, attr) - if val_1 is None: val_1 = None - if val_2 is None: val_2 = None - - if invert == True : - return -cmp(val_1, val_2) - else : - return cmp(val_1, val_2) + if not hasattr(comment, attr): + return None + return getattr(comment, attr) + # 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") -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) +def idx_uuid(comment): + return idx_attr(comment, "uuid") + + +def idx_author(comment): + return idx_attr(comment, "author") + + +def idx_in_reply_to(comment): + return idx_attr(comment, "in_reply_to") -DEFAULT_CMP_FULL_CMP_LIST = \ - (cmp_time, cmp_author, cmp_content_type, cmp_body, cmp_in_reply_to, - cmp_uuid, cmp_extra_strings) +def idx_content_type(comment): + return idx_attr(comment, "content_type") + + +def idx_body(comment): + return idx_attr(comment, "body") + + +def idx_extra_strings(comment): + return idx_attr(comment, "extra_strings") + + +# chronological rankings (newer < older) +def idx_time(comment): + return idx_attr(comment, "time") -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() +DEFAULT_IDX_FULL_IDX_LIST = \ + (idx_uuid, idx_author, idx_in_reply_to, idx_content_type, idx_body, + idx_extra_strings, idx_time) if libbe.TESTING: suite = doctest.DocTestSuite() |