aboutsummaryrefslogtreecommitdiffstats
path: root/libbe
diff options
context:
space:
mode:
authorW. Trevor King <wking@drexel.edu>2009-12-29 19:00:40 -0500
committerW. Trevor King <wking@drexel.edu>2009-12-29 19:00:40 -0500
commit4372a17b4215df25b3da0b68daf4d6b490a8955c (patch)
tree4464d284fe0653701e43b3dc5a465dd14da056b3 /libbe
parentd0fdc606a0420807cfbde0519d1807bd16f14c37 (diff)
downloadbugseverywhere-4372a17b4215df25b3da0b68daf4d6b490a8955c.tar.gz
Fixed up the completion helpers in libbe.command.util
This entailed a fairly thorough cleanup of libbe.util.id. Remaining unimplemented completion helpers: * complete_assigned() * complete_extra_strings() Since these would require scanning all (active?) bugs to compile lists, and I was feeling lazy...
Diffstat (limited to 'libbe')
-rw-r--r--libbe/bugdir.py18
-rw-r--r--libbe/command/base.py6
-rw-r--r--libbe/command/util.py79
-rwxr-xr-xlibbe/ui/command_line.py13
-rw-r--r--libbe/util/id.py286
5 files changed, 296 insertions, 106 deletions
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index cec1e3b..737dacf 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -39,6 +39,7 @@ import libbe.storage.util.settings_object as settings_object
import libbe.storage.util.mapfile as mapfile
import libbe.bug as bug
import libbe.util.utility as utility
+import libbe.util.id
if libbe.TESTING == True:
import doctest
@@ -73,11 +74,13 @@ class MultipleBugMatches(ValueError):
self.shortname = shortname
self.matches = matches
-class NoBugMatches(KeyError):
- def __init__(self, shortname):
- msg = "No bug matches %s" % shortname
- KeyError.__init__(self, msg)
- self.shortname = shortname
+class NoBugMatches(libbe.util.id.NoIDMatches):
+ def __init__(self, *args, **kwargs):
+ libbe.util.id.NoIDMatches.__init__(self, *args, **kwargs)
+ def __str__(self):
+ if self.msg == None:
+ return 'No bug matches %s' % self.id
+ return self.msg
class DiskAccessRequired (Exception):
def __init__(self, goal):
@@ -270,8 +273,9 @@ class BugDir (list, settings_object.SavedSettingsObject):
def bug_from_uuid(self, uuid):
if not self.has_bug(uuid):
- raise NoBugMatches('No bug matches %s\n bug map: %s\n repo: %s' \
- % (uuid, self._bug_map, self.storage))
+ raise NoBugMatches(
+ uuid, self.uuids(),
+ 'No bug matches %s in %s' % (uuid, self.storage))
if self._bug_map[uuid] == None:
self._load_bug(uuid)
return self._bug_map[uuid]
diff --git a/libbe/command/base.py b/libbe/command/base.py
index cdb4043..2318aa7 100644
--- a/libbe/command/base.py
+++ b/libbe/command/base.py
@@ -62,6 +62,12 @@ class CommandInput (object):
self.name = name
self.help = help
+ def __str__(self):
+ return '<%s %s>' % (self.__class__.__name__, self.name)
+
+ def __repr__(self):
+ return self.__str__()
+
class Argument (CommandInput):
def __init__(self, metavar=None, default=None, type='string',
optional=False, repeatable=False,
diff --git a/libbe/command/util.py b/libbe/command/util.py
index 3bd02d0..a5398cf 100644
--- a/libbe/command/util.py
+++ b/libbe/command/util.py
@@ -34,17 +34,86 @@ def complete_path(command, argument, fragment=None):
return comps
def complete_status(command, argument, fragment=None):
- return [fragment]
+ bd = command._get_bugdir()
+ import libbe.bug
+ return libbe.bug.status_values
+
def complete_severity(command, argument, fragment=None):
- return [fragment]
+ bd = command._get_bugdir()
+ import libbe.bug
+ return libbe.bug.severity_values
+
def complete_assigned(command, argument, fragment=None):
+ if fragment == None:
+ return []
return [fragment]
+
def complete_extra_strings(command, argument, fragment=None):
+ if fragment == None:
+ return []
return [fragment]
+
def complete_bug_id(command, argument, fragment=None):
- return [fragment]
-def complete_bug_comment_id(command, argument, fragment=None):
- return [fragment]
+ return complete_bug_comment_id(command, argument, fragment,
+ comments=False)
+
+def complete_bug_comment_id(command, argument, fragment=None,
+ active_only=True, comments=True):
+ import libbe.bugdir
+ import libbe.util.id
+ bd = command._get_bugdir()
+ if fragment == None or len(fragment) == 0:
+ fragment = '/'
+ try:
+ p = libbe.util.id.parse_user(bd, fragment)
+ matches = None
+ root,residual = (fragment, None)
+ if not root.endswith('/'):
+ root += '/'
+ except libbe.util.id.InvalidIDStructure, e:
+ return []
+ except libbe.util.id.NoIDMatches:
+ return []
+ except libbe.util.id.MultipleIDMatches, e:
+ if e.common == None:
+ # choose among bugdirs
+ return e.matches
+ common = e.common
+ matches = e.matches
+ root,residual = libbe.util.id.residual(common, fragment)
+ p = libbe.util.id.parse_user(bd, e.common)
+ bug = None
+ if matches == None: # fragment was complete, get a list of children uuids
+ if p['type'] == 'bugdir':
+ matches = bd.uuids()
+ common = bd.id.user()
+ elif p['type'] == 'bug':
+ if comments == False:
+ return [fragment]
+ bug = bd.bug_from_uuid(p['bug'])
+ matches = bug.uuids()
+ common = bug.id.user()
+ else:
+ assert p['type'] == 'comment', p
+ return [fragment]
+ if p['type'] == 'bugdir':
+ child_fn = bd.bug_from_uuid
+ elif p['type'] == 'bug':
+ if comments == False:
+ return[fragment]
+ if bug == None:
+ bug = bd.bug_from_uuid(p['bug'])
+ child_fn = bug.comment_from_uuid
+ elif p['type'] == 'comment':
+ assert matches == None, matches
+ return [fragment]
+ possible = []
+ common += '/'
+ for m in matches:
+ child = child_fn(m)
+ id = child.id.user()
+ possible.append(id.replace(common, root))
+ return possible
def select_values(string, possible_values, name="unkown"):
"""
diff --git a/libbe/ui/command_line.py b/libbe/ui/command_line.py
index b5a3991..b99f812 100755
--- a/libbe/ui/command_line.py
+++ b/libbe/ui/command_line.py
@@ -113,15 +113,19 @@ class CmdOptionParser(optparse.OptionParser):
self.complete(argument, fragment)
for i,arg in enumerate(parsed_args):
if arg == '--complete':
- if i < len(self.command.args):
+ if i > 0 and self.command.name == 'be':
+ break # let this pass through for the command parser to handle
+ elif i < len(self.command.args):
argument = self.command.args[i]
+ elif len(self.command.args) == 0:
+ break # command doesn't take arguments
else:
argument = self.command.args[-1]
if argument.repeatable == False:
raise libbe.command.UserError('Too many arguments')
fragment = None
- if i < len(args) - 1:
- fragment = args[i+1]
+ if i < len(parsed_args) - 1:
+ fragment = parsed_args[i+1]
self.complete(argument, fragment)
if len(parsed_args) > len(self.command.args) \
and self.command.args[-1].repeatable == False:
@@ -149,7 +153,8 @@ class CmdOptionParser(optparse.OptionParser):
comps = self.command.complete(argument, fragment)
if fragment != None:
comps = [c for c in comps if c.startswith(fragment)]
- print '\n'.join(comps)
+ if len(comps) > 0:
+ print '\n'.join(comps)
raise CallbackExit
diff --git a/libbe/util/id.py b/libbe/util/id.py
index 6b6b51d..f229bef 100644
--- a/libbe/util/id.py
+++ b/libbe/util/id.py
@@ -67,33 +67,56 @@ HIERARCHY = ['bugdir', 'bug', 'comment']
class MultipleIDMatches (ValueError):
- def __init__(self, id, matches):
- msg = ("More than one id matches %s. "
- "Please be more specific.\n%s" % (id, matches))
+ def __init__(self, id, common, matches):
+ msg = ('More than one id matches %s. '
+ 'Please be more specific (%s/*).\n%s' % (id, common, matches))
ValueError.__init__(self, msg)
self.id = id
+ self.common = common
self.matches = matches
class NoIDMatches (KeyError):
- def __init__(self, id, possible_ids):
- msg = "No id matches %s.\n%s" % (id, possible_ids)
- KeyError.__init__(self, msg)
+ def __init__(self, id, possible_ids, msg=None):
+ KeyError.__init__(self, id)
self.id = id
self.possible_ids = possible_ids
+ self.msg = msg
+ def __str__(self):
+ if self.msg == None:
+ return 'No id matches %s.\n%s' % (self.id, self.possible_ids)
+ return self.msg
+
+class InvalidIDStructure (KeyError):
+ def __init__(self, id, msg=None):
+ KeyError.__init__(self, id)
+ self.id = id
+ self.msg = msg
+ def __str__(self):
+ if self.msg == None:
+ return 'Invalid id structure "%s"' % self.id
+ return self.msg
-
-def _assemble(*args):
+def _assemble(args, check_length=False):
args = list(args)
for i,arg in enumerate(args):
if arg == None:
args[i] = ''
- return '/'.join(args)
-
-def _split(id):
+ id = '/'.join(args)
+ if check_length == True:
+ assert len(args) > 0, args
+ if len(args) > 3:
+ raise InvalidIDStructure(id, '%d > 3 levels in "%s"' % (len(args), id))
+ return id
+
+def _split(id, check_length=False):
args = id.split('/')
for i,arg in enumerate(args):
if arg == '':
args[i] = None
+ if check_length == True:
+ assert len(args) > 0, args
+ if len(args) > 3:
+ raise InvalidIDStructure(id, '%d > 3 levels in "%s"' % (len(args), id))
return args
def _truncate(uuid, other_uuids, min_length=3):
@@ -105,14 +128,21 @@ def _truncate(uuid, other_uuids, min_length=3):
chars+=1
return uuid[:chars]
-def _expand(truncated_id, other_ids):
+def _expand(truncated_id, common, other_ids):
+ other_ids = list(other_ids)
+ if len(other_ids) == 0:
+ raise NoIDMatches(truncated_id, other_ids)
+ if truncated_id == None:
+ if len(other_ids) == 1:
+ return other_ids[0]
+ raise MultipleIDMatches(truncated_id, common, other_ids)
matches = []
other_ids = list(other_ids)
for id in other_ids:
if id.startswith(truncated_id):
matches.append(id)
if len(matches) > 1:
- raise MultipleIDMatches(truncated_id, matches)
+ raise MultipleIDMatches(truncated_id, common, matches)
if len(matches) == 0:
raise NoIDMatches(truncated_id, other_ids)
return matches[0]
@@ -172,7 +202,7 @@ class ID (object):
assert self._type in HIERARCHY, self._type
def storage(self, *args):
- return _assemble(self._object.uuid, *args)
+ return _assemble([self._object.uuid]+list(args))
def _ancestors(self):
ret = [self._object]
@@ -187,7 +217,8 @@ class ID (object):
return ret
def long_user(self):
- return _assemble(*[o.uuid for o in self._ancestors()])
+ return _assemble([o.uuid for o in self._ancestors()],
+ check_length=True)
def user(self):
ids = []
@@ -196,7 +227,7 @@ class ID (object):
ids.append(None)
else:
ids.append(_truncate(o.uuid, o.sibling_uuids()))
- return _assemble(*ids)
+ return _assemble(ids, check_length=True)
def child_uuids(child_storage_ids):
"""
@@ -210,54 +241,74 @@ def child_uuids(child_storage_ids):
if len(fields) == 1:
yield fields[0]
+def long_to_short_user(bugdirs, id):
+ ids = _split(id, check_length=True)
+ bugdir = [bd for bd in bugdirs if bd.uuid == ids[0]][0]
+ objects = [bugdir]
+ if len(ids) >= 2:
+ bug = bugdir.bug_from_uuid(ids[1])
+ objects.append(bug)
+ if len(ids) >= 3:
+ comment = bug.comment_from_uuid(ids[2])
+ objects.append(comment)
+ for i,obj in enumerate(objects):
+ ids[i] = _truncate(ids[i], obj.sibling_uuids())
+ return _assemble(ids)
+
+def short_to_long_user(bugdirs, id):
+ ids = _split(id, check_length=True)
+ ids[0] = _expand(ids[0], common=None,
+ other_ids=[bd.uuid for bd in bugdirs])
+ if len(ids) == 1:
+ return _assemble(ids)
+ bugdir = [bd for bd in bugdirs if bd.uuid == ids[0]][0]
+ ids[1] = _expand(ids[1], common=bugdir.id.user(),
+ other_ids=bugdir.uuids())
+ if len(ids) == 2:
+ return _assemble(ids)
+ bug = bugdir.bug_from_uuid(ids[1])
+ ids[2] = _expand(ids[2], common=bug.id.user(),
+ other_ids=bug.uuids())
+ return _assemble(ids)
+
REGEXP = '#([-a-f0-9]*)(/[-a-g0-9]*)?(/[-a-g0-9]*)?#'
class IDreplacer (object):
- def __init__(self, bugdirs, direction):
+ def __init__(self, bugdirs, replace_fn):
self.bugdirs = bugdirs
- self.direction = direction
+ self.replace_fn = replace_fn
def __call__(self, match):
- ids = [m.lstrip('/') for m in match.groups() if m != None]
- ids = self.switch_ids(ids)
- return '#' + '/'.join(ids) + '#'
- def switch_id(self, id, sibling_uuids):
- if id == None:
- return None
- if self.direction == 'long_to_short':
- return _truncate(id, sibling_uuids)
- return _expand(id, sibling_uuids)
- def switch_ids(self, ids):
- assert ids[0] != None, ids
- if self.direction == 'long_to_short':
- bugdir = [bd for bd in self.bugdirs if bd.uuid == ids[0]][0]
- objects = [bugdir]
- if len(ids) >= 2:
- bug = bugdir.bug_from_uuid(ids[1])
- objects.append(bug)
- if len(ids) >= 3:
- comment = bug.comment_from_uuid(ids[2])
- objects.append(comment)
- for i,obj in enumerate(objects):
- ids[i] = self.switch_id(ids[i], obj.sibling_uuids())
- else:
- ids[0] = self.switch_id(ids[0], [bd.uuid for bd in self.bugdirs])
- if len(ids) == 1:
- return ids
- bugdir = [bd for bd in self.bugdirs if bd.uuid == ids[0]][0]
- ids[1] = self.switch_id(ids[1], bugdir.uuids())
- if len(ids) == 2:
- return ids
- bug = bugdir.bug_from_uuid(ids[1])
- ids[2] = self.switch_id(ids[2], bug.uuids())
- return ids
-
-def short_to_long_user(bugdirs, text):
- return re.sub(REGEXP, IDreplacer(bugdirs, 'short_to_long'), text)
-
-def long_to_short_user(bugdirs, text):
- return re.sub(REGEXP, IDreplacer(bugdirs, 'long_to_short'), text)
+ ids = []
+ for m in match.groups():
+ if m == None:
+ m = ''
+ ids.append(m)
+ return '#' + self.replace_fn(self.bugdirs, ''.join(ids)) + '#'
+
+def short_to_long_text(bugdirs, text):
+ return re.sub(REGEXP, IDreplacer(bugdirs, short_to_long_user), text)
+def long_to_short_text(bugdirs, text):
+ return re.sub(REGEXP, IDreplacer(bugdirs, long_to_short_user), text)
+
+def residual(base, fragment):
+ """
+ >>> residual('ABC/DEF/', '//GHI')
+ ('//', 'GHI')
+ >>> residual('ABC/DEF/', '/D/GHI')
+ ('/D/', 'GHI')
+ >>> residual('ABC/DEF', 'A/D/GHI')
+ ('A/D/', 'GHI')
+ >>> residual('ABC/DEF', 'A/D/GHI/JKL')
+ ('A/D/', 'GHI/JKL')
+ """
+ base = base.rstrip('/') + '/'
+ ids = fragment.split('/')
+ base_count = base.count('/')
+ root_ids = ids[:base_count] + ['']
+ residual_ids = ids[base_count:]
+ return ('/'.join(root_ids), '/'.join(residual_ids))
def _parse_user(id):
"""
@@ -270,21 +321,34 @@ def _parse_user(id):
>>> _parse_user('ABC') == \\
... {'bugdir':'ABC', 'type':'bugdir'}
True
+ >>> _parse_user('') == \\
+ ... {'bugdir':None, 'type':'bugdir'}
+ True
+ >>> _parse_user('/') == \\
+ ... {'bugdir':None, 'bug':None, 'type':'bug'}
+ True
+ >>> _parse_user('/DEF/') == \\
+ ... {'bugdir':None, 'bug':'DEF', 'comment':None, 'type':'comment'}
+ True
+ >>> _parse_user('a/b/c/d')
+ Traceback (most recent call last):
+ ...
+ InvalidIDStructure: 4 > 3 levels in "a/b/c/d"
"""
ret = {}
- args = _split(id)
- assert len(args) > 0 and len(args) < 4, 'Invalid id "%s"' % id
- for type,arg in zip(HIERARCHY, args):
- assert len(arg) > 0, 'Invalid part "%s" of id "%s"' % (arg, id)
+ args = _split(id, check_length=True)
+ for i,(type,arg) in enumerate(zip(HIERARCHY, args)):
+ if arg != None and len(arg) == 0:
+ raise InvalidIDStructure(
+ id, 'Invalid %s part %d "%s" of id "%s"' % (type, i, arg, id))
ret['type'] = type
ret[type] = arg
return ret
def parse_user(bugdir, id):
- long_id = short_to_long_user([bugdir], '#%s#' % id).strip('#')
+ long_id = short_to_long_user([bugdir], id)
return _parse_user(long_id)
-
if libbe.TESTING == True:
class UUIDtestCase(unittest.TestCase):
def testUUID_gen(self):
@@ -292,22 +356,28 @@ if libbe.TESTING == True:
self.failUnless(len(id) == 36, 'invalid UUID "%s"' % id)
class DummyObject (object):
- def __init__(self, uuid, siblings=[]):
+ def __init__(self, uuid, parent=None, siblings=[]):
self.uuid = uuid
self._siblings = siblings
+ if parent == None:
+ type_i = 0
+ else:
+ assert parent.type in HIERARCHY, parent
+ setattr(self, parent.type, parent)
+ type_i = HIERARCHY.index(parent.type) + 1
+ self.type = HIERARCHY[type_i]
+ self.id = ID(self, self.type)
def sibling_uuids(self):
return self._siblings
class IDtestCase(unittest.TestCase):
def setUp(self):
self.bugdir = DummyObject('1234abcd')
- self.bug = DummyObject('abcdef', ['a1234', 'ab9876'])
- self.bug.bugdir = self.bugdir
- self.comment = DummyObject('12345678', ['1234abcd', '1234cdef'])
- self.comment.bug = self.bug
- self.bd_id = ID(self.bugdir, 'bugdir')
- self.b_id = ID(self.bug, 'bug')
- self.c_id = ID(self.comment, 'comment')
+ self.bug = DummyObject('abcdef', self.bugdir, ['a1234', 'ab9876'])
+ self.comment = DummyObject('12345678', self.bug, ['1234abcd', '1234cdef'])
+ self.bd_id = self.bugdir.id
+ self.b_id = self.bug.id
+ self.c_id = self.comment.id
def test_storage(self):
self.failUnless(self.bd_id.storage() == self.bugdir.uuid,
self.bd_id.storage())
@@ -315,8 +385,9 @@ if libbe.TESTING == True:
self.b_id.storage())
self.failUnless(self.c_id.storage() == self.comment.uuid,
self.c_id.storage())
- self.failUnless(self.bd_id.storage('x','y','z') == \
- '1234abcd/x/y/z', self.bd_id.storage())
+ self.failUnless(self.bd_id.storage('x', 'y', 'z') == \
+ '1234abcd/x/y/z',
+ self.bd_id.storage('x', 'y', 'z'))
def test_long_user(self):
self.failUnless(self.bd_id.long_user() == self.bugdir.uuid,
self.bd_id.long_user())
@@ -338,30 +409,65 @@ if libbe.TESTING == True:
class ShortLongParseTestCase(unittest.TestCase):
def setUp(self):
self.bugdir = DummyObject('1234abcd')
- self.bug = DummyObject('abcdef', ['a1234', 'ab9876'])
- self.bug.bugdir = self.bugdir
+ self.bug = DummyObject('abcdef', self.bugdir, ['a1234', 'ab9876'])
+ self.comment = DummyObject('12345678', self.bug, ['1234abcd', '1234cdef'])
+ self.bd_id = self.bugdir.id
+ self.b_id = self.bug.id
+ self.c_id = self.comment.id
self.bugdir.bug_from_uuid = lambda uuid: self.bug
self.bugdir.uuids = lambda : self.bug.sibling_uuids() + [self.bug.uuid]
- self.comment = DummyObject('12345678', ['1234abcd', '1234cdef'])
- self.comment.bug = self.bug
self.bug.comment_from_uuid = lambda uuid: self.comment
self.bug.uuids = lambda : self.comment.sibling_uuids() + [self.comment.uuid]
- self.bd_id = ID(self.bugdir, 'bugdir')
- self.b_id = ID(self.bug, 'bug')
- self.c_id = ID(self.comment, 'comment')
self.short = 'bla bla #123/abc# bla bla #123/abc/12345# bla bla'
self.long = 'bla bla #1234abcd/abcdef# bla bla #1234abcd/abcdef/12345678# bla bla'
- self.short_id = '123/abc'
- def test_short_to_long(self):
- self.failUnless(short_to_long_user([self.bugdir], self.short) == self.long,
- '\n' + self.short + '\n' + short_to_long_user([self.bugdir], self.short) + '\n' + self.long)
- def test_long_to_short(self):
- self.failUnless(long_to_short_user([self.bugdir], self.long) == self.short,
- '\n' + long_to_short_user([self.bugdir], self.long) + '\n' + self.short)
+ self.short_id_parse_pairs = [
+ ('', {'bugdir':'1234abcd', 'type':'bugdir'}),
+ ('123/abc', {'bugdir':'1234abcd', 'bug':'abcdef',
+ 'type':'bug'}),
+ ('123/abc/12345', {'bugdir':'1234abcd', 'bug':'abcdef',
+ 'comment':'12345678', 'type':'comment'}),
+ ]
+ self.short_id_exception_pairs = [
+ ('z', NoIDMatches('z', ['1234abcd'])),
+ ('///', InvalidIDStructure(
+ '///', msg='4 > 3 levels in "///"')),
+ ('/', MultipleIDMatches(
+ None, '123', ['a1234', 'ab9876', 'abcdef'])),
+ ('123/', MultipleIDMatches(
+ None, '123', ['a1234', 'ab9876', 'abcdef'])),
+ ('123/abc/', MultipleIDMatches(
+ None, '123/abc', ['1234abcd','1234cdef','12345678'])),
+ ]
+ def test_short_to_long_text(self):
+ self.failUnless(short_to_long_text([self.bugdir], self.short) == self.long,
+ '\n' + self.short + '\n' + short_to_long_text([self.bugdir], self.short) + '\n' + self.long)
+ def test_long_to_short_text(self):
+ self.failUnless(long_to_short_text([self.bugdir], self.long) == self.short,
+ '\n' + long_to_short_text([self.bugdir], self.long) + '\n' + self.short)
def test_parse_user(self):
- self.failUnless(parse_user(self.bugdir, self.short_id) == \
- {'bugdir':'1234abcd', 'bug':'abcdef', 'type':'bug'},
- parse_user(self.bugdir, self.short_id))
+ for short_id,parsed in self.short_id_parse_pairs:
+ ret = parse_user(self.bugdir, short_id)
+ self.failUnless(ret == parsed,
+ 'got %s\nexpected %s' % (ret, parsed))
+ def test_parse_user_exceptions(self):
+ for short_id,exception in self.short_id_exception_pairs:
+ try:
+ ret = parse_user(self.bugdir, short_id)
+ self.fail('Expected parse_user(bugdir, "%s") to raise %s,'
+ '\n but it returned %s'
+ % (short_id, exception.__class__.__name__, ret))
+ except exception.__class__, e:
+ for attr in dir(e):
+ if attr.startswith('_') or attr == 'args':
+ continue
+ value = getattr(e, attr)
+ expected = getattr(exception, attr)
+ self.failUnless(
+ value == expected,
+ 'Expected parse_user(bugdir, "%s") %s.%s'
+ '\n to be %s, but it is %s\n\n%s'
+ % (short_id, exception.__class__.__name__,
+ attr, expected, value, e))
unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])