aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorW. Trevor King <wking@drexel.edu>2009-12-05 08:16:27 -0500
committerW. Trevor King <wking@drexel.edu>2009-12-05 08:16:27 -0500
commita4c0d5ffed4bbaaf2cfbd791fb13dbf666c4ae12 (patch)
tree200162e04d789caae909f3d62654dcbebd283cca
parent49b8c7e7da7fa3a4f3c6092cf0bfdb1c1de863e8 (diff)
parentd3122f5c72cc0a0c345bf0bd545f9e3217ca934f (diff)
downloadbugseverywhere-a4c0d5ffed4bbaaf2cfbd791fb13dbf666c4ae12.tar.gz
Merged be.diff-subscribe
Highlights: * 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 * New method diff.Diff.full_report() allows fast generation of similar report_tree()s via diff.DiffTree.masked. * New method diff.subscriptions_from_string() for consistent subscription string parsing. * clean up be-handle-mail.Message.subscriber_emails() with diff.Diff.report_tree(subscriptions) * hardcoded 'DIR' replaced with diff.BUGDIR_ID * assorted cleanups and bugfixes
-rw-r--r--NEWS8
-rw-r--r--becommands/diff.py98
-rw-r--r--becommands/subscribe.py120
-rwxr-xr-xinterfaces/email/interactive/be-handle-mail67
-rw-r--r--libbe/diff.py265
5 files changed, 357 insertions, 201 deletions
diff --git a/NEWS b/NEWS
index 669bed2..21a0140 100644
--- a/NEWS
+++ b/NEWS
@@ -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.