From 4372a17b4215df25b3da0b68daf4d6b490a8955c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 19:00:40 -0500 Subject: [PATCH] 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... --- libbe/bugdir.py | 18 ++- libbe/command/base.py | 6 + libbe/command/util.py | 79 ++++++++++- libbe/ui/command_line.py | 13 +- libbe/util/id.py | 286 +++++++++++++++++++++++++++------------ 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()]) -- 2.26.2