From e5177b9150290004f472d08c13dfe78075f029e8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 03:51:27 -0500 Subject: Extend libbe.util.id to handle id (path) creation. --- libbe/util/id.py | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 libbe/util/id.py (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py new file mode 100644 index 0000000..0f1576c --- /dev/null +++ b/libbe/util/id.py @@ -0,0 +1,110 @@ +# Copyright (C) 2008-2009 Gianluca Montecchi +# W. Trevor King +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +Handle ID creation and parsing. +""" + +import libbe + +if libbe.TESTING == True: + import unittest + +try: + from uuid import uuid4 # Python >= 2.5 + def uuid_gen(): + id = uuid4() + idstr = id.urn + start = "urn:uuid:" + assert idstr.startswith(start) + return idstr[len(start):] +except ImportError: + import os + import sys + from subprocess import Popen, PIPE + + def uuid_gen(): + # Shell-out to system uuidgen + args = ['uuidgen', 'r'] + try: + if sys.platform != "win32": + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) + else: + # win32 don't have os.execvp() so have to run command in a shell + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, + shell=True, cwd=cwd) + except OSError, e : + strerror = "%s\nwhile executing %s" % (e.args[1], args) + raise OSError, strerror + output, error = q.communicate() + status = q.wait() + if status != 0: + strerror = "%s\nwhile executing %s" % (status, args) + raise Exception, strerror + return output.rstrip('\n') + + +def _assemble(*args): + for i,arg in enumerate(args): + if arg == None: + args[i] = '' + return '/'.join(args) + +def _split(id): + args = id.split('/') + for i,arg in enumerate(args): + if arg == '': + args[i] = None + return args + + +def bugdir_id(bugdir, *args): + return _assemble(bugdir.uuid, args) + +def bug_id(bug, *args): + if bug.bug == None: + bugdir_id = None + else: + bugdir_id = bugdir_id(bug.bugdir) + return _assemble(bugdir_id, bug.uuid, args) + +def comment_id(comment, *args): + if comment.bug == None: + bug_id = None + else: + bug_id = bug_id(comment.bug) + return _assemble(bug_id, comment.uuid, args) + +def parse_id(id): + args = _split(id) + ret = {'bugdir':args.pop(0)} + type = 'bugdir' + for child_name in ['bug', 'comment']: + if len(args) > 0 and is_a_uuid(args[0]): + ret[child_name] = args.pop(0) + type = child_name + ret['type'] = type + ret['remaining'] = os.path.join(args) + return ret + +if libbe.TESTING == True: + class UUIDtestCase(unittest.TestCase): + def testUUID_gen(self): + id = uuid_gen() + self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) + + suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) -- cgit From 44b4e3f8b6405d0e1e0ebf6cb526ab62cdbbdb25 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 08:54:50 -0500 Subject: Transitioned bugdir.py to new storage format. --- libbe/util/id.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index 0f1576c..d57205f 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -19,6 +19,8 @@ Handle ID creation and parsing. """ +import os.path + import libbe if libbe.TESTING == True: @@ -59,6 +61,7 @@ except ImportError: def _assemble(*args): + args = list(args) for i,arg in enumerate(args): if arg == None: args[i] = '' @@ -71,31 +74,41 @@ def _split(id): args[i] = None return args +def _is_a_uuid(id): + if id.startswith('uuid:'): + return True + return False + +def _uuid_to_id(id): + return 'uuid:' + id + +def _id_to_uuid(id): + return id[len('uuid:'):] def bugdir_id(bugdir, *args): - return _assemble(bugdir.uuid, args) + return _assemble(_uuid_to_id(bugdir.uuid), *args) def bug_id(bug, *args): - if bug.bug == None: - bugdir_id = None + if bug.bugdir == None: + bdid = None else: - bugdir_id = bugdir_id(bug.bugdir) - return _assemble(bugdir_id, bug.uuid, args) + bdid = bugdir_id(bug.bugdir) + return _assemble(bdid, _uuid_to_id(bug.uuid), *args) def comment_id(comment, *args): if comment.bug == None: - bug_id = None + bid = None else: - bug_id = bug_id(comment.bug) - return _assemble(bug_id, comment.uuid, args) + bid = bug_id(comment.bug) + return _assemble(bid, _uuid_to_id(comment.uuid), *args) def parse_id(id): args = _split(id) - ret = {'bugdir':args.pop(0)} + ret = {'bugdir':_id_to_uuid(args.pop(0))} type = 'bugdir' for child_name in ['bug', 'comment']: - if len(args) > 0 and is_a_uuid(args[0]): - ret[child_name] = args.pop(0) + if len(args) > 0 and _is_a_uuid(args[0]): + ret[child_name] = _id_to_uuid(args.pop(0)) type = child_name ret['type'] = type ret['remaining'] = os.path.join(args) -- cgit From f52fc3a243edf5ccef2dcdfd0c4b4cded4357e13 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Wed, 9 Dec 2009 07:23:54 -0500 Subject: Rethought libbe.util.id module --- libbe/util/id.py | 294 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 254 insertions(+), 40 deletions(-) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index d57205f..d443706 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -20,10 +20,13 @@ Handle ID creation and parsing. """ import os.path +import re import libbe if libbe.TESTING == True: + import doctest + import sys import unittest try: @@ -60,6 +63,25 @@ except ImportError: return output.rstrip('\n') +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)) + ValueError.__init__(self, msg) + self.id = id + 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) + self.id = id + self.possible_ids = possible_ids + + def _assemble(*args): args = list(args) for i,arg in enumerate(args): @@ -74,50 +96,242 @@ def _split(id): args[i] = None return args -def _is_a_uuid(id): - if id.startswith('uuid:'): - return True - return False - -def _uuid_to_id(id): - return 'uuid:' + id - -def _id_to_uuid(id): - return id[len('uuid:'):] - -def bugdir_id(bugdir, *args): - return _assemble(_uuid_to_id(bugdir.uuid), *args) - -def bug_id(bug, *args): - if bug.bugdir == None: - bdid = None - else: - bdid = bugdir_id(bug.bugdir) - return _assemble(bdid, _uuid_to_id(bug.uuid), *args) - -def comment_id(comment, *args): - if comment.bug == None: - bid = None - else: - bid = bug_id(comment.bug) - return _assemble(bid, _uuid_to_id(comment.uuid), *args) - -def parse_id(id): - args = _split(id) - ret = {'bugdir':_id_to_uuid(args.pop(0))} - type = 'bugdir' - for child_name in ['bug', 'comment']: - if len(args) > 0 and _is_a_uuid(args[0]): - ret[child_name] = _id_to_uuid(args.pop(0)) - type = child_name - ret['type'] = type - ret['remaining'] = os.path.join(args) +def _truncate(uuid, other_uuids, min_length=3): + chars = min_length + for id in other_uuids: + if id == uuid: + continue + while (id[:chars] == uuid[:chars]): + chars+=1 + return uuid[:chars] + +def _expand(truncated_id, other_ids): + matches = [] + for id in other_ids: + if id.startswith(truncated_id): + matches.append(id) + if len(matches) > 1: + raise MultipleIDMatches(truncated_id, matches) + if len(matches) == 0: + raise NoIDMatches(truncated_id, other_ids) + return matches[0] + + +class ID (object): + """ + IDs have several formats specialized for different uses. + + In storage, all objects are represented by their uuid alone, + because that is the simplest globally unique identifier. You can + generate ids of this sort with the .storage() method. Because an + object's storage may be distributed across several chunks, and the + chunks may not have their own uuid, we generate chunk ids by + prepending the objects uuid to the chunk name. The user id types + do not support this chunk extension feature. + + For users, the full uuids are a bit overwhelming, so we truncate + them while retaining local uniqueness (with regards to the other + objects currently in storage). We also prepend truncated parent + ids for two reasons: + (1) so that a user can locate the repository containing the + referenced object. It would be hard to find bug 'XYZ' if + that's all you knew. Much easier with 'ABC/XYZ', where ABC + is the bugdir. Each project can publish a list of bugdir-id +x - to - location mappings, e.g. + ABC...(full uuid)...DEF https://server.com/projectX/be/ + which is easier than publishing all-object-ids-to-location + mappings. + (2) because it's easier to generate and parse truncated ids if + you don't have to fetch all the ids in the storage + repository, but can restrict yourself to a specific branch. + You can generate ids of this sort with the .user() method, + although in order to preform the truncation, your object (and its + parents must define a .sibling_uuids() method. + + + While users can use the convenient short user ids in the short + term, the truncation will inevitably lead to name collision. To + avoid that, we provide a non-truncated form of the short user ids + via the .long_user() method. These long user ids should be + converted to short user ids by intelligent user interfaces. + + Related tools: + * get uuids back out of the user ids: + parse_user() + * scan text for user ids & convert to long user ids: + short_to_long_user() + * scan text for long user ids & convert to short user ids: + long_to_short_user() + + Supported types: 'bugdir', 'bug', 'comment' + """ + def __init__(self, object, type): + self._object = object + self._type = type + assert self._type in HIERARCHY, self._type + self.uuid = self._object.uuid + + def storage(self, *args): + return _assemble(self._object.uuid, *args) + + def _ancestors(self): + ret = [self._object] + index = HIERARCHY.index(self._type) + if index == 0: + return ret + o = self._object + for i in range(index, 0, -1): + parent_name = HIERARCHY[i-1] + o = getattr(o, parent_name) + ret.insert(0, o) + return ret + + def long_user(self): + return _assemble(*[o.uuid for o in self._ancestors()]) + + def user(self): + return _assemble(*[_truncate(o.uuid, o.sibling_uuids()) + for o in self._ancestors()]) + +def parse_user(id): + """ + >>> parse_user('ABC/DEF/GHI') == \\ + ... {'bugdir':'ABC', 'bug':'DEF', 'comment':'GHI', 'type':'comment'} + True + >>> parse_user('ABC/DEF') == \\ + ... {'bugdir':'ABC', 'bug':'DEF', 'type':'bug'} + True + >>> parse_user('ABC') == \\ + ... {'bugdir':'ABC', 'type':'bugdir'} + True + """ + 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) + ret['type'] = type + ret[type] = arg return ret +REGEXP = '#([-a-f0-9]*)(/[-a-g0-9]*)?(/[-a-g0-9]*)?#' + +class IDreplacer (object): + def __init__(self, bugdirs, direction): + self.bugdirs = bugdirs + self.direction = direction + 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) + if libbe.TESTING == True: class UUIDtestCase(unittest.TestCase): def testUUID_gen(self): id = uuid_gen() - self.failUnless(len(id) == 36, "invalid UUID '%s'" % id) + self.failUnless(len(id) == 36, 'invalid UUID "%s"' % id) + + class DummyObject (object): + def __init__(self, uuid, siblings=[]): + self.uuid = uuid + self._siblings = siblings + 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') + def test_storage(self): + self.failUnless(self.bd_id.storage() == self.bugdir.uuid, + self.bd_id.storage()) + self.failUnless(self.b_id.storage() == self.bug.uuid, + 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()) + def test_long_user(self): + self.failUnless(self.bd_id.long_user() == self.bugdir.uuid, + self.bd_id.long_user()) + self.failUnless(self.b_id.long_user() == \ + '/'.join([self.bugdir.uuid, self.bug.uuid]), + self.b_id.long_user()) + self.failUnless(self.c_id.long_user() == + '/'.join([self.bugdir.uuid, self.bug.uuid, + self.comment.uuid]), + self.c_id.long_user) + def test_user(self): + self.failUnless(self.bd_id.user() == '123', + self.bd_id.user()) + self.failUnless(self.b_id.user() == '123/abc', + self.b_id.user()) + self.failUnless(self.c_id.user() == '123/abc/12345', + self.c_id.user()) + + class IDtestCase(unittest.TestCase): + def setUp(self): + self.bugdir = DummyObject('1234abcd') + self.bug = DummyObject('abcdef', ['a1234', 'ab9876']) + self.bug.bugdir = self.bugdir + 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' + 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) - suite = unittest.TestLoader().loadTestsFromTestCase(UUIDtestCase) + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) -- cgit From a153347564e4c6baa0388fda05530f5548d16ac5 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 10 Dec 2009 19:31:47 -0500 Subject: Moved bugdir, bug, and comment over to new id implementation. --- libbe/util/id.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index d443706..ab62359 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -137,7 +137,7 @@ class ID (object): referenced object. It would be hard to find bug 'XYZ' if that's all you knew. Much easier with 'ABC/XYZ', where ABC is the bugdir. Each project can publish a list of bugdir-id -x - to - location mappings, e.g. + - to - location mappings, e.g. ABC...(full uuid)...DEF https://server.com/projectX/be/ which is easier than publishing all-object-ids-to-location mappings. @@ -169,9 +169,9 @@ x - to - location mappings, e.g. self._object = object self._type = type assert self._type in HIERARCHY, self._type - self.uuid = self._object.uuid def storage(self, *args): + import libbe.comment return _assemble(self._object.uuid, *args) def _ancestors(self): @@ -182,7 +182,7 @@ x - to - location mappings, e.g. o = self._object for i in range(index, 0, -1): parent_name = HIERARCHY[i-1] - o = getattr(o, parent_name) + o = getattr(o, parent_name, None) ret.insert(0, o) return ret @@ -190,8 +190,26 @@ x - to - location mappings, e.g. return _assemble(*[o.uuid for o in self._ancestors()]) def user(self): - return _assemble(*[_truncate(o.uuid, o.sibling_uuids()) - for o in self._ancestors()]) + ids = [] + for o in self._ancestors(): + if o == None: + ids.append(None) + else: + ids.append(_truncate(o.uuid, o.sibling_uuids())) + return _assemble(*ids) + +def child_uuids(child_storage_ids): + """ + Extract uuid children from other children generated by the + ID.storage() method. + >>> list(child_uuids(['abc123/values', '123abc', '123def'])) + ['123abc', '123def'] + """ + for id in child_storage_ids: + fields = libbe.util.id._split(id) + if len(fields) == 1: + yield fields[0] + def parse_user(id): """ -- cgit From 4d057dab603f42ec40b911dbee6792dcf107bd14 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 13 Dec 2009 06:19:23 -0500 Subject: Converted libbe.storage.vcs.base to new Storage format. --- libbe/util/id.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index ab62359..645a17c 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -206,7 +206,7 @@ def child_uuids(child_storage_ids): ['123abc', '123def'] """ for id in child_storage_ids: - fields = libbe.util.id._split(id) + fields = _split(id) if len(fields) == 1: yield fields[0] -- cgit From 1bec5c0d3880a1cd848d765365104e221f390e71 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 14 Dec 2009 02:01:06 -0500 Subject: Added parse_user() calls to Assign --- libbe/util/id.py | 59 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 23 deletions(-) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index 645a17c..3838259 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -107,6 +107,7 @@ def _truncate(uuid, other_uuids, min_length=3): def _expand(truncated_id, other_ids): matches = [] + other_ids = list(other_ids) for id in other_ids: if id.startswith(truncated_id): matches.append(id) @@ -209,28 +210,7 @@ def child_uuids(child_storage_ids): fields = _split(id) if len(fields) == 1: yield fields[0] - - -def parse_user(id): - """ - >>> parse_user('ABC/DEF/GHI') == \\ - ... {'bugdir':'ABC', 'bug':'DEF', 'comment':'GHI', 'type':'comment'} - True - >>> parse_user('ABC/DEF') == \\ - ... {'bugdir':'ABC', 'bug':'DEF', 'type':'bug'} - True - >>> parse_user('ABC') == \\ - ... {'bugdir':'ABC', 'type':'bugdir'} - True - """ - 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) - ret['type'] = type - ret[type] = arg - return ret + REGEXP = '#([-a-f0-9]*)(/[-a-g0-9]*)?(/[-a-g0-9]*)?#' @@ -275,9 +255,37 @@ class IDreplacer (object): 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) + +def _parse_user(id): + """ + >>> _parse_user('ABC/DEF/GHI') == \\ + ... {'bugdir':'ABC', 'bug':'DEF', 'comment':'GHI', 'type':'comment'} + True + >>> _parse_user('ABC/DEF') == \\ + ... {'bugdir':'ABC', 'bug':'DEF', 'type':'bug'} + True + >>> _parse_user('ABC') == \\ + ... {'bugdir':'ABC', 'type':'bugdir'} + True + """ + 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) + ret['type'] = type + ret[type] = arg + return ret + +def parse_user(bugdir, id): + long_id = short_to_long_user([bugdir], '#%s#' % id).strip('#') + return _parse_user(long_id) + + if libbe.TESTING == True: class UUIDtestCase(unittest.TestCase): def testUUID_gen(self): @@ -328,7 +336,7 @@ if libbe.TESTING == True: self.failUnless(self.c_id.user() == '123/abc/12345', self.c_id.user()) - class IDtestCase(unittest.TestCase): + class ShortLongParseTestCase(unittest.TestCase): def setUp(self): self.bugdir = DummyObject('1234abcd') self.bug = DummyObject('abcdef', ['a1234', 'ab9876']) @@ -344,12 +352,17 @@ if libbe.TESTING == True: 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) + 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)) unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) -- cgit From 89b7a1411e4658e831f5d635534b24355dbb941d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 15 Dec 2009 06:44:20 -0500 Subject: Fixed libbe.command.diff + ugly BugDir.duplicate_bugdir implementation duplicate_bugdir() works, but for the vcs backends, it could require shelling out for _every_ file read. This could, and probably will, be horribly slow. Still it works ;). I'm not sure what a better implementation would be. The old implementation checked out the entire earlier state into a temporary directory pros: single shell out, simple upgrade implementation cons: wouldn't work well for HTTP backens I think a good solution would run along the lines of the currently commented out code in duplicate_bugdir(), where a VersionedStorage.changed_since(revision) call would give you a list of changed files. diff could work off of that directly, without the need to generate a whole duplicate bugdir. I'm stuck on how to handle upgrades though... Also removed trailing whitespace from all python files. --- libbe/util/id.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index 3838259..adc827c 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -50,7 +50,7 @@ except ImportError: q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE) else: # win32 don't have os.execvp() so have to run command in a shell - q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, + q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True, cwd=cwd) except OSError, e : strerror = "%s\nwhile executing %s" % (e.args[1], args) @@ -210,7 +210,7 @@ def child_uuids(child_storage_ids): fields = _split(id) if len(fields) == 1: yield fields[0] - + REGEXP = '#([-a-f0-9]*)(/[-a-g0-9]*)?(/[-a-g0-9]*)?#' @@ -298,7 +298,7 @@ if libbe.TESTING == True: self._siblings = siblings def sibling_uuids(self): return self._siblings - + class IDtestCase(unittest.TestCase): def setUp(self): self.bugdir = DummyObject('1234abcd') @@ -342,11 +342,11 @@ if libbe.TESTING == True: self.bug = DummyObject('abcdef', ['a1234', 'ab9876']) self.bug.bugdir = self.bugdir self.bugdir.bug_from_uuid = lambda uuid: self.bug - self.bugdir.uuids = lambda : self.bug.sibling_uuids() + [self.bug.uuid] + 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.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') -- cgit From 214c4317bb90684dcfdab4d2402daa66fbad2e77 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 27 Dec 2009 15:58:29 -0500 Subject: Fixed libbe.storage.util.upgrade Note that it only upgrades on-disk versions, so you can't use a non-VCS storage backend whose version isn't your command's current storage version. See #bea/110/bd1# for reasoning. To see the on-disk storage version, look at .be/version To see your command's supported storage version, look at be --full-version I added test_upgrade.sh to exercise the upgrade mechanism on BE's own repository. --- libbe/util/id.py | 1 - 1 file changed, 1 deletion(-) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index adc827c..6b6b51d 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -172,7 +172,6 @@ class ID (object): assert self._type in HIERARCHY, self._type def storage(self, *args): - import libbe.comment return _assemble(self._object.uuid, *args) def _ancestors(self): -- cgit From 4372a17b4215df25b3da0b68daf4d6b490a8955c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 19:00:40 -0500 Subject: 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/util/id.py | 286 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 196 insertions(+), 90 deletions(-) (limited to 'libbe/util/id.py') 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()]) -- cgit From 4d4283ecd654f1efb058cd7f7dba6be88b70ee92 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 1 Jan 2010 08:11:08 -0500 Subject: Updated copyright information --- libbe/util/id.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index f229bef..3945b20 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -1,4 +1,4 @@ -# Copyright (C) 2008-2009 Gianluca Montecchi +# Copyright (C) 2008-2010 Gianluca Montecchi # W. Trevor King # # This program is free software; you can redistribute it and/or modify -- cgit From 8b143aa8280181ebdd7b48ac72cda685329f3002 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 21 Jan 2010 17:25:05 -0500 Subject: Don't raise MultipleIDMatches if one of the matches is exact. --- libbe/util/id.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index 3945b20..4537c86 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -140,6 +140,8 @@ def _expand(truncated_id, common, other_ids): other_ids = list(other_ids) for id in other_ids: if id.startswith(truncated_id): + if id == truncated_id: + return id matches.append(id) if len(matches) > 1: raise MultipleIDMatches(truncated_id, common, matches) -- cgit From 4bdab70a66bf050ae72dc5960462adcb717b3f26 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Wed, 27 Jan 2010 12:06:53 -0500 Subject: `be html` links ( Date: Sat, 30 Jan 2010 11:24:39 -0500 Subject: libbe.command.html.HTMLGen._long_to_linked_user() handles failed conversion. Before, anything matching libbe.util.id.REGEXP was convert-or-die. Now it's convert-or-no-op. Much safer ;). The new _long_to_linked_user doctest would have failed with the old implementation. --- libbe/util/id.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index 3c6c957..81f5396 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -69,7 +69,7 @@ HIERARCHY = ['bugdir', 'bug', 'comment'] class MultipleIDMatches (ValueError): def __init__(self, id, common, matches): msg = ('More than one id matches %s. ' - 'Please be more specific (%s/*).\n%s' % (id, common, matches)) + 'Please be more specific (%s*).\n%s' % (id, common, matches)) ValueError.__init__(self, msg) self.id = id self.common = common @@ -249,7 +249,12 @@ def child_uuids(child_storage_ids): 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] + matching_bugdirs = [bd for bd in bugdirs if bd.uuid == ids[0]] + if len(matching_bugdirs) == 0: + raise NoIDMatches(id, [bd.uuid for bd in bugdirs]) + elif len(matching_bugdirs) > 1: + raise MultipleIDMatches(id, '', [bd.uuid for bd in bugdirs]) + bugdir = matching_bugdirs[0] objects = [bugdir] if len(ids) >= 2: bug = bugdir.bug_from_uuid(ids[1]) -- cgit From 977eff5af10b50ba6e6edb6abc4f40804c418b12 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 7 Feb 2010 17:53:53 -0500 Subject: Fixed docstrings so only Sphinx errors are "autosummary" and "missing attribute" --- libbe/util/id.py | 303 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 263 insertions(+), 40 deletions(-) (limited to 'libbe/util/id.py') diff --git a/libbe/util/id.py b/libbe/util/id.py index 81f5396..76079e7 100644 --- a/libbe/util/id.py +++ b/libbe/util/id.py @@ -15,8 +15,57 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" -Handle ID creation and parsing. +"""Handle ID creation and parsing. + +Format +====== + +BE IDs are formatted:: + + [/[/]] + +where each ``<..>`` is a UUID. For example:: + + bea86499-824e-4e77-b085-2d581fa9ccab/3438b72c-6244-4f1d-8722-8c8d41484e35 + +refers to bug ``3438b72c-6244-4f1d-8722-8c8d41484e35`` which is +located in bug directory ``bea86499-824e-4e77-b085-2d581fa9ccab``. +This is a bit of a mouthful, so you can truncate each UUID so long as +it remains unique. For example:: + + bea/343 + +If there were two bugs ``3438...`` and ``343a...`` in ``bea``, you'd +have to use:: + + bea/3438 + +BE will only truncate each UUID down to three characters to slightly +future-proof the short user ids. However, if you want to save keystrokes +and you *know* there is only one bug directory, feel free to truncate +all the way to zero characters:: + + /3438 + +Cross references +================ + +To refer to other bug-directories/bugs/comments from bug comments, simply +enclose the ID in pound signs (``#``). BE will automatically expand the +truncations to the full UUIDs before storing the comment, and the reference +will be appropriately truncated (and hyperlinked, if possible) when the +comment is displayed. + +Scope +===== + +Although bug and comment IDs always appear in compound references, +UUIDs at each level are globally unique. For example, comment +``bea/343/ba96f1c0-ba48-4df8-aaf0-4e3a3144fc46`` will *only* appear +under ``bea/343``. The prefix (``bea/343``) allows BE to reduce +caching global comment-lookup tables and enables easy error messages +("I couldn't find ``bea/343/ba9`` because I don't know where the +``bea`` bug directory is located"). """ import os.path @@ -64,9 +113,21 @@ except ImportError: HIERARCHY = ['bugdir', 'bug', 'comment'] - +"""Keep track of the object type hierarchy. +""" class MultipleIDMatches (ValueError): + """Multiple IDs match the given user ID. + + Parameters + ---------- + id : str + The not-specific-enough truncated UUID. + common : str + The initial characters common to all matching UUIDs. + matches : list of str + The list of possibly matching UUIDs. + """ def __init__(self, id, common, matches): msg = ('More than one id matches %s. ' 'Please be more specific (%s*).\n%s' % (id, common, matches)) @@ -76,6 +137,17 @@ class MultipleIDMatches (ValueError): self.matches = matches class NoIDMatches (KeyError): + """No IDs match the given user ID. + + Parameters + ---------- + id : str + The not-matching, possibly truncated UUID. + possible_ids : list of str + The list of potential UUIDs at that level. + msg : str, optional + A helpful message explaining what went wrong. + """ def __init__(self, id, possible_ids, msg=None): KeyError.__init__(self, id) self.id = id @@ -87,6 +159,15 @@ class NoIDMatches (KeyError): return self.msg class InvalidIDStructure (KeyError): + """A purported ID does not have the appropriate syntax. + + Parameters + ---------- + id : str + The purported ID. + msg : str, optional + A helpful message explaining what went wrong. + """ def __init__(self, id, msg=None): KeyError.__init__(self, id) self.id = id @@ -97,6 +178,12 @@ class InvalidIDStructure (KeyError): return self.msg def _assemble(args, check_length=False): + """Join a bunch of level UUIDs into a single ID. + + See Also + -------- + _split : inverse + """ args = list(args) for i,arg in enumerate(args): if arg == None: @@ -104,22 +191,47 @@ def _assemble(args, check_length=False): 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)) + if len(args) > len(HIERARCHY): + raise InvalidIDStructure( + id, '%d > %d levels in "%s"' % (len(args), len(HIERARCHY), id)) return id def _split(id, check_length=False): + """Split an ID into a list of level UUIDs. + + See Also + -------- + _assemble : inverse + """ 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)) + if len(args) > len(HIERARCHY): + raise InvalidIDStructure( + id, '%d > %d levels in "%s"' % (len(args), len(HIERARCHY), id)) return args def _truncate(uuid, other_uuids, min_length=3): + """Truncate a UUID to the shortest length >= `min_length` such that it + is *not* a truncated form of a UUID in `other_uuids`. + + Parameters + ---------- + uuid : str + The UUID to truncate. + other_uuids : list of str + The other UUIDs which the truncation *might* (but doesn't) refer + to. + min_length : int + Avoid rapidly outdated truncations, even if they are unique now. + + See Also + -------- + _expand : inverse + """ chars = min_length for id in other_uuids: if id == uuid: @@ -129,6 +241,29 @@ def _truncate(uuid, other_uuids, min_length=3): return uuid[:chars] def _expand(truncated_id, common, other_ids): + """Expand a truncated UUID. + + Parameters + ---------- + truncated_id : str + The ID to expand. + common : str + The common portion `truncated_id` shares with the UUIDs in + `other_ids`. Not used by ``_expand``, but passed on to the + matching exceptions if they occur. + other_uuids : list of str + The other UUIDs which the truncation *might* (but doesn't) refer + to. + + Raises + ------ + NoIDMatches + MultipleIDMatches + + See Also + -------- + _expand : inverse + """ other_ids = list(other_ids) if len(other_ids) == 0: raise NoIDMatches(truncated_id, other_ids) @@ -151,7 +286,18 @@ def _expand(truncated_id, common, other_ids): class ID (object): - """ + """Store an object ID and produce various representations. + + Parameters + ---------- + object : :class:`~libbe.bugdir.BugDir` or :class:`~libbe.bug.Bug` or :class:`~libbe.comment.Comment` + The object that the ID applies to. + type : 'bugdir' or 'bug' or 'comment' + The type of the object. + + Notes + ----- + IDs have several formats specialized for different uses. In storage, all objects are represented by their uuid alone, @@ -166,41 +312,39 @@ class ID (object): them while retaining local uniqueness (with regards to the other objects currently in storage). We also prepend truncated parent ids for two reasons: - (1) so that a user can locate the repository containing the - referenced object. It would be hard to find bug 'XYZ' if - that's all you knew. Much easier with 'ABC/XYZ', where ABC - is the bugdir. Each project can publish a list of bugdir-id - - to - location mappings, e.g. + + 1. So that a user can locate the repository containing the + referenced object. It would be hard to find bug ``XYZ`` if + that's all you knew. Much easier with ``ABC/XYZ``, where + ``ABC`` is the bugdir. Each project can publish a list of + bugdir-id-to-location mappings, e.g.:: + ABC...(full uuid)...DEF https://server.com/projectX/be/ - which is easier than publishing all-object-ids-to-location - mappings. - (2) because it's easier to generate and parse truncated ids if - you don't have to fetch all the ids in the storage - repository, but can restrict yourself to a specific branch. - You can generate ids of this sort with the .user() method, - although in order to preform the truncation, your object (and its - parents must define a .sibling_uuids() method. + which is easier than publishing all-object-ids-to-location + mappings. + + 2. Because it's easier to generate and parse truncated ids if you + don't have to fetch all the ids in the storage repository but + can restrict yourself to a specific branch. + + You can generate ids of this sort with the :meth:`user` method, + although in order to preform the truncation, your object (and its + parents must define a `sibling_uuids` method. While users can use the convenient short user ids in the short term, the truncation will inevitably lead to name collision. To avoid that, we provide a non-truncated form of the short user ids - via the .long_user() method. These long user ids should be + via the :meth:`long_user` method. These long user ids should be converted to short user ids by intelligent user interfaces. - Related tools: - * get uuids back out of the user ids: - parse_user() - * convert a single short user id to a long user id: - short_to_long_user() - * convert a single long user id to a short user id: - long_to_short_user() - * scan text for user ids & convert to long user ids: - short_to_long_text() - * scan text for long user ids & convert to short user ids: - long_to_short_text() - - Supported types: 'bugdir', 'bug', 'comment' + See Also + -------- + parse_user : get uuids back out of the user ids. + short_to_long_user : convert a single short user id to a long user id. + long_to_short_user : convert a single long user id to a short user id. + short_to_long_text : scan text for user ids & convert to long user ids. + long_to_short_text : scan text for long user ids & convert to short user ids. """ def __init__(self, object, type): self._object = object @@ -236,9 +380,17 @@ class ID (object): return _assemble(ids, check_length=True) def child_uuids(child_storage_ids): - """ - Extract uuid children from other children generated by the - ID.storage() method. + """Extract uuid children from other children generated by + :meth:`ID.storage`. + + This is useful for separating data belonging to a particular + object directly from entries for its child objects. Since the + :class:`~libbe.storage.base.Storage` backend doesn't distinguish + between the two. + + Examples + -------- + >>> list(child_uuids(['abc123/values', '123abc', '123def'])) ['123abc', '123def'] """ @@ -248,6 +400,15 @@ def child_uuids(child_storage_ids): yield fields[0] def long_to_short_user(bugdirs, id): + """Convert a long user ID to a short user ID (see :class:`ID`). + The list of bugdirs allows uniqueness-maintaining truncation of + the bugdir portion of the ID. + + See Also + -------- + short_to_long_user : inverse + long_to_short_text : conversion on a block of text + """ ids = _split(id, check_length=True) matching_bugdirs = [bd for bd in bugdirs if bd.uuid == ids[0]] if len(matching_bugdirs) == 0: @@ -267,6 +428,15 @@ def long_to_short_user(bugdirs, id): return _assemble(ids) def short_to_long_user(bugdirs, id): + """Convert a short user ID to a long user ID (see :class:`ID`). The + list of bugdirs allows uniqueness-checking during expansion of the + bugdir portion of the ID. + + See Also + -------- + long_to_short_user : inverse + short_to_long_text : conversion on a block of text + """ ids = _split(id, check_length=True) ids[0] = _expand(ids[0], common=None, other_ids=[bd.uuid for bd in bugdirs]) @@ -284,8 +454,19 @@ def short_to_long_user(bugdirs, id): REGEXP = '#([-a-f0-9]*)(/[-a-g0-9]*)?(/[-a-g0-9]*)?#' +"""Regular expression for matching IDs (both short and long) in text. +""" class IDreplacer (object): + """Helper class for ID replacement in text. + + Reassembles the match elements from :data:`REGEXP` matching + into the original ID, for easier replacement. + + See Also + -------- + short_to_long_text, long_to_short_text + """ def __init__(self, bugdirs, replace_fn, wrap=True): self.bugdirs = bugdirs self.replace_fn = replace_fn @@ -302,13 +483,36 @@ class IDreplacer (object): return replacement def short_to_long_text(bugdirs, text): + """Convert short user IDs to long user IDs in text (see :class:`ID`). + The list of bugdirs allows uniqueness-checking during expansion of + the bugdir portion of the ID. + + See Also + -------- + short_to_long_user : conversion on a single ID + long_to_short_text : inverse + """ return re.sub(REGEXP, IDreplacer(bugdirs, short_to_long_user), text) def long_to_short_text(bugdirs, text): + """Convert long user IDs to short user IDs in text (see :class:`ID`). + The list of bugdirs allows uniqueness-maintaining truncation of + the bugdir portion of the ID. + + See Also + -------- + long_to_short_user : conversion on a single ID + short_to_long_text : inverse + """ return re.sub(REGEXP, IDreplacer(bugdirs, long_to_short_user), text) def residual(base, fragment): - """ + """Split the short ID `fragment` into a portion corresponding + to `base`, and a portion inside `base`. + + Examples + -------- + >>> residual('ABC/DEF/', '//GHI') ('//', 'GHI') >>> residual('ABC/DEF/', '/D/GHI') @@ -326,7 +530,15 @@ def residual(base, fragment): return ('/'.join(root_ids), '/'.join(residual_ids)) def _parse_user(id): - """ + """Parse a user ID (see :class:`ID`), returning a dict of parsed + information. + + The returned dict will contain a value for "type" (from + :data:`HIERARCHY`) and values for the levels that are defined. + + Examples + -------- + >>> _parse_user('ABC/DEF/GHI') == \\ ... {'bugdir':'ABC', 'bug':'DEF', 'comment':'GHI', 'type':'comment'} True @@ -361,6 +573,17 @@ def _parse_user(id): return ret def parse_user(bugdir, id): + """Parse a user ID (see :class:`ID`), returning a dict of parsed + information. + + The returned dict will contain a value for "type" (from + :data:`HIERARCHY`) and values for the levels that are defined. + + Notes + ----- + This function tries to expand IDs before parsing, so it can handle + both short and long IDs successfully. + """ long_id = short_to_long_user([bugdir], id) return _parse_user(long_id) -- cgit