+ Examples
+ --------
+
+ >>> subscriptions_from_string(None)
+ [<Subscription: DIR (all)>]
+ >>> subscriptions_from_string('DIR:new,DIR:rem,ABC:all,XYZ:all')
+ [<Subscription: DIR (new)>, <Subscription: DIR (rem)>, <Subscription: ABC (all)>, <Subscription: XYZ (all)>]
+ >>> subscriptions_from_string('DIR::new')
+ Traceback (most recent call last):
+ ...
+ ValueError: Invalid subscription "DIR::new", should be ID:TYPE
+ """
+ if string == None:
+ return [Subscription(BUGDIR_ID, BUGDIR_TYPE_ALL)]
+ subscriptions = []
+ for subscription in string.split(','):
+ fields = subscription.split(':')
+ if len(fields) != 2:
+ raise ValueError('Invalid subscription "%s", should be ID:TYPE'
+ % subscription)
+ id,type = fields
+ subscriptions.append(Subscription(id, type))
+ return subscriptions
+
+class DiffTree (libbe.util.tree.Tree):
+ """A tree holding difference data for easy report generation.
+
+ Examples
+ --------
+
+ >>> bugdir = DiffTree('bugdir')
+ >>> bdsettings = DiffTree('settings', data='target: None -> 1.0')
+ >>> bugdir.append(bdsettings)
+ >>> bugs = DiffTree('bugs', 'bug-count: 5 -> 6')
+ >>> bugdir.append(bugs)
+ >>> new = DiffTree('new', 'new bugs: ABC, DEF')
+ >>> bugs.append(new)
+ >>> rem = DiffTree('rem', 'removed bugs: RST, UVW')
+ >>> bugs.append(rem)
+ >>> print bugdir.report_string()
+ target: None -> 1.0
+ bug-count: 5 -> 6
+ new bugs: ABC, DEF
+ removed bugs: RST, UVW
+ >>> print '\\n'.join(bugdir.paths())
+ bugdir
+ bugdir/settings
+ bugdir/bugs
+ bugdir/bugs/new
+ bugdir/bugs/rem
+ >>> bugdir.child_by_path('/') == bugdir
+ True
+ >>> bugdir.child_by_path('/bugs') == bugs
+ True
+ >>> bugdir.child_by_path('/bugs/rem') == rem
+ True
+ >>> bugdir.child_by_path('bugdir') == bugdir
+ True
+ >>> bugdir.child_by_path('bugdir/') == bugdir
+ True
+ >>> bugdir.child_by_path('bugdir/bugs') == bugs
+ True
+ >>> bugdir.child_by_path('/bugs').masked = True
+ >>> print bugdir.report_string()
+ target: None -> 1.0
+ """
+ def __init__(self, name, data=None, data_part_fn=str,
+ requires_children=False, masked=False):
+ libbe.util.tree.Tree.__init__(self)
+ self.name = name
+ self.data = data
+ self.data_part_fn = data_part_fn
+ self.requires_children = requires_children
+ self.masked = masked
+ def paths(self, parent_path=None):
+ paths = []
+ if parent_path == None:
+ path = self.name
+ else:
+ path = '%s/%s' % (parent_path, self.name)
+ paths.append(path)
+ for child in self:
+ paths.extend(child.paths(path))
+ return paths
+ def child_by_path(self, path):
+ if hasattr(path, 'split'): # convert string path to a list of names
+ names = path.split('/')
+ if names[0] == '':
+ names[0] = self.name # replace root with self
+ if len(names) > 1 and names[-1] == '':
+ names = names[:-1] # strip empty tail
+ else: # it was already an array
+ names = path
+ assert len(names) > 0, path
+ if names[0] == self.name:
+ if len(names) == 1:
+ return self
+ for child in self:
+ if names[1] == child.name:
+ return child.child_by_path(names[1:])
+ if len(names) == 1:
+ raise KeyError, "%s doesn't match '%s'" % (names, self.name)
+ raise KeyError, '%s points to child not in %s' % (names, [c.name for c in self])
+ def report_string(self):
+ report = self.report()
+ if report == None:
+ return ''
+ return '\n'.join(report)
+ def report(self, root=None, parent=None, depth=0):
+ if root == None:
+ root = self.make_root()
+ if self.masked == True:
+ return root
+ data_part = self.data_part(depth)
+ if self.requires_children == True \
+ and len([c for c in self if c.masked == False]) == 0:
+ pass
+ else:
+ self.join(root, parent, data_part)
+ if data_part != None:
+ depth += 1
+ for child in self:
+ root = child.report(root, self, depth)
+ return root
+ def make_root(self):
+ return []
+ def join(self, root, parent, data_part):
+ if data_part != None:
+ root.append(data_part)
+ def data_part(self, depth, indent=True):
+ if self.data == None:
+ return None
+ if hasattr(self, '_cached_data_part'):
+ return self._cached_data_part
+ data_part = self.data_part_fn(self.data)
+ if indent == True:
+ data_part_lines = data_part.splitlines()
+ indent = ' '*(depth)
+ line_sep = '\n'+indent
+ data_part = indent+line_sep.join(data_part_lines)
+ self._cached_data_part = data_part
+ return data_part
+
+class Diff (object):
+ """Difference tree generator for BugDirs.
+
+ Examples
+ --------
+
+ >>> import copy
+ >>> bd = libbe.bugdir.SimpleBugDir(memory=True)
+ >>> bd_new = copy.deepcopy(bd)
+ >>> bd_new.target = '1.0'
+ >>> a = bd_new.bug_from_uuid('a')
+ >>> rep = a.comment_root.new_reply("I'm closing this bug")
+ >>> rep.uuid = 'acom'
+ >>> rep.author = 'John Doe <j@doe.com>'
+ >>> rep.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
+ >>> a.status = 'closed'
+ >>> b = bd_new.bug_from_uuid('b')
+ >>> bd_new.remove_bug(b)
+ >>> c = bd_new.new_bug('Bug C', _uuid='c')
+ >>> d = Diff(bd, bd_new)
+ >>> r = d.report_tree()
+ >>> print '\\n'.join(r.paths())
+ bugdir
+ bugdir/settings
+ bugdir/bugs
+ bugdir/bugs/new
+ bugdir/bugs/new/c
+ bugdir/bugs/rem
+ bugdir/bugs/rem/b
+ bugdir/bugs/mod
+ bugdir/bugs/mod/a
+ bugdir/bugs/mod/a/settings
+ bugdir/bugs/mod/a/comments
+ bugdir/bugs/mod/a/comments/new
+ bugdir/bugs/mod/a/comments/new/acom
+ bugdir/bugs/mod/a/comments/rem
+ bugdir/bugs/mod/a/comments/mod
+ >>> print r.report_string()
+ Changed bug directory settings:
+ target: None -> 1.0
+ New bugs:
+ abc/c:om: Bug C
+ Removed bugs:
+ abc/b:cm: Bug B
+ Modified bugs:
+ abc/a:cm: Bug A
+ Changed bug settings:
+ status: open -> closed
+ New comments:
+ from John Doe <j@doe.com> on Thu, 01 Jan 1970 00:00:00 +0000
+ I'm closing this bug...
+
+ You can also limit the report generation by providing a list of
+ subscriptions.
+
+ >>> subscriptions = [Subscription('DIR', BUGDIR_TYPE_NEW),
+ ... Subscription('b', BUG_TYPE_ALL)]
+ >>> r = d.report_tree(subscriptions)
+ >>> print r.report_string()
+ New bugs:
+ abc/c:om: Bug C
+ Removed bugs:
+ abc/b:cm: Bug B
+
+ While sending subscriptions to report_tree() makes the report
+ generation more efficient (because you may not need to compare
+ _all_ the bugs, etc.), sometimes you will have several sets of
+ subscriptions. In that case, it's better to run full_report()
+ first, and then use report_tree() to avoid redundant comparisons.
+
+ >>> d.full_report()
+ >>> print d.report_tree([subscriptions[0]]).report_string()
+ New bugs:
+ abc/c:om: Bug C
+ >>> print d.report_tree([subscriptions[1]]).report_string()
+ Removed bugs:
+ abc/b:cm: Bug B
+
+ >>> bd.cleanup()
+ """
+ def __init__(self, old_bugdir, new_bugdir):
+ self.old_bugdir = old_bugdir
+ self.new_bugdir = new_bugdir
+
+ # data assembly methods
+
+ def _changed_bugs(self, subscriptions):
+ """
+ Search for differences in all bugs between .old_bugdir and
+ .new_bugdir. Returns
+ (added_bugs, modified_bugs, removed_bugs)
+ where added_bugs and removed_bugs are lists of added and
+ removed bugs respectively. modified_bugs is a list of
+ (old_bug,new_bug) pairs.
+ """
+ bugdir_types = [s.type for s in subscriptions if s.id == BUGDIR_ID]
+ new_uuids = []
+ old_uuids = []
+ for bd_type in [BUGDIR_TYPE_ALL, BUGDIR_TYPE_NEW, BUGDIR_TYPE_MOD]:
+ if bd_type in bugdir_types:
+ new_uuids = list(self.new_bugdir.uuids())
+ break
+ for bd_type in [BUGDIR_TYPE_ALL, BUGDIR_TYPE_REM]:
+ if bd_type in bugdir_types:
+ old_uuids = list(self.old_bugdir.uuids())
+ break
+ subscribed_bugs = []
+ for s in subscriptions:
+ if s.id != BUGDIR_ID:
+ try:
+ bug = self.new_bugdir.bug_from_uuid(s.id)
+ except libbe.bugdir.NoBugMatches:
+ bug = self.old_bugdir.bug_from_uuid(s.id)
+ subscribed_bugs.append(bug.uuid)
+ new_uuids.extend([s for s in subscribed_bugs
+ if self.new_bugdir.has_bug(s)])
+ new_uuids = sorted(set(new_uuids))
+ old_uuids.extend([s for s in subscribed_bugs
+ if self.old_bugdir.has_bug(s)])
+ old_uuids = sorted(set(old_uuids))
+
+ added = []
+ removed = []
+ modified = []
+ if hasattr(self.old_bugdir, 'changed'):
+ # take advantage of a RevisionedBugDir-style changed() method
+ new_ids,mod_ids,rem_ids = self.old_bugdir.changed()
+ for id in new_ids:
+ for a_id in self.new_bugdir.storage.ancestors(id):
+ if a_id.count('/') == 0:
+ if a_id in [b.id.storage() for b in added]:
+ break
+ try:
+ bug = self.new_bugdir.bug_from_uuid(a_id)
+ added.append(bug)
+ except libbe.bugdir.NoBugMatches:
+ pass
+ for id in rem_ids:
+ for a_id in self.old_bugdir.storage.ancestors(id):
+ if a_id.count('/') == 0:
+ if a_id in [b.id.storage() for b in removed]:
+ break
+ try:
+ bug = self.old_bugdir.bug_from_uuid(a_id)
+ removed.append(bug)
+ except libbe.bugdir.NoBugMatches:
+ pass
+ for id in mod_ids:
+ for a_id in self.new_bugdir.storage.ancestors(id):
+ if a_id.count('/') == 0:
+ if a_id in [b[0].id.storage() for b in modified]:
+ break
+ try:
+ new_bug = self.new_bugdir.bug_from_uuid(a_id)
+ old_bug = self.old_bugdir.bug_from_uuid(a_id)
+ modified.append((old_bug, new_bug))
+ except libbe.bugdir.NoBugMatches:
+ pass
+ else:
+ for uuid in new_uuids:
+ new_bug = self.new_bugdir.bug_from_uuid(uuid)
+ try:
+ old_bug = self.old_bugdir.bug_from_uuid(uuid)
+ except KeyError:
+ if BUGDIR_TYPE_ALL in bugdir_types \
+ or BUGDIR_TYPE_NEW in bugdir_types \
+ or uuid in subscribed_bugs:
+ added.append(new_bug)
+ continue
+ if BUGDIR_TYPE_ALL in bugdir_types \
+ or BUGDIR_TYPE_MOD in bugdir_types \
+ or uuid in subscribed_bugs:
+ if old_bug.storage != None and old_bug.storage.is_readable():
+ old_bug.load_comments()
+ if new_bug.storage != None and new_bug.storage.is_readable():
+ new_bug.load_comments()
+ if old_bug != new_bug:
+ modified.append((old_bug, new_bug))
+ for uuid in old_uuids:
+ if not self.new_bugdir.has_bug(uuid):
+ old_bug = self.old_bugdir.bug_from_uuid(uuid)
+ removed.append(old_bug)
+ added.sort()
+ removed.sort()
+ modified.sort(self._bug_modified_cmp)
+ return (added, modified, removed)
+ def _bug_modified_cmp(self, left, right):
+ return cmp(left[1], right[1])
+ def _changed_comments(self, old, new):
+ """
+ Search for differences in all loaded comments between the bugs
+ old and new. Returns
+ (added_comments, modified_comments, removed_comments)
+ analogous to ._changed_bugs.
+ """
+ if hasattr(self, '__changed_comments'):
+ if new.uuid in self.__changed_comments:
+ return self.__changed_comments[new.uuid]
+ else:
+ self.__changed_comments = {}
+ added = []
+ removed = []
+ modified = []
+ old.comment_root.sort(key=lambda comm : comm.time)
+ new.comment_root.sort(key=lambda comm : comm.time)
+ old_comment_ids = [c.uuid for c in old.comments()]
+ new_comment_ids = [c.uuid for c in new.comments()]
+ for uuid in new_comment_ids:
+ new_comment = new.comment_from_uuid(uuid)
+ try:
+ old_comment = old.comment_from_uuid(uuid)
+ except KeyError:
+ added.append(new_comment)
+ else:
+ if old_comment != new_comment:
+ modified.append((old_comment, new_comment))
+ for uuid in old_comment_ids:
+ if uuid not in new_comment_ids:
+ old_comment = old.comment_from_uuid(uuid)
+ removed.append(old_comment)
+ self.__changed_comments[new.uuid] = (added, modified, removed)
+ return self.__changed_comments[new.uuid]
+ def _attribute_changes(self, old, new, attributes):
+ """
+ Take two objects old and new, and compare the value of *.attr
+ for attr in the list attribute names. Returns a list of
+ (attr_name, old_value, new_value)
+ tuples.
+ """
+ change_list = []
+ for attr in attributes:
+ old_value = getattr(old, attr)
+ new_value = getattr(new, attr)
+ if old_value != new_value:
+ change_list.append((attr, old_value, new_value))
+ if len(change_list) >= 0:
+ return change_list