From fe1d6dff22f73671928eaadbf4c83bdaa21d9bb9 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 01:49:06 -0500 Subject: Added libbe.storage.base and test suite. --- libbe/storage/base.py | 687 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 687 insertions(+) create mode 100644 libbe/storage/base.py (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py new file mode 100644 index 0000000..002cc0f --- /dev/null +++ b/libbe/storage/base.py @@ -0,0 +1,687 @@ +# Copyright + +""" +Abstract bug repository data storage to easily support multiple backends. +""" + +import copy +import os +import pickle + +from libbe.error import NotSupported +from libbe.util.tree import Tree +from libbe.util import InvalidObject +from libbe import TESTING + +if TESTING == True: + import doctest + import os.path + import sys + import unittest + + from libbe.util.utility import Dir + +class ConnectionError (Exception): + pass + +class InvalidID (KeyError): + pass + +class InvalidRevision (KeyError): + pass + +class EmptyCommit(Exception): + def __init__(self): + Exception.__init__(self, 'No changes to commit') + +class Entry (Tree): + def __init__(self, id, value=None, parent=None, children=None): + if children == None: + Tree.__init__(self) + else: + Tree.__init__(self, children) + self.id = id + self.value = value + self.parent = parent + if self.parent != None: + parent.append(self) + + def __str__(self): + return '' % (self.id, self.value) + + def __repr__(self): + return str(self) + + def __cmp__(self, other, local=False): + if other == None: + return cmp(1, None) + if cmp(self.id, other.id) != 0: + return cmp(self.id, other.id) + if cmp(self.value, other.value) != 0: + return cmp(self.value, other.value) + if local == False: + if self.parent == None: + if cmp(self.parent, other.parent) != 0: + return cmp(self.parent, other.parent) + elif self.parent.__cmp__(other.parent, local=True) != 0: + return self.parent.__cmp__(other.parent, local=True) + for sc,oc in zip(self, other): + if sc.__cmp__(oc, local=True) != 0: + return sc.__cmp__(oc, local=True) + return 0 + + def _objects_to_ids(self): + if self.parent != None: + self.parent = self.parent.id + for i,c in enumerate(self): + self[i] = c.id + return self + + def _ids_to_objects(self, dict): + if self.parent != None: + self.parent = dict[self.parent] + for i,c in enumerate(self): + self[i] = dict[c] + return self + +class Storage (object): + """ + This class declares all the methods required by a Storage + interface. This implementation just keeps the data in a + dictionary and uses pickle for persistent storage. + """ + name = 'Storage' + + def __init__(self, repo, options=None): + self.repo = repo + self.options = options + self.read_only = False + self.versioned = False + self.can_init = True + + def __str__(self): + return '<%s %s>' % (self.__class__.__name__, id(self)) + + def __repr__(self): + return str(self) + + def version(self): + """Return a version string for this backend.""" + return '0' + + def init(self): + """Create a new storage repository.""" + if self.can_init == False: + raise NotSupported('init', + 'Cannot initialize this repository format.') + if self.read_only == True: + raise NotSupported('init', 'Cannot initialize read only storage.') + return self._init() + + def _init(self): + f = open(self.repo, 'wb') + root = Entry(id='__ROOT__') + d = {root.id:root} + pickle.dump(dict((k,v._objects_to_ids()) for k,v in d.items()), f, -1) + f.close() + + def destroy(self): + """Remove the storage repository.""" + if self.read_only == True: + raise NotSupported('destroy', 'Cannot destroy read only storage.') + return self._destroy() + + def _destroy(self): + os.remove(self.repo) + + def connect(self): + """Open a connection to the repository.""" + try: + f = open(self.repo, 'rb') + except IOError: + raise ConnectionError(self) + d = pickle.load(f) + self._data = dict((k,v._ids_to_objects(d)) for k,v in d.items()) + f.close() + + def disconnect(self): + """Close the connection to the repository.""" + if self.read_only == True: + return + f = open(self.repo, 'wb') + pickle.dump(dict((k,v._objects_to_ids()) + for k,v in self._data.items()), f, -1) + f.close() + self._data = None + + def add(self, *args, **kwargs): + """Add an entry""" + if self.read_only == True: + raise NotSupported('add', 'Cannot add entry to read only storage.') + self._add(*args, **kwargs) + + def _add(self, id, parent=None): + if parent == None: + parent = '__ROOT__' + p = self._data[parent] + self._data[id] = Entry(id, parent=p) + + def remove(self, *args, **kwargs): + """Remove an entry.""" + if self.read_only == True: + raise NotSupported('remove', + 'Cannot remove entry from read only storage.') + self._remove(*args, **kwargs) + + def _remove(self, id): + e = self._data.pop(id) + e.parent.remove(e) + + def recursive_remove(self, *args, **kwargs): + """Remove an entry and all its decendents.""" + if self.read_only == True: + raise NotSupported('recursive_remove', + 'Cannot remove entries from read only storage.') + self._recursive_remove(*args, **kwargs) + + def _recursive_remove(self, id): + for entry in self._data[id].traverse(): + self._remove(entry.id) + + def children(self, id=None, revision=None): + """Return a list of specified entry's children's ids.""" + if id == None: + id = '__ROOT__' + return [c.id for c in self._data[id] if not c.id.startswith('__')] + + def get(self, id, default=InvalidObject, revision=None): + """ + Get contents of and entry as they were in a given revision. + revision==None specifies the current revision. + + If there is no id, return default, unless default is not + given, in which case raise InvalidID. + """ + if id in self._data: + return self._data[id].value + elif default == InvalidObject: + raise InvalidID(id) + return default + + def set(self, *args, **kwargs): + """ + Set the entry contents. + """ + if self.read_only == True: + raise NotSupported('set', 'Cannot set entry in read only storage.') + self._set(*args, **kwargs) + + def _set(self, id, value): + if id not in self._data: + raise InvalidID(id) + self._data[id].value = value + +class VersionedStorage (Storage): + """ + This class declares all the methods required by a Storage + interface that supports versioning. This implementation just + keeps the data in a list and uses pickle for persistent + storage. + """ + name = 'VersionedStorage' + + def __init__(self, *args, **kwargs): + Storage.__init__(self, *args, **kwargs) + self.versioned = True + + def _init(self): + f = open(self.repo, 'wb') + root = Entry(id='__ROOT__') + summary = Entry(id='__COMMIT__SUMMARY__', value='Initial commit') + body = Entry(id='__COMMIT__BODY__') + initial_commit = {root.id:root, summary.id:summary, body.id:body} + d = dict((k,v._objects_to_ids()) for k,v in initial_commit.items()) + pickle.dump([d, copy.deepcopy(d)], f, -1) # [inital tree, working tree] + f.close() + + def connect(self): + """Open a connection to the repository.""" + try: + f = open(self.repo, 'rb') + except IOError: + raise ConnectionError(self) + d = pickle.load(f) + self._data = [dict((k,v._ids_to_objects(t)) for k,v in t.items()) + for t in d] + f.close() + + def disconnect(self): + """Close the connection to the repository.""" + if self.read_only == True: + return + f = open(self.repo, 'wb') + pickle.dump([dict((k,v._objects_to_ids()) + for k,v in t.items()) for t in self._data], f, -1) + f.close() + self._data = None + + def _add(self, id, parent=None): + if parent == None: + parent = '__ROOT__' + p = self._data[-1][parent] + self._data[-1][id] = Entry(id, parent=p) + + def _remove(self, id): + e = self._data[-1].pop(id) + e.parent.remove(e) + + def _recursive_remove(self, id): + for entry in self._data[-1][id].traverse(): + self._remove(entry.id) + + def children(self, id=None, revision=None): + """Return a list of specified entry's children's ids.""" + if id == None: + id = '__ROOT__' + if revision == None: + revision = -1 + return [c.id for c in self._data[revision][id] + if not c.id.startswith('__')] + + def get(self, id, default=InvalidObject, revision=None): + """ + Get contents of and entry as they were in a given revision. + revision==None specifies the current revision. + + If there is no id, return default, unless default is not + given, in which case raise InvalidID. + """ + if revision == None: + revision = -1 + if id in self._data[revision]: + return self._data[revision][id].value + elif default == InvalidObject: + raise InvalidID(id) + return default + + def _set(self, id, value): + if id not in self._data[-1]: + raise InvalidID(id) + self._data[-1][id].value = value + + def commit(self, *args, **kwargs): + """ + Commit the current repository, with a commit message string + summary and body. Return the name of the new revision. + + If allow_empty == False (the default), raise EmptyCommit if + there are no changes to commit. + """ + if self.read_only == True: + raise NotSupported('commit', 'Cannot commit to read only storage.') + return self._commit(*args, **kwargs) + + def _commit(self, summary, body=None, allow_empty=False): + if self._data[-1] == self._data[-2] and allow_empty == False: + raise EmptyCommit + self._data[-1]["__COMMIT__SUMMARY__"].value = summary + self._data[-1]["__COMMIT__BODY__"].value = body + rev = len(self._data)-1 + self._data.append(copy.deepcopy(self._data[-1])) + return rev + + def revision_id(self, index=None): + """ + Return the name of the th revision. The choice of + which branch to follow when crossing branches/merges is not + defined. Revision indices start at 1; ID 0 is the blank + repository. + + Return None if index==None. + + If the specified revision does not exist, raise InvalidRevision. + """ + if index == None: + return None + try: + if int(index) != index: + raise InvalidRevision(index) + except ValueError: + raise InvalidRevision(index) + L = len(self._data) - 1 # -1 b/c of initial commit + if index >= -L and index <= L: + return index % L + raise InvalidRevision(i) + +if TESTING == True: + class StorageTestCase (unittest.TestCase): + """Test cases for base Storage class.""" + + Class = Storage + + def __init__(self, *args, **kwargs): + super(StorageTestCase, self).__init__(*args, **kwargs) + self.dirname = None + + def setUp(self): + """Set up test fixtures for Storage test case.""" + super(StorageTestCase, self).setUp() + self.dir = Dir() + self.dirname = self.dir.path + self.s = self.Class(repo=os.path.join(self.dirname, 'repo.pkl')) + self.assert_failed_connect() + self.s.init() + self.s.connect() + + def tearDown(self): + super(StorageTestCase, self).tearDown() + self.s.disconnect() + self.s.destroy() + self.assert_failed_connect() + + def assert_failed_connect(self): + try: + self.s.connect() + self.fail( + "Connected to %(name)s repository before initialising" + % vars(self.Class)) + except ConnectionError: + pass + + class Storage_init_TestCase (StorageTestCase): + """Test cases for Storage.init method.""" + + def test_connect_should_succeed_after_init(self): + """Should connect after initialization.""" + self.s.connect() + + class Storage_add_remove_TestCase (StorageTestCase): + """Test cases for Storage.add, .remove, and .recursive_remove methods.""" + + def test_initially_empty(self): + """New repository should be empty.""" + self.failUnless(len(self.s.children()) == 0, self.s.children()) + + def test_add_rooted(self): + """ + Adding entries should increase the number of children (rooted). + """ + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1]) + s = sorted(self.s.children()) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + + def test_add_nonrooted(self): + """ + Adding entries should increase the number of children (nonrooted). + """ + self.s.add('parent') + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1], 'parent') + s = sorted(self.s.children('parent')) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + s = self.s.children() + self.failUnless(s == ['parent'], s) + + def test_remove_rooted(self): + """ + Removing entries should decrease the number of children (rooted). + """ + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1]) + for i in range(10): + self.s.remove(ids.pop()) + s = sorted(self.s.children()) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + + def test_remove_nonrooted(self): + """ + Removing entries should decrease the number of children (nonrooted). + """ + self.s.add('parent') + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1], 'parent') + for i in range(10): + self.s.remove(ids.pop()) + s = sorted(self.s.children('parent')) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + s = self.s.children() + self.failUnless(s == ['parent'], s) + + def test_recursive_remove(self): + """ + Recursive remove should empty the tree. + """ + self.s.add('parent') + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1], 'parent') + for j in range(10): # add some grandkids + self.s.add(str(20*i+j), ids[-i]) + self.s.recursive_remove('parent') + s = sorted(self.s.children()) + self.failUnless(s == [], s) + + class Storage_get_set_TestCase (StorageTestCase): + """Test cases for Storage.get and .set methods.""" + + id = 'unlikely id' + val = 'unlikely value' + + def test_get_default(self): + """ + Get should return specified default if id not in Storage. + """ + ret = self.s.get(self.id, default=self.val) + self.failUnless(ret == self.val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], ret, self.val)) + + def test_get_default_exception(self): + """ + Get should raise exception if id not in Storage and no default. + """ + try: + ret = self.s.get(self.id) + self.fail( + "%s.get() returned %s instead of raising InvalidID" + % (vars(self.Class)['name'], ret)) + except InvalidID: + pass + + def test_get_initial_value(self): + """ + Data value should be None before any value has been set. + """ + self.s.add(self.id) + ret = self.s.get(self.id) + self.failUnless(ret == None, + "%s.get() returned %s not None" + % (vars(self.Class)['name'], ret)) + + def test_set_exception(self): + """ + Set should raise exception if id not in Storage. + """ + try: + self.s.set(self.id, self.val) + self.fail( + "%(name)s.set() did not raise InvalidID" + % vars(self.Class)) + except InvalidID: + pass + + def test_set(self): + """ + Set should define the value returned by get. + """ + self.s.add(self.id) + self.s.set(self.id, self.val) + ret = self.s.get(self.id) + self.failUnless(ret == self.val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], ret, self.val)) + + class Storage_persistence_TestCase (StorageTestCase): + """Test cases for Storage.disconnect and .connect methods.""" + + id = 'unlikely id' + val = 'unlikely value' + + def test_get_set_persistence(self): + """ + Set should define the value returned by get after reconnect. + """ + self.s.add(self.id) + self.s.set(self.id, self.val) + self.s.disconnect() + self.s.connect() + ret = self.s.get(self.id) + self.failUnless(ret == self.val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], ret, self.val)) + + def test_add_nonrooted_persistence(self): + """ + Adding entries should increase the number of children after reconnect. + """ + self.s.add('parent') + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1], 'parent') + self.s.disconnect() + self.s.connect() + s = sorted(self.s.children('parent')) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + s = self.s.children() + self.failUnless(s == ['parent'], s) + + class VersionedStorageTestCase (StorageTestCase): + """Test cases for base VersionedStorage class.""" + + Class = VersionedStorage + + class VersionedStorage_commit_TestCase (VersionedStorageTestCase): + """Test cases for VersionedStorage methods.""" + + id = 'I' #unlikely id' + val = 'X' + commit_msg = 'C' #ommitting something interesting' + commit_body = 'B' #ome\nlonger\ndescription\n' + + def test_revision_id_exception(self): + """ + Invalid revision id should raise InvalidRevision. + """ + try: + rev = self.s.revision_id('highly unlikely revision id') + self.fail( + "%s.revision_id() didn't raise InvalidRevision, returned %s." + % (vars(self.Class)['name'], rev)) + except InvalidRevision: + pass + + def test_empty_commit_raises_exception(self): + """ + Empty commit should raise exception. + """ + try: + self.s.commit(self.commit_msg, self.commit_body) + self.fail( + "Empty %(name)s.commit() didn't raise EmptyCommit." + % vars(self.Class)) + except EmptyCommit: + pass + + def test_empty_commit_allowed(self): + """ + Empty commit should _not_ raise exception if allow_empty=True. + """ + self.s.commit(self.commit_msg, self.commit_body, + allow_empty=True) + + def test_commit_revision_ids(self): + """ + Commit / revision_id should agree on revision ids. + """ + revs = [] + for s in range(10): + revs.append(self.s.commit(self.commit_msg, + self.commit_body, + allow_empty=True)) + for i in range(10): + rev = self.s.revision_id(i+1) + self.failUnless(rev == revs[i], + "%s.revision_id(%d) returned %s not %s" + % (vars(self.Class)['name'], i+1, rev, revs[i])) + for i in range(-1, -9, -1): + rev = self.s.revision_id(i) + self.failUnless(rev == revs[i], + "%s.revision_id(%d) returned %s not %s" + % (vars(self.Class)['name'], i, rev, revs[i])) + + def test_get_previous_version(self): + """ + Get should be able to return the previous version. + """ + def val(i): + return '%s:%d' % (self.val, i+1) + self.s.add(self.id) + revs = [] + for i in range(10): + self.s.set(self.id, val(i)) + revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), + self.commit_body)) + for i in range(10): + ret = self.s.get(self.id, revision=revs[i]) + self.failUnless(ret == val(i), + "%s.get() returned %s not %s for revision %d" + % (vars(self.Class)['name'], ret, val(i), revs[i])) + + def make_storage_testcase_subclasses(storage_class, namespace): + """Make StorageTestCase subclasses for storage_class in namespace.""" + storage_testcase_classes = [ + c for c in ( + ob for ob in globals().values() if isinstance(ob, type)) + if issubclass(c, StorageTestCase) \ + and not issubclass(c, VersionedStorageTestCase)] + + for base_class in storage_testcase_classes: + testcase_class_name = storage_class.__name__ + base_class.__name__ + testcase_class_bases = (base_class,) + testcase_class_dict = dict(base_class.__dict__) + testcase_class_dict['Class'] = storage_class + testcase_class = type( + testcase_class_name, testcase_class_bases, testcase_class_dict) + setattr(namespace, testcase_class_name, testcase_class) + + def make_versioned_storage_testcase_subclasses(storage_class, namespace): + """Make VersionedStorageTestCase subclasses for storage_class in namespace.""" + storage_testcase_classes = [ + c for c in ( + ob for ob in globals().values() if isinstance(ob, type)) + if issubclass(c, StorageTestCase)] + + for base_class in storage_testcase_classes: + testcase_class_name = storage_class.__name__ + base_class.__name__ + testcase_class_bases = (base_class,) + testcase_class_dict = dict(base_class.__dict__) + testcase_class_dict['Class'] = storage_class + testcase_class = type( + testcase_class_name, testcase_class_bases, testcase_class_dict) + setattr(namespace, testcase_class_name, testcase_class) + + make_storage_testcase_subclasses(VersionedStorage, sys.modules[__name__]) + + unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) + suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) -- cgit From d64b6336f75078445f2b730b31598817ac1cdb7a Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 8 Dec 2009 03:52:37 -0500 Subject: Extended libbe.storage.base for separate read/write control. Rather than just having .read_only to set write permissions and assuming that read was always legal. We also added user and backend control of both readable and writeable: do you want to read/write? and can you read/write? Specialized NotSupported into NotWriteable and NotReadable. Added automatic unicode encoding on .set(), and decode option on .get(). --- libbe/storage/base.py | 130 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 94 insertions(+), 36 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 002cc0f..3526462 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -7,6 +7,7 @@ Abstract bug repository data storage to easily support multiple backends. import copy import os import pickle +import types from libbe.error import NotSupported from libbe.util.tree import Tree @@ -30,6 +31,14 @@ class InvalidID (KeyError): class InvalidRevision (KeyError): pass +class NotWriteable (NotSupported): + def __init__(self, msg): + NotSupported.__init__('write', msg) + +class NotReadable (NotSupported): + def __init__(self, msg): + NotSupported.__init__('read', msg) + class EmptyCommit(Exception): def __init__(self): Exception.__init__(self, 'No changes to commit') @@ -92,10 +101,14 @@ class Storage (object): """ name = 'Storage' - def __init__(self, repo, options=None): + def __init__(self, repo, encoding='utf-8', options=None): self.repo = repo + self.encoding = encoding self.options = options - self.read_only = False + self.readable = True # soft limit (user choice) + self._readable = True # hard limit (backend choice) + self.writeable = True # soft limit (user choice) + self._writeable = True # hard limit (backend choice) self.versioned = False self.can_init = True @@ -109,13 +122,19 @@ class Storage (object): """Return a version string for this backend.""" return '0' + def is_readable(self): + return self.readable and self._readable + + def is_writeable(self): + return self.writeable and self._writeable + def init(self): """Create a new storage repository.""" if self.can_init == False: raise NotSupported('init', 'Cannot initialize this repository format.') - if self.read_only == True: - raise NotSupported('init', 'Cannot initialize read only storage.') + if self.is_writeable() == False: + raise NotWriteable('Cannot initialize unwriteable storage.') return self._init() def _init(self): @@ -127,8 +146,8 @@ class Storage (object): def destroy(self): """Remove the storage repository.""" - if self.read_only == True: - raise NotSupported('destroy', 'Cannot destroy read only storage.') + if self.is_writeable() == False: + raise NotWriteable('Cannot destroy unwriteable storage.') return self._destroy() def _destroy(self): @@ -136,6 +155,11 @@ class Storage (object): def connect(self): """Open a connection to the repository.""" + if self.is_readable() == False: + raise NotReadable('Cannot connect to unreadable storage.') + self._connect() + + def _connect(self): try: f = open(self.repo, 'rb') except IOError: @@ -146,7 +170,7 @@ class Storage (object): def disconnect(self): """Close the connection to the repository.""" - if self.read_only == True: + if self.is_writeable() == False: return f = open(self.repo, 'wb') pickle.dump(dict((k,v._objects_to_ids()) @@ -156,8 +180,8 @@ class Storage (object): def add(self, *args, **kwargs): """Add an entry""" - if self.read_only == True: - raise NotSupported('add', 'Cannot add entry to read only storage.') + if self.is_writeable() == False: + raise NotWriteable('Cannot add entry to unwriteable storage.') self._add(*args, **kwargs) def _add(self, id, parent=None): @@ -168,9 +192,9 @@ class Storage (object): def remove(self, *args, **kwargs): """Remove an entry.""" - if self.read_only == True: - raise NotSupported('remove', - 'Cannot remove entry from read only storage.') + if self.is_writeable() == False: + raise NotSupported('write', + 'Cannot remove entry from unwriteable storage.') self._remove(*args, **kwargs) def _remove(self, id): @@ -179,22 +203,27 @@ class Storage (object): def recursive_remove(self, *args, **kwargs): """Remove an entry and all its decendents.""" - if self.read_only == True: - raise NotSupported('recursive_remove', - 'Cannot remove entries from read only storage.') + if self.is_writeable() == False: + raise NotSupported('write', + 'Cannot remove entries from unwriteable storage.') self._recursive_remove(*args, **kwargs) def _recursive_remove(self, id): for entry in self._data[id].traverse(): self._remove(entry.id) - def children(self, id=None, revision=None): + def children(self, *args, **kwargs): """Return a list of specified entry's children's ids.""" + if self.is_readable() == False: + raise NotReadable('Cannot list children with unreadable storage.') + return self._children(*args, **kwargs) + + def _children(self, id=None, revision=None): if id == None: id = '__ROOT__' return [c.id for c in self._data[id] if not c.id.startswith('__')] - def get(self, id, default=InvalidObject, revision=None): + def get(self, *args, **kwargs): """ Get contents of and entry as they were in a given revision. revision==None specifies the current revision. @@ -202,19 +231,33 @@ class Storage (object): If there is no id, return default, unless default is not given, in which case raise InvalidID. """ + if self.is_readable() == False: + raise NotReadable('Cannot get entry with unreadable storage.') + if 'decode' in kwargs: + decode = kwargs.pop('decode') + else: + decode = False + value = self._get(*args, **kwargs) + if decode == True: + return unicode(value, self.encoding) + return value + + def _get(self, id, default=InvalidObject, revision=None): if id in self._data: return self._data[id].value elif default == InvalidObject: raise InvalidID(id) return default - def set(self, *args, **kwargs): + def set(self, id, value, *args, **kwargs): """ Set the entry contents. """ - if self.read_only == True: - raise NotSupported('set', 'Cannot set entry in read only storage.') - self._set(*args, **kwargs) + if self.is_writeable() == False: + raise NotWriteable('Cannot set entry in unwriteable storage.') + if type(value) == types.UnicodeType: + value = value.encode(self.encoding) + self._set(id, value, *args, **kwargs) def _set(self, id, value): if id not in self._data: @@ -244,8 +287,7 @@ class VersionedStorage (Storage): pickle.dump([d, copy.deepcopy(d)], f, -1) # [inital tree, working tree] f.close() - def connect(self): - """Open a connection to the repository.""" + def _connect(self): try: f = open(self.repo, 'rb') except IOError: @@ -257,7 +299,7 @@ class VersionedStorage (Storage): def disconnect(self): """Close the connection to the repository.""" - if self.read_only == True: + if self.is_writeable() == False: return f = open(self.repo, 'wb') pickle.dump([dict((k,v._objects_to_ids()) @@ -279,8 +321,7 @@ class VersionedStorage (Storage): for entry in self._data[-1][id].traverse(): self._remove(entry.id) - def children(self, id=None, revision=None): - """Return a list of specified entry's children's ids.""" + def _children(self, id=None, revision=None): if id == None: id = '__ROOT__' if revision == None: @@ -288,14 +329,7 @@ class VersionedStorage (Storage): return [c.id for c in self._data[revision][id] if not c.id.startswith('__')] - def get(self, id, default=InvalidObject, revision=None): - """ - Get contents of and entry as they were in a given revision. - revision==None specifies the current revision. - - If there is no id, return default, unless default is not - given, in which case raise InvalidID. - """ + def _get(self, id, default=InvalidObject, revision=None): if revision == None: revision = -1 if id in self._data[revision]: @@ -317,8 +351,8 @@ class VersionedStorage (Storage): If allow_empty == False (the default), raise EmptyCommit if there are no changes to commit. """ - if self.read_only == True: - raise NotSupported('commit', 'Cannot commit to read only storage.') + if self.is_writeable() == False: + raise NotWriteable('Cannot commit to unwriteable storage.') return self._commit(*args, **kwargs) def _commit(self, summary, body=None, allow_empty=False): @@ -531,6 +565,30 @@ if TESTING == True: "%s.get() returned %s not %s" % (vars(self.Class)['name'], ret, self.val)) + def test_unicode_set(self): + """ + Set should define the value returned by get. + """ + val = u'Fran\xe7ois' + self.s.add(self.id) + self.s.set(self.id, val) + ret = self.s.get(self.id, decode=True) + self.failUnless(type(ret) == types.UnicodeType, + "%s.get() returned %s not UnicodeType" + % (vars(self.Class)['name'], type(ret))) + self.failUnless(ret == val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], ret, self.val)) + ret = self.s.get(self.id) + self.failUnless(type(ret) == types.StringType, + "%s.get() returned %s not StringType" + % (vars(self.Class)['name'], type(ret))) + s = unicode(ret, self.s.encoding) + self.failUnless(s == val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], s, self.val)) + + class Storage_persistence_TestCase (StorageTestCase): """Test cases for Storage.disconnect and .connect methods.""" -- 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/storage/base.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 3526462..eb2b94c 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -33,11 +33,11 @@ class InvalidRevision (KeyError): class NotWriteable (NotSupported): def __init__(self, msg): - NotSupported.__init__('write', msg) + NotSupported.__init__(self, 'write', msg) class NotReadable (NotSupported): def __init__(self, msg): - NotSupported.__init__('read', msg) + NotSupported.__init__(self, 'read', msg) class EmptyCommit(Exception): def __init__(self): @@ -182,7 +182,11 @@ class Storage (object): """Add an entry""" if self.is_writeable() == False: raise NotWriteable('Cannot add entry to unwriteable storage.') - self._add(*args, **kwargs) + try: # Maybe we've already added that id? + self.get(id) + pass # yup, no need to add another + except InvalidID: + self._add(*args, **kwargs) def _add(self, id, parent=None): if parent == None: @@ -436,6 +440,15 @@ if TESTING == True: """New repository should be empty.""" self.failUnless(len(self.s.children()) == 0, self.s.children()) + def test_add_rooted(self): + """ + Adding entries with the same ID should not increase the number of children. + """ + for i in range(10): + self.s.add('some id') + s = sorted(self.s.children()) + self.failUnless(s == ['some id'], s) + def test_add_rooted(self): """ Adding entries should increase the number of children (rooted). -- 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/storage/base.py | 139 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 106 insertions(+), 33 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index eb2b94c..8419796 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -31,6 +31,12 @@ class InvalidID (KeyError): class InvalidRevision (KeyError): pass +class InvalidDirectory (Exception): + pass + +class DirectoryNotEmpty (InvalidDirectory): + pass + class NotWriteable (NotSupported): def __init__(self, msg): NotSupported.__init__(self, 'write', msg) @@ -44,7 +50,8 @@ class EmptyCommit(Exception): Exception.__init__(self, 'No changes to commit') class Entry (Tree): - def __init__(self, id, value=None, parent=None, children=None): + def __init__(self, id, value=None, parent=None, directory=False, + children=None): if children == None: Tree.__init__(self) else: @@ -53,7 +60,11 @@ class Entry (Tree): self.value = value self.parent = parent if self.parent != None: + if self.parent.directory == False: + raise InvalidDirectory( + 'Non-directory %s cannot have children' % self.parent) parent.append(self) + self.directory = directory def __str__(self): return '' % (self.id, self.value) @@ -101,7 +112,7 @@ class Storage (object): """ name = 'Storage' - def __init__(self, repo, encoding='utf-8', options=None): + def __init__(self, repo='/', encoding='utf-8', options=None): self.repo = repo self.encoding = encoding self.options = options @@ -113,7 +124,7 @@ class Storage (object): self.can_init = True def __str__(self): - return '<%s %s>' % (self.__class__.__name__, id(self)) + return '<%s %s %s>' % (self.__class__.__name__, id(self), self.repo) def __repr__(self): return str(self) @@ -139,7 +150,7 @@ class Storage (object): def _init(self): f = open(self.repo, 'wb') - root = Entry(id='__ROOT__') + root = Entry(id='__ROOT__', directory=True) d = {root.id:root} pickle.dump(dict((k,v._objects_to_ids()) for k,v in d.items()), f, -1) f.close() @@ -178,7 +189,7 @@ class Storage (object): f.close() self._data = None - def add(self, *args, **kwargs): + def add(self, id, *args, **kwargs): """Add an entry""" if self.is_writeable() == False: raise NotWriteable('Cannot add entry to unwriteable storage.') @@ -186,13 +197,13 @@ class Storage (object): self.get(id) pass # yup, no need to add another except InvalidID: - self._add(*args, **kwargs) + self._add(id, *args, **kwargs) - def _add(self, id, parent=None): + def _add(self, id, parent=None, directory=False): if parent == None: parent = '__ROOT__' p = self._data[parent] - self._data[id] = Entry(id, parent=p) + self._data[id] = Entry(id, parent=p, directory=directory) def remove(self, *args, **kwargs): """Remove an entry.""" @@ -202,6 +213,9 @@ class Storage (object): self._remove(*args, **kwargs) def _remove(self, id): + if self._data[id].directory == True \ + and len(self.children(id)) > 0: + raise DirectoryNotEmpty(id) e = self._data.pop(id) e.parent.remove(e) @@ -213,7 +227,7 @@ class Storage (object): self._recursive_remove(*args, **kwargs) def _recursive_remove(self, id): - for entry in self._data[id].traverse(): + for entry in reversed(list(self._data[id].traverse())): self._remove(entry.id) def children(self, *args, **kwargs): @@ -266,6 +280,9 @@ class Storage (object): def _set(self, id, value): if id not in self._data: raise InvalidID(id) + if self._data[id].directory == True: + raise InvalidDirectory( + 'Directory %s cannot have data' % self.parent) self._data[id].value = value class VersionedStorage (Storage): @@ -283,7 +300,7 @@ class VersionedStorage (Storage): def _init(self): f = open(self.repo, 'wb') - root = Entry(id='__ROOT__') + root = Entry(id='__ROOT__', directory=True) summary = Entry(id='__COMMIT__SUMMARY__', value='Initial commit') body = Entry(id='__COMMIT__BODY__') initial_commit = {root.id:root, summary.id:summary, body.id:body} @@ -311,18 +328,21 @@ class VersionedStorage (Storage): f.close() self._data = None - def _add(self, id, parent=None): + def _add(self, id, parent=None, directory=False): if parent == None: parent = '__ROOT__' p = self._data[-1][parent] - self._data[-1][id] = Entry(id, parent=p) + self._data[-1][id] = Entry(id, parent=p, directory=directory) def _remove(self, id): + if self._data[-1][id].directory == True \ + and len(self.children(id)) > 0: + raise DirectoryNotEmpty(id) e = self._data[-1].pop(id) e.parent.remove(e) def _recursive_remove(self, id): - for entry in self._data[-1][id].traverse(): + for entry in reversed(list(self._data[-1][id].traverse())): self._remove(entry.id) def _children(self, id=None, revision=None): @@ -416,6 +436,7 @@ if TESTING == True: self.s.disconnect() self.s.destroy() self.assert_failed_connect() + self.dir.cleanup() def assert_failed_connect(self): try: @@ -440,12 +461,12 @@ if TESTING == True: """New repository should be empty.""" self.failUnless(len(self.s.children()) == 0, self.s.children()) - def test_add_rooted(self): + def test_add_identical_rooted(self): """ Adding entries with the same ID should not increase the number of children. """ for i in range(10): - self.s.add('some id') + self.s.add('some id', directory=False) s = sorted(self.s.children()) self.failUnless(s == ['some id'], s) @@ -456,7 +477,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1]) + self.s.add(ids[-1], directory=False) s = sorted(self.s.children()) self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) @@ -464,16 +485,50 @@ if TESTING == True: """ Adding entries should increase the number of children (nonrooted). """ - self.s.add('parent') + self.s.add('parent', directory=True) ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent') + self.s.add(ids[-1], 'parent', directory=True) s = sorted(self.s.children('parent')) self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) s = self.s.children() self.failUnless(s == ['parent'], s) - + + def test_children(self): + """ + Non-UUID ids should be returned as such. + """ + self.s.add('parent', directory=True) + ids = [] + for i in range(10): + ids.append('parent/%s' % str(i)) + self.s.add(ids[-1], 'parent', directory=True) + s = sorted(self.s.children('parent')) + self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) + + def test_add_invalid_directory(self): + """ + Should not be able to add children to non-directories. + """ + self.s.add('parent', directory=False) + try: + self.s.add('child', 'parent', directory=False) + self.fail( + '%s.add() succeeded instead of raising InvalidDirectory' + % (vars(self.Class)['name'])) + except InvalidDirectory: + pass + try: + self.s.add('child', 'parent', directory=True) + self.fail( + '%s.add() succeeded instead of raising InvalidDirectory' + % (vars(self.Class)['name'])) + except InvalidDirectory: + pass + self.failUnless(len(self.s.children('parent')) == 0, + self.s.children('parent')) + def test_remove_rooted(self): """ Removing entries should decrease the number of children (rooted). @@ -481,7 +536,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1]) + self.s.add(ids[-1], directory=True) for i in range(10): self.s.remove(ids.pop()) s = sorted(self.s.children()) @@ -491,11 +546,11 @@ if TESTING == True: """ Removing entries should decrease the number of children (nonrooted). """ - self.s.add('parent') + self.s.add('parent', directory=True) ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent') + self.s.add(ids[-1], 'parent', directory=False) for i in range(10): self.s.remove(ids.pop()) s = sorted(self.s.children('parent')) @@ -503,17 +558,35 @@ if TESTING == True: s = self.s.children() self.failUnless(s == ['parent'], s) + def test_remove_directory_not_empty(self): + """ + Removing a non-empty directory entry should raise exception. + """ + self.s.add('parent', directory=True) + ids = [] + for i in range(10): + ids.append(str(i)) + self.s.add(ids[-1], 'parent', directory=True) + self.s.remove(ids.pop()) # empty directory removal succeeds + try: + self.s.remove('parent') # empty directory removal succeeds + self.fail( + "%s.remove() didn't raise DirectoryNotEmpty" + % (vars(self.Class)['name'])) + except DirectoryNotEmpty: + pass + def test_recursive_remove(self): """ Recursive remove should empty the tree. """ - self.s.add('parent') + self.s.add('parent', directory=True) ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent') + self.s.add(ids[-1], 'parent', directory=True) for j in range(10): # add some grandkids - self.s.add(str(20*i+j), ids[-i]) + self.s.add(str(20*(i+1)+j), ids[-1], directory=False) self.s.recursive_remove('parent') s = sorted(self.s.children()) self.failUnless(s == [], s) @@ -549,7 +622,7 @@ if TESTING == True: """ Data value should be None before any value has been set. """ - self.s.add(self.id) + self.s.add(self.id, directory=False) ret = self.s.get(self.id) self.failUnless(ret == None, "%s.get() returned %s not None" @@ -571,7 +644,7 @@ if TESTING == True: """ Set should define the value returned by get. """ - self.s.add(self.id) + self.s.add(self.id, directory=False) self.s.set(self.id, self.val) ret = self.s.get(self.id) self.failUnless(ret == self.val, @@ -583,7 +656,7 @@ if TESTING == True: Set should define the value returned by get. """ val = u'Fran\xe7ois' - self.s.add(self.id) + self.s.add(self.id, directory=False) self.s.set(self.id, val) ret = self.s.get(self.id, decode=True) self.failUnless(type(ret) == types.UnicodeType, @@ -612,7 +685,7 @@ if TESTING == True: """ Set should define the value returned by get after reconnect. """ - self.s.add(self.id) + self.s.add(self.id, directory=False) self.s.set(self.id, self.val) self.s.disconnect() self.s.connect() @@ -625,11 +698,11 @@ if TESTING == True: """ Adding entries should increase the number of children after reconnect. """ - self.s.add('parent') + self.s.add('parent', directory=True) ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent') + self.s.add(ids[-1], 'parent', directory=False) self.s.disconnect() self.s.connect() s = sorted(self.s.children('parent')) @@ -707,7 +780,7 @@ if TESTING == True: """ def val(i): return '%s:%d' % (self.val, i+1) - self.s.add(self.id) + self.s.add(self.id, directory=False) revs = [] for i in range(10): self.s.set(self.id, val(i)) @@ -716,7 +789,7 @@ if TESTING == True: for i in range(10): ret = self.s.get(self.id, revision=revs[i]) self.failUnless(ret == val(i), - "%s.get() returned %s not %s for revision %d" + "%s.get() returned %s not %s for revision %s" % (vars(self.Class)['name'], ret, val(i), revs[i])) def make_storage_testcase_subclasses(storage_class, namespace): -- cgit From 6f23fccbde4ede1121d5baf35c81932b7c8aa7bb Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 13 Dec 2009 07:20:31 -0500 Subject: Converted libbe.storage.vcs.hg to new Storage format. --- libbe/storage/base.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 8419796..56b59ba 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -149,7 +149,7 @@ class Storage (object): return self._init() def _init(self): - f = open(self.repo, 'wb') + f = open(os.path.join(self.repo, 'repo.pkl'), 'wb') root = Entry(id='__ROOT__', directory=True) d = {root.id:root} pickle.dump(dict((k,v._objects_to_ids()) for k,v in d.items()), f, -1) @@ -162,7 +162,7 @@ class Storage (object): return self._destroy() def _destroy(self): - os.remove(self.repo) + os.remove(os.path.join(self.repo, 'repo.pkl')) def connect(self): """Open a connection to the repository.""" @@ -172,7 +172,7 @@ class Storage (object): def _connect(self): try: - f = open(self.repo, 'rb') + f = open(os.path.join(self.repo, 'repo.pkl'), 'rb') except IOError: raise ConnectionError(self) d = pickle.load(f) @@ -183,7 +183,7 @@ class Storage (object): """Close the connection to the repository.""" if self.is_writeable() == False: return - f = open(self.repo, 'wb') + f = open(os.path.join(self.repo, 'repo.pkl'), 'wb') pickle.dump(dict((k,v._objects_to_ids()) for k,v in self._data.items()), f, -1) f.close() @@ -299,7 +299,7 @@ class VersionedStorage (Storage): self.versioned = True def _init(self): - f = open(self.repo, 'wb') + f = open(os.path.join(self.repo, 'repo.pkl'), 'wb') root = Entry(id='__ROOT__', directory=True) summary = Entry(id='__COMMIT__SUMMARY__', value='Initial commit') body = Entry(id='__COMMIT__BODY__') @@ -310,7 +310,7 @@ class VersionedStorage (Storage): def _connect(self): try: - f = open(self.repo, 'rb') + f = open(os.path.join(self.repo, 'repo.pkl'), 'rb') except IOError: raise ConnectionError(self) d = pickle.load(f) @@ -322,7 +322,7 @@ class VersionedStorage (Storage): """Close the connection to the repository.""" if self.is_writeable() == False: return - f = open(self.repo, 'wb') + f = open(os.path.join(self.repo, 'repo.pkl'), 'wb') pickle.dump([dict((k,v._objects_to_ids()) for k,v in t.items()) for t in self._data], f, -1) f.close() @@ -426,7 +426,7 @@ if TESTING == True: super(StorageTestCase, self).setUp() self.dir = Dir() self.dirname = self.dir.path - self.s = self.Class(repo=os.path.join(self.dirname, 'repo.pkl')) + self.s = self.Class(repo=self.dirname) self.assert_failed_connect() self.s.init() self.s.connect() -- cgit From fa491e55c00363009985dadca264bc23fd0fc76d Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 13 Dec 2009 19:58:50 -0500 Subject: Don't require new revisions on empty commits. For example, hg can't: $ mkdir x; cd x; x$ hg init; x$ echo a> b; hg add b; x$ hg commit -m 'r1'; x$ hg commit -m 'r2'; nothing changed x$ hg log; changeset: 0:e30558c36fca tag: tip user: W. Trevor King date: Sun Dec 13 19:48:47 2009 -0500 summary: hi x$ cd ..; rm -rf x We shouldn't need this functionality anyway ;). --- libbe/storage/base.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 56b59ba..dd35586 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -758,11 +758,14 @@ if TESTING == True: """ Commit / revision_id should agree on revision ids. """ + def val(i): + return '%s:%d' % (self.val, i+1) + self.s.add(self.id, directory=False) revs = [] - for s in range(10): - revs.append(self.s.commit(self.commit_msg, - self.commit_body, - allow_empty=True)) + for i in range(10): + self.s.set(self.id, val(i)) + revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), + self.commit_body)) for i in range(10): rev = self.s.revision_id(i+1) self.failUnless(rev == revs[i], -- 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/storage/base.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index dd35586..97c8b29 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -256,8 +256,10 @@ class Storage (object): else: decode = False value = self._get(*args, **kwargs) - if decode == True: + if decode == True and type(value) != types.UnicodeType: return unicode(value, self.encoding) + if decode == False and type(value) != types.StringType: + return value.encode(self.encoding) return value def _get(self, id, default=InvalidObject, revision=None): @@ -673,7 +675,7 @@ if TESTING == True: self.failUnless(s == val, "%s.get() returned %s not %s" % (vars(self.Class)['name'], s, self.val)) - + class Storage_persistence_TestCase (StorageTestCase): """Test cases for Storage.disconnect and .connect methods.""" @@ -767,7 +769,7 @@ if TESTING == True: revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), self.commit_body)) for i in range(10): - rev = self.s.revision_id(i+1) + rev = self.s.revision_id(i+1) self.failUnless(rev == revs[i], "%s.revision_id(%d) returned %s not %s" % (vars(self.Class)['name'], i+1, rev, revs[i])) @@ -794,7 +796,7 @@ if TESTING == True: self.failUnless(ret == val(i), "%s.get() returned %s not %s for revision %s" % (vars(self.Class)['name'], ret, val(i), revs[i])) - + def make_storage_testcase_subclasses(storage_class, namespace): """Make StorageTestCase subclasses for storage_class in namespace.""" storage_testcase_classes = [ -- cgit From dff704764d77bffbf6cc94c5ba4bb03309da45f8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 27 Dec 2009 16:30:54 -0500 Subject: Added storage.Storage.storage_version() and command.InvalidStorageVersion. Now commands automatically check for storage version compatibility. --- libbe/storage/base.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 97c8b29..f32353f 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -10,6 +10,7 @@ import pickle import types from libbe.error import NotSupported +import libbe.storage from libbe.util.tree import Tree from libbe.util import InvalidObject from libbe import TESTING @@ -133,6 +134,10 @@ class Storage (object): """Return a version string for this backend.""" return '0' + def storage_version(self): + """Return the storage format for this backend.""" + return libbe.storage.STORAGE_VERSION + def is_readable(self): return self.readable and self._readable -- cgit From cfebc238cbda9b6338ec57d5c215c4cbf0246f8b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Sun, 27 Dec 2009 16:50:36 -0500 Subject: Moved InvalidStorageVersion from libbe.command to libbe.storage Also added ConnectionError pretty-print to ui.command_line, storage version checking to BugDir.duplicate_bugdir(), and optional revision argument to Storage.storage_version(). --- libbe/storage/base.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index f32353f..b43f765 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -26,6 +26,16 @@ if TESTING == True: class ConnectionError (Exception): pass +class InvalidStorageVersion(ConnectionError): + def __init__(self, active_version, expected_version=None): + if expected_version == None: + expected_version = libbe.storage.STORAGE_VERSION + msg = 'Storage in "%s" not the expected "%s"' \ + % (active_version, expected_version) + Exception.__init__(self, msg) + self.active_version = active_version + self.expected_version = expected_version + class InvalidID (KeyError): pass @@ -50,6 +60,7 @@ class EmptyCommit(Exception): def __init__(self): Exception.__init__(self, 'No changes to commit') + class Entry (Tree): def __init__(self, id, value=None, parent=None, directory=False, children=None): @@ -134,7 +145,7 @@ class Storage (object): """Return a version string for this backend.""" return '0' - def storage_version(self): + def storage_version(self, revision=None): """Return the storage format for this backend.""" return libbe.storage.STORAGE_VERSION -- cgit From e0c58cc0577fbb1b692e051eabd8597ba35c886a Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 10:06:40 -0500 Subject: libbe.storage.vcs.base.VCS._init() now creates the '.be/version' file. And python test.py libbe.storage.vcs.base passes again. --- libbe/storage/base.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index b43f765..9da60ad 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -272,10 +272,11 @@ class Storage (object): else: decode = False value = self._get(*args, **kwargs) - if decode == True and type(value) != types.UnicodeType: - return unicode(value, self.encoding) - if decode == False and type(value) != types.StringType: - return value.encode(self.encoding) + if value != None: + if decode == True and type(value) != types.UnicodeType: + return unicode(value, self.encoding) + elif decode == False and type(value) != types.StringType: + return value.encode(self.encoding) return value def _get(self, id, default=InvalidObject, revision=None): -- cgit From 7607146d13233dc4ca6c2ed99889ceb43d7298d0 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 10:24:57 -0500 Subject: VersionedStorage_commit_TestCase now allows for versioned files created by self.s.init() --- libbe/storage/base.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 9da60ad..d99c22c 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -737,11 +737,22 @@ if TESTING == True: class VersionedStorage_commit_TestCase (VersionedStorageTestCase): """Test cases for VersionedStorage methods.""" - id = 'I' #unlikely id' - val = 'X' - commit_msg = 'C' #ommitting something interesting' - commit_body = 'B' #ome\nlonger\ndescription\n' + id = 'unlikely id' + val = 'Some value' + commit_msg = 'Committing something interesting' + commit_body = 'Some\nlonger\ndescription\n' + def _setup_for_empty_commit(self): + """ + Initialization might add some files to version control, so + commit those first, before testing the empty commit + functionality. + """ + try: + self.s.commit('Added initialization files') + except EmptyCommit: + pass + def test_revision_id_exception(self): """ Invalid revision id should raise InvalidRevision. @@ -758,6 +769,7 @@ if TESTING == True: """ Empty commit should raise exception. """ + self._setup_for_empty_commit() try: self.s.commit(self.commit_msg, self.commit_body) self.fail( @@ -770,6 +782,7 @@ if TESTING == True: """ Empty commit should _not_ raise exception if allow_empty=True. """ + self._setup_for_empty_commit() self.s.commit(self.commit_msg, self.commit_body, allow_empty=True) -- cgit From 2d6ed9ec7181ef805f305c6c8b7152c1b9ec6ec8 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 11:13:49 -0500 Subject: Added VersionedStorage_commit_TestCase.test_commit_revision_ids() --- libbe/storage/base.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index d99c22c..9807d86 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -827,6 +827,29 @@ if TESTING == True: "%s.get() returned %s not %s for revision %s" % (vars(self.Class)['name'], ret, val(i), revs[i])) + def test_get_previous_children(self): + """ + Children list should be revision dependent. + """ + self.s.add('parent', directory=True) + revs = [] + cur_children = [] + children = [] + for i in range(10): + new_child = str(i) + self.s.add(new_child, 'parent', directory=False) + self.s.set(new_child, self.val) + revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), + self.commit_body)) + cur_children.append(new_child) + children.append(list(cur_children)) + for i in range(10): + ret = self.s.children('parent', revision=revs[i]) + self.failUnless(ret == children[i], + "%s.get() returned %s not %s for revision %s" + % (vars(self.Class)['name'], ret, + children[i], revs[i])) + def make_storage_testcase_subclasses(storage_class, namespace): """Make StorageTestCase subclasses for storage_class in namespace.""" storage_testcase_classes = [ -- cgit From c90044dff5feaf5f43fee9e8559fecec2ec60091 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 28 Dec 2009 12:30:19 -0500 Subject: Fixed VCS.children() and Bzr.children() for non-None revisions. Now they both pass VersionedStorage_commit_TestCase.test_commit_revision_ids() The .children() implementation for previous revisions lacks the working directory's id<->path cache, so it's fairly slow... --- libbe/storage/base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 9807d86..d16c30b 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -37,7 +37,12 @@ class InvalidStorageVersion(ConnectionError): self.expected_version = expected_version class InvalidID (KeyError): - pass + def __init__(self, id=None, revision=None, msg=None): + if msg == None and id != None: + msg = id + KeyError.__init__(self, msg) + self.id = id + self.revision = revision class InvalidRevision (KeyError): pass -- cgit From b1540a08131173ace920f2d3d0829e54b8f26283 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 29 Dec 2009 21:12:12 -0500 Subject: Fixed make_*_testcase_subclasses() to avoid duplication. Also removed final check for 'parent' existence in Storage_add_remove_TestCase.test_remove_nonrooted() because some VCSs (e.g. Git) don't keep track of blank directories. --- libbe/storage/base.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index d16c30b..ffde475 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -501,7 +501,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], directory=False) + self.s.add(ids[-1], directory=(i % 2 == 0)) s = sorted(self.s.children()) self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) @@ -513,7 +513,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent', directory=True) + self.s.add(ids[-1], 'parent', directory=(i % 2 == 0)) s = sorted(self.s.children('parent')) self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) s = self.s.children() @@ -527,7 +527,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append('parent/%s' % str(i)) - self.s.add(ids[-1], 'parent', directory=True) + self.s.add(ids[-1], 'parent', directory=(i % 2 == 0)) s = sorted(self.s.children('parent')) self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) @@ -560,7 +560,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], directory=True) + self.s.add(ids[-1], directory=(i % 2 == 0)) for i in range(10): self.s.remove(ids.pop()) s = sorted(self.s.children()) @@ -574,13 +574,14 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent', directory=False) + self.s.add(ids[-1], 'parent', directory=False)#(i % 2 == 0)) for i in range(10): self.s.remove(ids.pop()) s = sorted(self.s.children('parent')) self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) - s = self.s.children() - self.failUnless(s == ['parent'], s) + if len(s) > 0: + s = self.s.children() + self.failUnless(s == ['parent'], s) def test_remove_directory_not_empty(self): """ @@ -590,7 +591,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent', directory=True) + self.s.add(ids[-1], 'parent', directory=(i % 2 == 0)) self.s.remove(ids.pop()) # empty directory removal succeeds try: self.s.remove('parent') # empty directory removal succeeds @@ -610,7 +611,7 @@ if TESTING == True: ids.append(str(i)) self.s.add(ids[-1], 'parent', directory=True) for j in range(10): # add some grandkids - self.s.add(str(20*(i+1)+j), ids[-1], directory=False) + self.s.add(str(20*(i+1)+j), ids[-1], directory=(i%2 == 0)) self.s.recursive_remove('parent') s = sorted(self.s.children()) self.failUnless(s == [], s) @@ -726,7 +727,7 @@ if TESTING == True: ids = [] for i in range(10): ids.append(str(i)) - self.s.add(ids[-1], 'parent', directory=False) + self.s.add(ids[-1], 'parent', directory=(i % 2 == 0)) self.s.disconnect() self.s.connect() s = sorted(self.s.children('parent')) @@ -842,7 +843,7 @@ if TESTING == True: children = [] for i in range(10): new_child = str(i) - self.s.add(new_child, 'parent', directory=False) + self.s.add(new_child, 'parent', directory=(i % 2 == 0)) self.s.set(new_child, self.val) revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), self.commit_body)) @@ -861,7 +862,7 @@ if TESTING == True: c for c in ( ob for ob in globals().values() if isinstance(ob, type)) if issubclass(c, StorageTestCase) \ - and not issubclass(c, VersionedStorageTestCase)] + and c.Class == Storage] for base_class in storage_testcase_classes: testcase_class_name = storage_class.__name__ + base_class.__name__ @@ -877,7 +878,8 @@ if TESTING == True: storage_testcase_classes = [ c for c in ( ob for ob in globals().values() if isinstance(ob, type)) - if issubclass(c, StorageTestCase)] + if issubclass(c, StorageTestCase) \ + and c.Class == Storage] for base_class in storage_testcase_classes: testcase_class_name = storage_class.__name__ + base_class.__name__ -- cgit From 0d31c5a14719ed8f18b1554b3975690aea81c719 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 31 Dec 2009 11:47:33 -0500 Subject: Track connection status to allow multiple Storage.disconnect() calls. This makes cleaning up UIs easier: just call disconnect() :p. --- libbe/storage/base.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index ffde475..1c711fa 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -139,6 +139,7 @@ class Storage (object): self._writeable = True # hard limit (backend choice) self.versioned = False self.can_init = True + self.connected = False def __str__(self): return '<%s %s %s>' % (self.__class__.__name__, id(self), self.repo) @@ -190,6 +191,7 @@ class Storage (object): if self.is_readable() == False: raise NotReadable('Cannot connect to unreadable storage.') self._connect() + self.connected = True def _connect(self): try: @@ -204,6 +206,12 @@ class Storage (object): """Close the connection to the repository.""" if self.is_writeable() == False: return + if self.connected == False: + return + self._disconnect() + self.connected = False + + def _disconnect(self): f = open(os.path.join(self.repo, 'repo.pkl'), 'wb') pickle.dump(dict((k,v._objects_to_ids()) for k,v in self._data.items()), f, -1) @@ -342,10 +350,7 @@ class VersionedStorage (Storage): for t in d] f.close() - def disconnect(self): - """Close the connection to the repository.""" - if self.is_writeable() == False: - return + def _disconnect(self): f = open(os.path.join(self.repo, 'repo.pkl'), 'wb') pickle.dump([dict((k,v._objects_to_ids()) for k,v in t.items()) for t in self._data], f, -1) @@ -478,6 +483,14 @@ if TESTING == True: """Should connect after initialization.""" self.s.connect() + class Storage_connect_disconnect_TestCase (StorageTestCase): + """Test cases for Storage.connect and .disconnect methods.""" + + def test_multiple_disconnects(self): + """Should be able to call .disconnect multiple times.""" + self.s.disconnect() + self.s.disconnect() + class Storage_add_remove_TestCase (StorageTestCase): """Test cases for Storage.add, .remove, and .recursive_remove methods.""" -- 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/storage/base.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 1c711fa..d85627f 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -1,4 +1,18 @@ -# Copyright +# Copyright (C) 2009-2010 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. """ Abstract bug repository data storage to easily support multiple backends. -- cgit From 95f9395487126f97225f5e25f0c833ee6c02a644 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 1 Jan 2010 10:46:23 -0500 Subject: Return a meaningful Storage.version() --- libbe/storage/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index d85627f..aa32ea9 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -27,6 +27,7 @@ from libbe.error import NotSupported import libbe.storage from libbe.util.tree import Tree from libbe.util import InvalidObject +import libbe.version from libbe import TESTING if TESTING == True: @@ -163,7 +164,7 @@ class Storage (object): def version(self): """Return a version string for this backend.""" - return '0' + return libbe.version.version() def storage_version(self, revision=None): """Return the storage format for this backend.""" -- cgit From 2ab53616af78a0f84b768f65c7a5dbb0c6ed9492 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 15 Jan 2010 21:30:04 -0500 Subject: Added libbe.storage.base.VersionedStorage.changed() and a test. Also converted libbe.storage.base.VersionedStorage revision ids from integers to strings. --- libbe/storage/base.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 5 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index aa32ea9..0f6b095 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -394,12 +394,16 @@ class VersionedStorage (Storage): id = '__ROOT__' if revision == None: revision = -1 + else: + revision = int(revision) return [c.id for c in self._data[revision][id] if not c.id.startswith('__')] def _get(self, id, default=InvalidObject, revision=None): if revision == None: revision = -1 + else: + revision = int(revision) if id in self._data[revision]: return self._data[revision][id].value elif default == InvalidObject: @@ -428,7 +432,7 @@ class VersionedStorage (Storage): raise EmptyCommit self._data[-1]["__COMMIT__SUMMARY__"].value = summary self._data[-1]["__COMMIT__BODY__"].value = body - rev = len(self._data)-1 + rev = str(len(self._data)-1) self._data.append(copy.deepcopy(self._data[-1])) return rev @@ -452,12 +456,34 @@ class VersionedStorage (Storage): raise InvalidRevision(index) L = len(self._data) - 1 # -1 b/c of initial commit if index >= -L and index <= L: - return index % L + return str(index % L) raise InvalidRevision(i) + def changed(self, revision): + """ + Return a tuple of lists of ids + (new, modified, removed) + from the specified revision to the current situation. + """ + new = [] + modified = [] + removed = [] + for id,value in self._data[int(revision)].items(): + if id.startswith('__'): + continue + if not id in self._data[-1]: + removed.append(id) + elif value.value != self._data[-1][id].value: + modified.append(id) + for id in self._data[-1]: + if not id in self._data[int(revision)]: + new.append(id) + return (new, modified, removed) + + if TESTING == True: class StorageTestCase (unittest.TestCase): - """Test cases for base Storage class.""" + """Test cases for Storage class.""" Class = Storage @@ -764,12 +790,12 @@ if TESTING == True: self.failUnless(s == ['parent'], s) class VersionedStorageTestCase (StorageTestCase): - """Test cases for base VersionedStorage class.""" + """Test cases for VersionedStorage methods.""" Class = VersionedStorage class VersionedStorage_commit_TestCase (VersionedStorageTestCase): - """Test cases for VersionedStorage methods.""" + """Test cases for VersionedStorage.commit and revision_ids methods.""" id = 'unlikely id' val = 'Some value' @@ -884,6 +910,34 @@ if TESTING == True: % (vars(self.Class)['name'], ret, children[i], revs[i])) + class VersionedStorage_changed_TestCase (VersionedStorageTestCase): + """Test cases for VersionedStorage.changed() method.""" + + def test_changed(self): + self.s.add('dir', directory=True) + self.s.add('modified', parent='dir') + self.s.set('modified', 'some value to be modified') + self.s.add('moved', parent='dir') + self.s.set('moved', 'this entry will be moved') + self.s.add('removed', parent='dir') + self.s.set('removed', 'this entry will be deleted') + revA = self.s.commit('Initial state') + self.s.add('new', parent='dir') + self.s.set('new', 'this entry is new') + self.s.set('modified', 'a new value') + self.s.remove('moved') + self.s.add('moved2', parent='dir') + self.s.set('moved2', 'this entry will be moved') + self.s.remove('removed') + revB = self.s.commit('Final state') + new,mod,rem = self.s.changed(revA) + self.failUnless(sorted(new) == ['moved2', 'new'], + 'Unexpected new: %s' % new) + self.failUnless(mod == ['modified'], + 'Unexpected modified: %s' % mod) + self.failUnless(sorted(rem) == ['moved', 'removed'], + 'Unexpected removed: %s' % rem) + def make_storage_testcase_subclasses(storage_class, namespace): """Make StorageTestCase subclasses for storage_class in namespace.""" storage_testcase_classes = [ -- cgit From 8688804803235e1ca472526ed1904599bfba8e6c Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 18 Jan 2010 07:53:59 -0500 Subject: Add VersionedStorageTestCases in make_versioned_storage_testcase_subclasses --- libbe/storage/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 0f6b095..1b0ceba 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -960,8 +960,11 @@ if TESTING == True: storage_testcase_classes = [ c for c in ( ob for ob in globals().values() if isinstance(ob, type)) - if issubclass(c, StorageTestCase) \ - and c.Class == Storage] + if ((issubclass(c, StorageTestCase) \ + and c.Class == Storage) + or + (issubclass(c, VersionedStorageTestCase) \ + and c.Class == VersionedStorage))] for base_class in storage_testcase_classes: testcase_class_name = storage_class.__name__ + base_class.__name__ -- cgit From 1c42075f6b3ba35bbb32e3ec6549f30024ad7179 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 18 Jan 2010 08:02:52 -0500 Subject: Too much trouble to handle Git's lack of dir versioning in test_get_previous_children --- libbe/storage/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 1b0ceba..d88f802 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -897,7 +897,7 @@ if TESTING == True: children = [] for i in range(10): new_child = str(i) - self.s.add(new_child, 'parent', directory=(i % 2 == 0)) + self.s.add(new_child, 'parent') self.s.set(new_child, self.val) revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), self.commit_body)) -- cgit From 3de27f5863be5ce00a4afef7eb5a4ea918c136db Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 18 Jan 2010 10:00:31 -0500 Subject: Added Storage.ancestors --- libbe/storage/base.py | 124 +++++++++++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 47 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index d88f802..84ec1d4 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -274,6 +274,26 @@ class Storage (object): for entry in reversed(list(self._data[id].traverse())): self._remove(entry.id) + def ancestors(self, *args, **kwargs): + """Return a list of the specified entry's ancestors' ids.""" + if self.is_readable() == False: + raise NotReadable('Cannot list parents with unreadable storage.') + return self._ancestors(*args, **kwargs) + + def _ancestors(self, id=None, revision=None): + if id == None: + return [] + ancestors = [] + stack = [id] + while len(stack) > 0: + id = stack.pop(0) + parent = self._data[id].parent + if parent != None and not parent.id.startswith('__'): + ancestor = parent.id + ancestors.append(ancestor) + stack.append(ancestor) + return ancestors + def children(self, *args, **kwargs): """Return a list of specified entry's children's ids.""" if self.is_readable() == False: @@ -389,6 +409,24 @@ class VersionedStorage (Storage): for entry in reversed(list(self._data[-1][id].traverse())): self._remove(entry.id) + def _ancestors(self, id=None, revision=None): + if id == None: + return [] + if revision == None: + revision = -1 + else: + revision = int(revision) + ancestors = [] + stack = [id] + while len(stack) > 0: + id = stack.pop(0) + parent = self._data[revision][id].parent + if parent != None and not parent.id.startswith('__'): + ancestor = parent.id + ancestors.append(ancestor) + stack.append(ancestor) + return ancestors + def _children(self, id=None, revision=None): if id == None: id = '__ROOT__' @@ -540,8 +578,7 @@ if TESTING == True: self.failUnless(len(self.s.children()) == 0, self.s.children()) def test_add_identical_rooted(self): - """ - Adding entries with the same ID should not increase the number of children. + """Adding entries with the same ID should not increase the number of children. """ for i in range(10): self.s.add('some id', directory=False) @@ -549,8 +586,7 @@ if TESTING == True: self.failUnless(s == ['some id'], s) def test_add_rooted(self): - """ - Adding entries should increase the number of children (rooted). + """Adding entries should increase the number of children (rooted). """ ids = [] for i in range(10): @@ -560,8 +596,7 @@ if TESTING == True: self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) def test_add_nonrooted(self): - """ - Adding entries should increase the number of children (nonrooted). + """Adding entries should increase the number of children (nonrooted). """ self.s.add('parent', directory=True) ids = [] @@ -573,9 +608,23 @@ if TESTING == True: s = self.s.children() self.failUnless(s == ['parent'], s) - def test_children(self): + def test_ancestors(self): + """Check ancestors lists. """ - Non-UUID ids should be returned as such. + self.s.add('parent', directory=True) + for i in range(10): + i_id = str(i) + self.s.add(i_id, 'parent', directory=True) + for j in range(10): # add some grandkids + j_id = str(20*(i+1)+j) + self.s.add(j_id, i_id, directory=(i%2 == 0)) + ancestors = sorted(self.s.ancestors(j_id)) + self.failUnless(ancestors == [i_id, 'parent'], + 'Unexpected ancestors for %s/%s, "%s"' + % (i_id, j_id, ancestors)) + + def test_children(self): + """Non-UUID ids should be returned as such. """ self.s.add('parent', directory=True) ids = [] @@ -586,8 +635,7 @@ if TESTING == True: self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) def test_add_invalid_directory(self): - """ - Should not be able to add children to non-directories. + """Should not be able to add children to non-directories. """ self.s.add('parent', directory=False) try: @@ -608,8 +656,7 @@ if TESTING == True: self.s.children('parent')) def test_remove_rooted(self): - """ - Removing entries should decrease the number of children (rooted). + """Removing entries should decrease the number of children (rooted). """ ids = [] for i in range(10): @@ -621,8 +668,7 @@ if TESTING == True: self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) def test_remove_nonrooted(self): - """ - Removing entries should decrease the number of children (nonrooted). + """Removing entries should decrease the number of children (nonrooted). """ self.s.add('parent', directory=True) ids = [] @@ -638,8 +684,7 @@ if TESTING == True: self.failUnless(s == ['parent'], s) def test_remove_directory_not_empty(self): - """ - Removing a non-empty directory entry should raise exception. + """Removing a non-empty directory entry should raise exception. """ self.s.add('parent', directory=True) ids = [] @@ -656,9 +701,7 @@ if TESTING == True: pass def test_recursive_remove(self): - """ - Recursive remove should empty the tree. - """ + """Recursive remove should empty the tree.""" self.s.add('parent', directory=True) ids = [] for i in range(10): @@ -677,8 +720,7 @@ if TESTING == True: val = 'unlikely value' def test_get_default(self): - """ - Get should return specified default if id not in Storage. + """Get should return specified default if id not in Storage. """ ret = self.s.get(self.id, default=self.val) self.failUnless(ret == self.val, @@ -686,8 +728,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, self.val)) def test_get_default_exception(self): - """ - Get should raise exception if id not in Storage and no default. + """Get should raise exception if id not in Storage and no default. """ try: ret = self.s.get(self.id) @@ -698,8 +739,7 @@ if TESTING == True: pass def test_get_initial_value(self): - """ - Data value should be None before any value has been set. + """Data value should be None before any value has been set. """ self.s.add(self.id, directory=False) ret = self.s.get(self.id) @@ -708,8 +748,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret)) def test_set_exception(self): - """ - Set should raise exception if id not in Storage. + """Set should raise exception if id not in Storage. """ try: self.s.set(self.id, self.val) @@ -720,8 +759,7 @@ if TESTING == True: pass def test_set(self): - """ - Set should define the value returned by get. + """Set should define the value returned by get. """ self.s.add(self.id, directory=False) self.s.set(self.id, self.val) @@ -731,8 +769,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, self.val)) def test_unicode_set(self): - """ - Set should define the value returned by get. + """Set should define the value returned by get. """ val = u'Fran\xe7ois' self.s.add(self.id, directory=False) @@ -761,8 +798,7 @@ if TESTING == True: val = 'unlikely value' def test_get_set_persistence(self): - """ - Set should define the value returned by get after reconnect. + """Set should define the value returned by get after reconnect. """ self.s.add(self.id, directory=False) self.s.set(self.id, self.val) @@ -774,8 +810,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, self.val)) def test_add_nonrooted_persistence(self): - """ - Adding entries should increase the number of children after reconnect. + """Adding entries should increase the number of children after reconnect. """ self.s.add('parent', directory=True) ids = [] @@ -814,8 +849,7 @@ if TESTING == True: pass def test_revision_id_exception(self): - """ - Invalid revision id should raise InvalidRevision. + """Invalid revision id should raise InvalidRevision. """ try: rev = self.s.revision_id('highly unlikely revision id') @@ -826,8 +860,7 @@ if TESTING == True: pass def test_empty_commit_raises_exception(self): - """ - Empty commit should raise exception. + """Empty commit should raise exception. """ self._setup_for_empty_commit() try: @@ -839,16 +872,14 @@ if TESTING == True: pass def test_empty_commit_allowed(self): - """ - Empty commit should _not_ raise exception if allow_empty=True. + """Empty commit should _not_ raise exception if allow_empty=True. """ self._setup_for_empty_commit() self.s.commit(self.commit_msg, self.commit_body, allow_empty=True) def test_commit_revision_ids(self): - """ - Commit / revision_id should agree on revision ids. + """Commit / revision_id should agree on revision ids. """ def val(i): return '%s:%d' % (self.val, i+1) @@ -870,8 +901,7 @@ if TESTING == True: % (vars(self.Class)['name'], i, rev, revs[i])) def test_get_previous_version(self): - """ - Get should be able to return the previous version. + """Get should be able to return the previous version. """ def val(i): return '%s:%d' % (self.val, i+1) @@ -888,8 +918,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, val(i), revs[i])) def test_get_previous_children(self): - """ - Children list should be revision dependent. + """Children list should be revision dependent. """ self.s.add('parent', directory=True) revs = [] @@ -914,6 +943,7 @@ if TESTING == True: """Test cases for VersionedStorage.changed() method.""" def test_changed(self): + """Changed lists should reflect past activity""" self.s.add('dir', directory=True) self.s.add('modified', parent='dir') self.s.set('modified', 'some value to be modified') -- cgit From 7ae29f930fe73adada5174a2ce74266411809ac7 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 18 Jan 2010 12:25:17 -0500 Subject: Added VCS._u_find_id_from_manifest for faster id->path calculation --- libbe/storage/base.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 84ec1d4..10649a8 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -53,11 +53,15 @@ class InvalidStorageVersion(ConnectionError): class InvalidID (KeyError): def __init__(self, id=None, revision=None, msg=None): - if msg == None and id != None: - msg = id - KeyError.__init__(self, msg) + KeyError.__init__(self, id) + self.msg = msg self.id = id self.revision = revision + def __str__(self): + if self.msg == None: + return '%s in revision %s' % (self.id, self.revision) + return self.msg + class InvalidRevision (KeyError): pass -- cgit From e06f1aa3f5db039fa8bf1f26412059fe99588a2b Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Mon, 18 Jan 2010 12:46:08 -0500 Subject: Add class name to StorageTestCase failure reporting --- libbe/storage/base.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 10649a8..202305b 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -533,6 +533,23 @@ if TESTING == True: super(StorageTestCase, self).__init__(*args, **kwargs) self.dirname = None + # this class will be the basis of tests for several classes, + # so make sure we print the name of the class we're dealing with. + def fail(self, msg=None): + """Fail immediately, with the given message.""" + raise self.failureException, \ + '(%s) %s' % (self.Class.__name__, msg) + + def failIf(self, expr, msg=None): + "Fail the test if the expression is true." + if expr: raise self.failureException, \ + '(%s) %s' % (self.Class.__name__, msg) + + def failUnless(self, expr, msg=None): + """Fail the test unless the expression is true.""" + if not expr: raise self.failureException, \ + '(%s) %s' % (self.Class.__name__, msg) + def setUp(self): """Set up test fixtures for Storage test case.""" super(StorageTestCase, self).setUp() -- cgit From d72430fee347e21a9b9e7912417615bbdb22e6d4 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 22 Jan 2010 13:28:01 -0500 Subject: Added _EMPTY and Storage.exists() to libbe.storage.base. There seem to be problems distinguishing between "added but unset" IDs and "added and set to ''" IDs. Now _EMPTY lets us mark "added but unset", and Storage.exists() handles "already added?" more clearly than the old hack "does .get() succeed?". --- libbe/storage/base.py | 55 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 12 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 202305b..64ae3e7 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -84,9 +84,12 @@ class EmptyCommit(Exception): def __init__(self): Exception.__init__(self, 'No changes to commit') +class _EMPTY (object): + """Entry has been added but has no user-set value.""" + pass class Entry (Tree): - def __init__(self, id, value=None, parent=None, directory=False, + def __init__(self, id, value=_EMPTY, parent=None, directory=False, children=None): if children == None: Tree.__init__(self) @@ -241,10 +244,7 @@ class Storage (object): """Add an entry""" if self.is_writeable() == False: raise NotWriteable('Cannot add entry to unwriteable storage.') - try: # Maybe we've already added that id? - self.get(id) - pass # yup, no need to add another - except InvalidID: + if not self.exists(id): self._add(id, *args, **kwargs) def _add(self, id, parent=None, directory=False): @@ -253,6 +253,15 @@ class Storage (object): p = self._data[parent] self._data[id] = Entry(id, parent=p, directory=directory) + def exists(self, *args, **kwargs): + """Check an entry's existence""" + if self.is_readable() == False: + raise NotReadable('Cannot check entry existence in unreadable storage.') + return self._exists(*args, **kwargs) + + def _exists(self, id, revision=None): + return id in self._data + def remove(self, *args, **kwargs): """Remove an entry.""" if self.is_writeable() == False: @@ -332,7 +341,7 @@ class Storage (object): return value def _get(self, id, default=InvalidObject, revision=None): - if id in self._data: + if id in self._data and self._data[id].value != _EMPTY: return self._data[id].value elif default == InvalidObject: raise InvalidID(id) @@ -402,6 +411,13 @@ class VersionedStorage (Storage): p = self._data[-1][parent] self._data[-1][id] = Entry(id, parent=p, directory=directory) + def _exists(self, id, revision=None): + if revision == None: + revision = -1 + else: + revision = int(revision) + return id in self._data[revision] + def _remove(self, id): if self._data[-1][id].directory == True \ and len(self.children(id)) > 0: @@ -446,7 +462,8 @@ class VersionedStorage (Storage): revision = -1 else: revision = int(revision) - if id in self._data[revision]: + if id in self._data[revision] \ + and self._data[revision][id].value != _EMPTY: return self._data[revision][id].value elif default == InvalidObject: raise InvalidID(id) @@ -760,13 +777,14 @@ if TESTING == True: pass def test_get_initial_value(self): - """Data value should be None before any value has been set. + """Data value should be default before any value has been set. """ self.s.add(self.id, directory=False) - ret = self.s.get(self.id) - self.failUnless(ret == None, - "%s.get() returned %s not None" - % (vars(self.Class)['name'], ret)) + val = 'UNLIKELY DEFAULT' + ret = self.s.get(self.id, default=val) + self.failUnless(ret == val, + "%s.get() returned %s not %s" + % (vars(self.Class)['name'], ret, val)) def test_set_exception(self): """Set should raise exception if id not in Storage. @@ -830,6 +848,19 @@ if TESTING == True: "%s.get() returned %s not %s" % (vars(self.Class)['name'], ret, self.val)) + def test_empty_get_set_persistence(self): + """After empty set, get may return either an empty string or default. + """ + self.s.add(self.id, directory=False) + self.s.set(self.id, '') + self.s.disconnect() + self.s.connect() + default = 'UNLIKELY DEFAULT' + ret = self.s.get(self.id, default=default) + self.failUnless(ret in ['', default], + "%s.get() returned %s not in %s" + % (vars(self.Class)['name'], ret, ['', default])) + def test_add_nonrooted_persistence(self): """Adding entries should increase the number of children after reconnect. """ -- cgit From 4203368d2a1f9cc794646754a5027e307074fbf6 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Wed, 27 Jan 2010 10:27:27 -0500 Subject: Make VCS error messages and Storage test failures more descriptive --- libbe/storage/base.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 64ae3e7..423f141 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -552,20 +552,29 @@ if TESTING == True: # this class will be the basis of tests for several classes, # so make sure we print the name of the class we're dealing with. + def _classname(self): + version = '?' + try: + if hasattr(self, 's'): + version = self.s.version() + except: + pass + return '%s:%s' % (self.Class.__name__, version) + def fail(self, msg=None): """Fail immediately, with the given message.""" raise self.failureException, \ - '(%s) %s' % (self.Class.__name__, msg) + '(%s) %s' % (self._classname(), msg) def failIf(self, expr, msg=None): "Fail the test if the expression is true." if expr: raise self.failureException, \ - '(%s) %s' % (self.Class.__name__, msg) + '(%s) %s' % (self.classname(), msg) def failUnless(self, expr, msg=None): """Fail the test unless the expression is true.""" if not expr: raise self.failureException, \ - '(%s) %s' % (self.Class.__name__, msg) + '(%s) %s' % (self.classname(), msg) def setUp(self): """Set up test fixtures for Storage test case.""" -- cgit From da686174480de4cb3b881e3d42bbc8d68e9dfb43 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Wed, 27 Jan 2010 12:41:18 -0500 Subject: Fix StorageTestCase.classname -> ._classname() Introduced in wking@drexel.edu-20100127152727-nu58o4g6jea5or7w --- libbe/storage/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index 423f141..ade6587 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -569,12 +569,12 @@ if TESTING == True: def failIf(self, expr, msg=None): "Fail the test if the expression is true." if expr: raise self.failureException, \ - '(%s) %s' % (self.classname(), msg) + '(%s) %s' % (self._classname(), msg) def failUnless(self, expr, msg=None): """Fail the test unless the expression is true.""" if not expr: raise self.failureException, \ - '(%s) %s' % (self.classname(), msg) + '(%s) %s' % (self._classname(), msg) def setUp(self): """Set up test fixtures for Storage test case.""" -- cgit From 9b42ab6d3e2372c2c0f26a0788f8b84d1d346171 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Wed, 27 Jan 2010 16:50:34 -0500 Subject: Implement Arch._vcs_path() Fixes VersionedStorage_commit_TestCase.test_get_previous_children. Should have fixed VersionedStorage_commit_TestCase.test_get_previous_version too, but 'tla file-find' is buggy: https://bugs.launchpad.net/ubuntu/+source/tla/+bug/513472 Also: * sort children in test_get_previous_children, since we shouldn't require a particular child order * unescape filenames in Arch._diff() * remove debugging prints from Arch._parse_diff() * remove silly blank line in git.py I'd stumbled across ;). --- libbe/storage/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index ade6587..ad6b291 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -994,7 +994,7 @@ if TESTING == True: cur_children.append(new_child) children.append(list(cur_children)) for i in range(10): - ret = self.s.children('parent', revision=revs[i]) + ret = sorted(self.s.children('parent', revision=revs[i])) self.failUnless(ret == children[i], "%s.get() returned %s not %s for revision %s" % (vars(self.Class)['name'], ret, -- 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/storage/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'libbe/storage/base.py') diff --git a/libbe/storage/base.py b/libbe/storage/base.py index ad6b291..0ae9c53 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -519,10 +519,8 @@ class VersionedStorage (Storage): raise InvalidRevision(i) def changed(self, revision): - """ - Return a tuple of lists of ids - (new, modified, removed) - from the specified revision to the current situation. + """Return a tuple of lists of ids `(new, modified, removed)` from the + specified revision to the current situation. """ new = [] modified = [] -- cgit