diff options
-rw-r--r-- | NEWS | 8 | ||||
-rw-r--r-- | becommands/diff.py | 98 | ||||
-rw-r--r-- | becommands/subscribe.py | 120 | ||||
-rwxr-xr-x | interfaces/email/interactive/be-handle-mail | 67 | ||||
-rw-r--r-- | libbe/diff.py | 265 |
5 files changed, 357 insertions, 201 deletions
@@ -1,3 +1,11 @@ +December 5, 2009 + * changes to `be diff` + * exits with an error if required revision control is not possible. + Previously it printed a message, but exitted with status 0. + * removed options --new, --removed, --modified, --all + * added options --uuids, --subscribe + * assorted cleanups and bugfixes + December 4, 2009 * new commands: email-bugs diff --git a/becommands/diff.py b/becommands/diff.py index 8e6c0f8..2cff537 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -40,17 +40,20 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): Changed bug settings: status: open -> closed >>> if bd.vcs.versioned == True: - ... execute(["--modified", original], manipulate_encodings=False) + ... execute(["--subscribe", "%(bugdir_id)s:mod", "--uuids", original], + ... manipulate_encodings=False) ... else: ... print "a" a >>> if bd.vcs.versioned == False: ... execute([original], manipulate_encodings=False) ... else: - ... print "This directory is not revision-controlled." - This directory is not revision-controlled. + ... raise cmdutil.UsageError('This directory is not revision-controlled.') + Traceback (most recent call last): + ... + UsageError: This directory is not revision-controlled. >>> bd.cleanup() - """ + """ % {'bugdir_id':diff.BUGDIR_ID} parser = get_parser() options, args = parser.parse_args(args) cmdutil.default_complete(options, args, parser) @@ -59,63 +62,58 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): if len(args) == 1: revision = args[0] if len(args) > 1: - raise cmdutil.UsageError("Too many arguments.") + raise cmdutil.UsageError('Too many arguments.') + try: + subscriptions = diff.subscriptions_from_string( + options.subscribe) + except ValueError, e: + raise cmdutil.UsageError(e.msg) bd = bugdir.BugDir(from_disk=True, manipulate_encodings=manipulate_encodings) if bd.vcs.versioned == False: - print "This directory is not revision-controlled." + raise cmdutil.UsageError('This directory is not revision-controlled.') + if options.dir == None: + if revision == None: # get the most recent revision + revision = bd.vcs.revision_id(-1) + old_bd = bd.duplicate_bugdir(revision) else: - if options.dir == None: - if revision == None: # get the most recent revision - revision = bd.vcs.revision_id(-1) - old_bd = bd.duplicate_bugdir(revision) + cwd = os.getcwd() + os.chdir(options.dir) + old_bd_current = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + if revision == None: # use the current working state + old_bd = old_bd_current else: - cwd = os.getcwd() - os.chdir(options.dir) - old_bd_current = bugdir.BugDir(from_disk=True, manipulate_encodings=False) - if revision == None: # use the current working state - old_bd = old_bd_current - else: - old_bd = old_bd_current.duplicate_bugdir(revision) - os.chdir(cwd) - d = diff.Diff(old_bd, bd) - tree = d.report_tree() + if old_bd_current.vcs.versioned == False: + raise cmdutil.UsageError('%s is not revision-controlled.' + % options.dir) + old_bd = old_bd_current.duplicate_bugdir(revision) + os.chdir(cwd) + d = diff.Diff(old_bd, bd) + tree = d.report_tree(subscriptions) + if options.uuids == True: uuids = [] - if options.all == True: - options.new = options.modified = options.removed = True - if options.new == True: - uuids.extend([c.name for c in tree.child_by_path("/bugs/new")]) - if options.modified == True: - uuids.extend([c.name for c in tree.child_by_path("/bugs/mod")]) - if options.removed == True: - uuids.extend([c.name for c in tree.child_by_path("/bugs/rem")]) - if (options.new or options.modified or options.removed) == True: - print "\n".join(uuids) - else : - rep = tree.report_string() - if rep != None: - print rep - bd.remove_duplicate_bugdir() - if options.dir != None and revision != None: - old_bd_current.remove_duplicate_bugdir() + bugs = tree.child_by_path('/bugs') + for bug_type in bugs: + uuids.extend([bug.name for bug in bug_type]) + print '\n'.join(uuids) + else : + rep = tree.report_string() + if rep != None: + print rep + bd.remove_duplicate_bugdir() + if options.dir != None and revision != None: + old_bd_current.remove_duplicate_bugdir() def get_parser(): parser = cmdutil.CmdOptionParser("be diff [options] REVISION") - # boolean options - bools = (("n", "new", "Print UUIDS for new bugs"), - ("m", "modified", "Print UUIDS for modified bugs"), - ("r", "removed", "Print UUIDS for removed bugs"), - ("a", "all", "Print UUIDS for all changed bugs")) - for s in bools: - attr = s[1].replace('-','_') - short = "-%c" % s[0] - long = "--%s" % s[1] - help = s[2] - parser.add_option(short, long, action="store_true", - default=False, dest=attr, help=help) parser.add_option("-d", "--dir", dest="dir", metavar="DIR", help="Compare with repository in DIR instead of the current directory.") + parser.add_option("-s", "--subscribe", dest="subscribe", metavar="SUBSCRIPTION", + help="Only print changes matching SUBSCRIPTION, subscription is a comma-separ\ated list of ID:TYPE tuples. See `be subscribe --help` for descriptions of ID and TYPE.") + parser.add_option("-u", "--uuids", action="store_true", dest="uuids", + help="Only print the bug UUIDS.", default=False) return parser longhelp=""" @@ -128,7 +126,7 @@ For Arch your specifier must be a fully-qualified revision name. Besides the standard summary output, you can use the options to output UUIDS for the different categories. This output can be used as the -input to 'be show' to get and understanding of the current status. +input to 'be show' to get an understanding of the current status. """ def help(): diff --git a/becommands/subscribe.py b/becommands/subscribe.py index 051341b..19aac53 100644 --- a/becommands/subscribe.py +++ b/becommands/subscribe.py @@ -14,47 +14,12 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """(Un)subscribe to change notification""" -from libbe import cmdutil, bugdir, tree +from libbe import cmdutil, bugdir, tree, diff import os, copy __desc__ = __doc__ TAG="SUBSCRIBE:" -class SubscriptionType (tree.Tree): - """ - Trees of subscription types to allow users to select exactly what - notifications they want to subscribe to. - """ - def __init__(self, type_name, *args, **kwargs): - tree.Tree.__init__(self, *args, **kwargs) - self.type = type_name - def __str__(self): - return self.type - def __repr__(self): - return "<SubscriptionType: %s>" % str(self) - def string_tree(self, indent=0): - lines = [] - for depth,node in self.thread(): - lines.append("%s%s" % (" "*(indent+2*depth), node)) - return "\n".join(lines) - -BUGDIR_TYPE_NEW = SubscriptionType("new") -BUGDIR_TYPE_ALL = SubscriptionType("all", [BUGDIR_TYPE_NEW]) - -# same name as BUGDIR_TYPE_ALL for consistency -BUG_TYPE_ALL = SubscriptionType(str(BUGDIR_TYPE_ALL)) - -INVALID_TYPE = SubscriptionType("INVALID") - -class InvalidType (ValueError): - def __init__(self, type_name, type_root): - msg = "Invalid type %s for tree:\n%s" \ - % (type_name, type_root.string_tree(4)) - ValueError.__init__(self, msg) - self.type_name = type_name - self.type_root = type_root - - def execute(args, manipulate_encodings=True, restrict_file_access=False): """ >>> bd = bugdir.SimpleBugDir() @@ -126,20 +91,20 @@ def execute(args, manipulate_encodings=True, restrict_file_access=False): servers = options.servers.split(",") types = options.types.split(",") - if len(args) == 0 or args[0] == "DIR": # directory-wide subscriptions - type_root = BUGDIR_TYPE_ALL + if len(args) == 0 or args[0] == diff.BUGDIR_ID: # directory-wide subscriptions + type_root = diff.BUGDIR_TYPE_ALL entity = bd entity_name = "bug directory" else: # bug-specific subscriptions - type_root = BUG_TYPE_ALL + type_root = diff.BUG_TYPE_ALL bug = bd.bug_from_shortname(args[0]) entity = bug entity_name = bug.uuid if options.list_all == True: entity_name = "anything in the bug directory" - types = [type_from_name(name, type_root, default=INVALID_TYPE, - default_ok=options.unsubscribe) + types = [diff.type_from_name(name, type_root, default=diff.INVALID_TYPE, + default_ok=options.unsubscribe) for name in types] estrs = entity.extra_strings if options.list == True or options.list_all == True: @@ -198,7 +163,7 @@ you of changes, although there is no way to guarantee this behavior. Available TYPES: For bugs: %s - For DIR : + For %s: %s For unsubscription, any listed SERVERS and TYPES are removed from your @@ -210,8 +175,9 @@ if you're just hacking away on your private repository, you'll known what's changed ;). This command just (un)sets the appropriate subscriptions, and leaves it up to each interface to perform the notification. -""" % (BUG_TYPE_ALL.string_tree(6), BUGDIR_TYPE_ALL.string_tree(6), - BUGDIR_TYPE_ALL) +""" % (diff.BUG_TYPE_ALL.string_tree(6), diff.BUGDIR_ID, + diff.BUGDIR_TYPE_ALL.string_tree(6), + diff.BUGDIR_TYPE_ALL) def help(): return get_parser().help_str() + longhelp @@ -227,7 +193,7 @@ def _parse_string(string, type_root): assert string.startswith(TAG), string string = string[len(TAG):] subscriber,types,servers = string.split("\t") - types = [type_from_name(name, type_root) for name in types.split(",")] + types = [diff.type_from_name(name, type_root) for name in types.split(",")] return (subscriber,types,servers.split(",")) def _get_subscriber(extra_strings, subscriber, type_root): @@ -240,16 +206,6 @@ def _get_subscriber(extra_strings, subscriber, type_root): # functions exposed to other modules -def type_from_name(name, type_root, default=None, default_ok=False): - if name == str(type_root): - return type_root - for t in type_root.traverse(): - if name == str(t): - return t - if default_ok: - return default - raise InvalidType(name, type_root) - def subscribe(extra_strings, subscriber, types, servers, type_root): args = _get_subscriber(extra_strings, subscriber, type_root) if args == None: # no match @@ -311,17 +267,22 @@ def get_subscribers(extra_strings, type, server, type_root, >>> def sgs(*args, **kwargs): ... return sorted(get_subscribers(*args, **kwargs)) >>> es = [] - >>> es = subscribe(es, "John Doe <j@doe.com>", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL) - >>> es = subscribe(es, "Jane Doe <J@doe.com>", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL) - >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "John Doe <j@doe.com>", [diff.BUGDIR_TYPE_ALL], + ... ["a.com"], diff.BUGDIR_TYPE_ALL) + >>> es = subscribe(es, "Jane Doe <J@doe.com>", [diff.BUGDIR_TYPE_NEW], + ... ["*"], diff.BUGDIR_TYPE_ALL) + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL) ['John Doe <j@doe.com>'] - >>> sgs(es, BUGDIR_TYPE_ALL, "a.com", BUGDIR_TYPE_ALL, match_descendant_types=True) + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "a.com", diff.BUGDIR_TYPE_ALL, + ... match_descendant_types=True) ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>'] - >>> sgs(es, BUGDIR_TYPE_ALL, "b.net", BUGDIR_TYPE_ALL, match_descendant_types=True) + >>> sgs(es, diff.BUGDIR_TYPE_ALL, "b.net", diff.BUGDIR_TYPE_ALL, + ... match_descendant_types=True) ['Jane Doe <J@doe.com>'] - >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL) + >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL) ['Jane Doe <J@doe.com>'] - >>> sgs(es, BUGDIR_TYPE_NEW, "a.com", BUGDIR_TYPE_ALL, match_ancestor_types=True) + >>> sgs(es, diff.BUGDIR_TYPE_NEW, "a.com", diff.BUGDIR_TYPE_ALL, + ... match_ancestor_types=True) ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>'] """ for string in extra_strings: @@ -353,36 +314,43 @@ def get_bugdir_subscribers(bugdir, server): Returns a dict of dicts: subscribers[user][id] = types where id is either a bug.uuid (in the case of a bug subscription) - or "DIR" (in the case of a bugdir subscription). + or "%(bugdir_id)s" (in the case of a bugdir subscription). Only checks bugs that are currently in memory, so you might want to call bugdir.load_all_bugs() first. >>> bd = bugdir.SimpleBugDir(sync_with_disk=False) >>> a = bd.bug_from_shortname("a") - >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe <j@doe.com>", [BUGDIR_TYPE_ALL], ["a.com"], BUGDIR_TYPE_ALL) - >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe <J@doe.com>", [BUGDIR_TYPE_NEW], ["*"], BUGDIR_TYPE_ALL) - >>> a.extra_strings = subscribe(a.extra_strings, "John Doe <j@doe.com>", [BUG_TYPE_ALL], ["a.com"], BUG_TYPE_ALL) + >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe <j@doe.com>", + ... [diff.BUGDIR_TYPE_ALL], ["a.com"], diff.BUGDIR_TYPE_ALL) + >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe <J@doe.com>", + ... [diff.BUGDIR_TYPE_NEW], ["*"], diff.BUGDIR_TYPE_ALL) + >>> a.extra_strings = subscribe(a.extra_strings, "John Doe <j@doe.com>", + ... [diff.BUG_TYPE_ALL], ["a.com"], diff.BUG_TYPE_ALL) >>> subscribers = get_bugdir_subscribers(bd, "a.com") - >>> subscribers["Jane Doe <J@doe.com>"]["DIR"] + >>> subscribers["Jane Doe <J@doe.com>"]["%(bugdir_id)s"] [<SubscriptionType: new>] - >>> subscribers["John Doe <j@doe.com>"]["DIR"] + >>> subscribers["John Doe <j@doe.com>"]["%(bugdir_id)s"] [<SubscriptionType: all>] >>> subscribers["John Doe <j@doe.com>"]["a"] [<SubscriptionType: all>] >>> get_bugdir_subscribers(bd, "b.net") - {'Jane Doe <J@doe.com>': {'DIR': [<SubscriptionType: new>]}} + {'Jane Doe <J@doe.com>': {'%(bugdir_id)s': [<SubscriptionType: new>]}} >>> bd.cleanup() - """ + """ % {'bugdir_id':diff.BUGDIR_ID} subscribers = {} - for sub in get_subscribers(bugdir.extra_strings, BUGDIR_TYPE_ALL, server, - BUGDIR_TYPE_ALL, match_descendant_types=True): - i,s,ts,srvs = _get_subscriber(bugdir.extra_strings,sub,BUGDIR_TYPE_ALL) + for sub in get_subscribers(bugdir.extra_strings, diff.BUGDIR_TYPE_ALL, + server, diff.BUGDIR_TYPE_ALL, + match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub, + diff.BUGDIR_TYPE_ALL) subscribers[sub] = {"DIR":ts} for bug in bugdir: - for sub in get_subscribers(bug.extra_strings, BUG_TYPE_ALL, server, - BUG_TYPE_ALL, match_descendant_types=True): - i,s,ts,srvs = _get_subscriber(bug.extra_strings,sub,BUG_TYPE_ALL) + for sub in get_subscribers(bug.extra_strings, diff.BUG_TYPE_ALL, + server, diff.BUG_TYPE_ALL, + match_descendant_types=True): + i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub, + diff.BUG_TYPE_ALL) if sub in subscribers: subscribers[sub][bug.uuid] = ts else: diff --git a/interfaces/email/interactive/be-handle-mail b/interfaces/email/interactive/be-handle-mail index 3b321cf..10f6884 100755 --- a/interfaces/email/interactive/be-handle-mail +++ b/interfaces/email/interactive/be-handle-mail @@ -306,6 +306,8 @@ class DiffTree (libbe.diff.DiffTree): """ def report_or_none(self): report = self.report() + if report == None: + return None payload = report.get_payload() if payload == None or len(payload) == 0: return None @@ -315,7 +317,7 @@ class DiffTree (libbe.diff.DiffTree): if report == None: return "No changes" else: - return send_pgp_mime.flatten(self.report(), to_unicode=True) + return send_pgp_mime.flatten(report, to_unicode=True) def make_root(self): return MIMEMultipart() def join(self, root, parent, data_part): @@ -658,64 +660,29 @@ class Message (object): bd.load_all_bugs() subscribers = subscribe.get_bugdir_subscribers(bd, THIS_SERVER) - if len(subscribers) == 0: - return [] + return [] + for subscriber,subscriptions in subscribers.items(): + subscribers[subscriber] = [] + for id,types in subscriptions.items(): + for type in types: + subscribers[subscriber].append( + libbe.diff.Subscription(id,type)) before_bd, after_bd = self._get_before_and_after_bugdirs(bd, previous_revision) diff = Diff(before_bd, after_bd) - diff_tree = diff.report_tree(diff_tree=DiffTree) - bug_index = {} - for child in diff_tree.child_by_path("/bugs/new"): - bug_index[child.name] = ("added", child) - for child in diff_tree.child_by_path("/bugs/mod"): - bug_index[child.name] = ("modified", child) - for child in diff_tree.child_by_path("/bugs/rem"): - bug_index[child.name] = ("removed", child) + diff.full_report(diff_tree=DiffTree) header = self._subscriber_header(bd, previous_revision) emails = [] for subscriber,subscriptions in subscribers.items(): header.replace_header("to", subscriber) - parts = [] - if "DIR" in subscriptions: # make sure we check the DIR level first - ordered_subscriptions = [("DIR", subscriptions.pop("DIR"))] - else: - ordered_subscriptions = [] - ordered_subscriptions.extend(subscriptions.items()) - for id,types in ordered_subscriptions: - if id == "DIR": - if subscribe.BUGDIR_TYPE_ALL in types: - parts.append(diff_tree.report_or_none()) - break # we've attached everything, so stop checking. - if subscribe.BUGDIR_TYPE_NEW in types: - new = diff_tree.child_by_path("/bugs/new") - parts.append(new.report_or_none()) - continue # move on to next id - # if we get this far, id refers to a bug. - assert types == [subscribe.BUG_TYPE_ALL], types - if id not in bug_index: - continue # no changes here, move on to next id - type,bug_root = bug_index[id] - if type == "added" \ - and "DIR" in subscriptions \ - and subscriptions["DIR"] == subscribe.BUGDIR_TYPE_NEW: - # this info already attached at the DIR level - continue # move on to next id - parts.append(bug_root.report_or_none()) - parts = [p for p in parts if p != None] - if len(parts) == 0: - continue # no email to this subscriber - elif len(parts) == 1: - root = parts[0] - else: # join subscription parts into a single body - root = MIMEMultipart() - root[u"Content-Description"] = u"Multiple subscription trees." - for part in parts: - root.attach(part) - emails.append(send_pgp_mime.attach_root(header, root)) - if LOGFILE != None: - LOGFILE.write(u"Preparing to notify %s of changes\n" % subscriber) + report = diff.report_tree(subscriptions, diff_tree=DiffTree) + root = report.report_or_none() + if root != None: + emails.append(send_pgp_mime.attach_root(header, root)) + if LOGFILE != None: + LOGFILE.write(u"Preparing to notify %s of changes\n" % subscriber) return emails def _get_before_and_after_bugdirs(self, bd, previous_revision=None): if previous_revision == None: diff --git a/libbe/diff.py b/libbe/diff.py index c25f7a7..b3cd6bc 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -19,6 +19,7 @@ """Compare two bug trees.""" import difflib +import types import libbe from libbe import bugdir, bug, settings_object, tree @@ -27,6 +28,111 @@ if libbe.TESTING == True: import doctest +class SubscriptionType (tree.Tree): + """ + Trees of subscription types to allow users to select exactly what + notifications they want to subscribe to. + """ + def __init__(self, type_name, *args, **kwargs): + tree.Tree.__init__(self, *args, **kwargs) + self.type = type_name + def __str__(self): + return self.type + def __cmp__(self, other): + return cmp(self.type, other.type) + def __repr__(self): + return "<SubscriptionType: %s>" % str(self) + def string_tree(self, indent=0): + lines = [] + for depth,node in self.thread(): + lines.append("%s%s" % (" "*(indent+2*depth), node)) + return "\n".join(lines) + +BUGDIR_ID = "DIR" +BUGDIR_TYPE_NEW = SubscriptionType("new") +BUGDIR_TYPE_MOD = SubscriptionType("mod") +BUGDIR_TYPE_REM = SubscriptionType("rem") +BUGDIR_TYPE_ALL = SubscriptionType("all", + [BUGDIR_TYPE_NEW, BUGDIR_TYPE_MOD, BUGDIR_TYPE_REM]) + +# same name as BUGDIR_TYPE_ALL for consistency +BUG_TYPE_ALL = SubscriptionType(str(BUGDIR_TYPE_ALL)) + +INVALID_TYPE = SubscriptionType("INVALID") + +class InvalidType (ValueError): + def __init__(self, type_name, type_root): + msg = "Invalid type %s for tree:\n%s" \ + % (type_name, type_root.string_tree(4)) + ValueError.__init__(self, msg) + self.type_name = type_name + self.type_root = type_root + +def type_from_name(name, type_root, default=None, default_ok=False): + if name == str(type_root): + return type_root + for t in type_root.traverse(): + if name == str(t): + return t + if default_ok: + return default + raise InvalidType(name, type_root) + +class Subscription (object): + """ + >>> subscriptions = [Subscription('XYZ', 'all'), + ... Subscription('DIR', 'new'), + ... Subscription('ABC', BUG_TYPE_ALL),] + >>> print sorted(subscriptions) + [<Subscription: DIR (new)>, <Subscription: ABC (all)>, <Subscription: XYZ (all)>] + """ + def __init__(self, id, subscription_type, **kwargs): + if 'type_root' not in kwargs: + if id == BUGDIR_ID: + kwargs['type_root'] = BUGDIR_TYPE_ALL + else: + kwargs['type_root'] = BUG_TYPE_ALL + if type(subscription_type) in types.StringTypes: + subscription_type = type_from_name(subscription_type, **kwargs) + self.id = id + self.type = subscription_type + def __cmp__(self, other): + for attr in 'id', 'type': + value = cmp(getattr(self, attr), getattr(other, attr)) + if value != 0: + if self.id == BUGDIR_ID: + return -1 + elif other.id == BUGDIR_ID: + return 1 + return value + def __str__(self): + return str(self.type) + def __repr__(self): + return "<Subscription: %s (%s)>" % (self.id, self.type) + +def subscriptions_from_string(string=None, subscription_sep=',', id_sep=':'): + """ + >>> 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 (tree.Tree): """ A tree holding difference data for easy report generation. @@ -104,21 +210,25 @@ class DiffTree (tree.Tree): 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): - return "\n".join(self.report()) + 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 None + return root data_part = self.data_part(depth) - if self.requires_children == True and len(self) == 0: + 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: - child.report(root, self, depth) + for child in self: + root = child.report(root, self, depth) return root def make_root(self): return [] @@ -187,6 +297,33 @@ class Diff (object): 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: + c:om: Bug C + Removed bugs: + 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: + c:om: Bug C + >>> print d.report_tree([subscriptions[1]]).report_string() + Removed bugs: + b:cm: Bug B + >>> bd.cleanup() """ def __init__(self, old_bugdir, new_bugdir): @@ -195,7 +332,7 @@ class Diff (object): # data assembly methods - def _changed_bugs(self): + def _changed_bugs(self, subscriptions): """ Search for differences in all bugs between .old_bugdir and .new_bugdir. Returns @@ -204,33 +341,56 @@ class Diff (object): removed bugs respectively. modified_bugs is a list of (old_bug,new_bug) pairs. """ - if hasattr(self, "__changed_bugs"): - return self.__changed_bugs + 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 = [s.id for s in subscriptions + if BUG_TYPE_ALL.has_descendant( \ + s.type, match_self=True)] + 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 = [] - for uuid in self.new_bugdir.uuids(): + 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: - added.append(new_bug) - else: + 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.sync_with_disk == True: old_bug.load_comments() if new_bug.sync_with_disk == True: new_bug.load_comments() if old_bug != new_bug: modified.append((old_bug, new_bug)) - for uuid in self.old_bugdir.uuids(): + 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) - self.__changed_bugs = (added, modified, removed) - return self.__changed_bugs + return (added, modified, removed) def _bug_modified_cmp(self, left, right): return cmp(left[1], right[1]) def _changed_comments(self, old, new): @@ -302,25 +462,81 @@ class Diff (object): # report generation methods - def report_tree(self, diff_tree=DiffTree): + def full_report(self, diff_tree=DiffTree): + """ + Generate a full report for efficiency if you'll be using + .report_tree() with several sets of subscriptions. + """ + self._cached_full_report = self.report_tree(diff_tree=diff_tree, + allow_cached=False) + self._cached_full_report_diff_tree = diff_tree + def _sub_report(self, subscriptions): + """ + Return ._cached_full_report masked for subscriptions. + """ + root = self._cached_full_report + bugdir_types = [s.type for s in subscriptions if s.id == BUGDIR_ID] + subscribed_bugs = [s.id for s in subscriptions + if BUG_TYPE_ALL.has_descendant( \ + s.type, match_self=True)] + selected_by_bug = [node.name + for node in root.child_by_path('bugdir/bugs')] + if BUGDIR_TYPE_ALL in bugdir_types: + for node in root.traverse(): + node.masked = False + selected_by_bug = [] + else: + try: + node = root.child_by_path('bugdir/settings') + node.masked = True + except KeyError: + pass + for name,type in (('new', BUGDIR_TYPE_NEW), + ('mod', BUGDIR_TYPE_MOD), + ('rem', BUGDIR_TYPE_REM)): + if type in bugdir_types: + bugs = root.child_by_path('bugdir/bugs/%s' % name) + for bug_node in bugs: + for node in bug_node.traverse(): + node.masked = False + selected_by_bug.remove(name) + for name in selected_by_bug: + bugs = root.child_by_path('bugdir/bugs/%s' % name) + for bug_node in bugs: + if bug_node.name in subscribed_bugs: + for node in bug_node.traverse(): + node.masked = False + else: + for node in bug_node.traverse(): + node.masked = True + return root + def report_tree(self, subscriptions=None, diff_tree=DiffTree, + allow_cached=True): """ Pretty bare to make it easy to adjust to specific cases. You can pass in a DiffTree subclass via diff_tree to override the default report assembly process. """ - if hasattr(self, "__report_tree"): - return self.__report_tree + if allow_cached == True \ + and hasattr(self, '_cached_full_report') \ + and diff_tree == self._cached_full_report_diff_tree: + return self._sub_report(subscriptions) + if subscriptions == None: + subscriptions = [Subscription(BUGDIR_ID, BUGDIR_TYPE_ALL)] bugdir_settings = sorted(self.new_bugdir.settings_properties) bugdir_settings.remove("vcs_name") # tweaked by bugdir.duplicate_bugdir root = diff_tree("bugdir") - bugdir_attribute_changes = self._bugdir_attribute_changes() - if len(bugdir_attribute_changes) > 0: - bugdir = diff_tree("settings", bugdir_attribute_changes, - self.bugdir_attribute_change_string) - root.append(bugdir) + bugdir_subscriptions = [s.type for s in subscriptions + if s.id == BUGDIR_ID] + if BUGDIR_TYPE_ALL in bugdir_subscriptions: + bugdir_attribute_changes = self._bugdir_attribute_changes() + if len(bugdir_attribute_changes) > 0: + bugdir = diff_tree("settings", bugdir_attribute_changes, + self.bugdir_attribute_change_string) + root.append(bugdir) bug_root = diff_tree("bugs") root.append(bug_root) - add,mod,rem = self._changed_bugs() + add,mod,rem = self._changed_bugs(subscriptions) bnew = diff_tree("new", "New bugs:", requires_children=True) bug_root.append(bnew) for bug in add: @@ -370,8 +586,7 @@ class Diff (object): self.comment_body_change_string) c.append(cbody) cr.extend([cnew, crem, cmod]) - self.__report_tree = root - return self.__report_tree + return root # change data -> string methods. # Feel free to play with these in subclasses. |