diff options
author | W. Trevor King <wking@drexel.edu> | 2010-01-23 11:16:42 -0500 |
---|---|---|
committer | W. Trevor King <wking@drexel.edu> | 2010-01-23 11:16:42 -0500 |
commit | 4ea7a28e1bd97df5997b5f884378bd7d64edff6e (patch) | |
tree | 8be1029b10240e9280581359e32aee4acf30ae75 | |
parent | e0ce49ffa3d09d37310ce04c50268275b8e2ca54 (diff) | |
parent | 2f7045deeb0225fed1692b3390502077e4907758 (diff) | |
download | bugseverywhere-4ea7a28e1bd97df5997b5f884378bd7d64edff6e.tar.gz |
Fixed some bugs in settings_object handling and got test_usage.sh working.
Highlights:
* clarified settings_object handling and improved unittests
* added `be list --mine`
* fixed some bugs in List, Import_XML, Init
* added Storage.exists() (haven't implemented it in all VCS backends though).
* added VCS.path()
-rw-r--r-- | NEWS | 3 | ||||
-rw-r--r-- | libbe/bug.py | 8 | ||||
-rw-r--r-- | libbe/bugdir.py | 8 | ||||
-rw-r--r-- | libbe/command/import_xml.py | 2 | ||||
-rw-r--r-- | libbe/command/init.py | 9 | ||||
-rw-r--r-- | libbe/command/list.py | 22 | ||||
-rw-r--r-- | libbe/comment.py | 8 | ||||
-rw-r--r-- | libbe/storage/base.py | 55 | ||||
-rw-r--r-- | libbe/storage/util/properties.py | 19 | ||||
-rw-r--r-- | libbe/storage/util/settings_object.py | 388 | ||||
-rw-r--r-- | libbe/storage/vcs/base.py | 62 | ||||
-rw-r--r-- | libbe/storage/vcs/bzr.py | 9 | ||||
-rwxr-xr-x | test_usage.sh | 77 |
13 files changed, 441 insertions, 229 deletions
@@ -1,3 +1,6 @@ +January 23, 2010 + * Added `be list --mine`, listing bugs belonging to you. + January 20, 2010 * Renamed 'be-mbox-to-xml' -> 'be-mail-to-xml' and added support for several mailbox formats. diff --git a/libbe/bug.py b/libbe/bug.py index 7e9999e..0b40921 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -121,7 +121,7 @@ def load_status(active_status_def, inactive_status_def): load_status(active_status_def, inactive_status_def) -class Bug(settings_object.SavedSettingsObject): +class Bug (settings_object.SavedSettingsObject): """ >>> b = Bug() >>> print b.status @@ -242,8 +242,6 @@ class Bug(settings_object.SavedSettingsObject): if from_storage == False: if uuid == None: self.uuid = libbe.util.id.uuid_gen() - self.settings = {} - self._setup_saved_settings() self.time = int(time.time()) # only save to second precision self.summary = summary dummy = self.comment_root @@ -630,11 +628,11 @@ class Bug(settings_object.SavedSettingsObject): settings_mapfile = \ self.storage.get(self.id.storage('values'), default='\n') try: - self.settings = mapfile.parse(settings_mapfile) + settings = mapfile.parse(settings_mapfile) except mapfile.InvalidMapfileContents, e: raise Exception('Invalid settings file for bug %s\n' '(BE version missmatch?)' % self.id.user()) - self._setup_saved_settings() + self._setup_saved_settings(settings) def save_settings(self): mf = mapfile.generate(self._get_saved_settings()) diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 8389716..dd467bf 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -178,12 +178,9 @@ class BugDir (list, settings_object.SavedSettingsObject): if self.uuid == None: self.uuid = [c for c in self.storage.children() if c != 'version'][0] - self.load_settings() else: if self.uuid == None: self.uuid = libbe.util.id.uuid_gen() - self.settings = {} - self._setup_saved_settings() if self.storage != None and self.storage.is_writeable(): self.save() @@ -194,12 +191,11 @@ class BugDir (list, settings_object.SavedSettingsObject): settings_mapfile = \ self.storage.get(self.id.storage('settings'), default='\n') try: - self.settings = mapfile.parse(settings_mapfile) + settings = mapfile.parse(settings_mapfile) except mapfile.InvalidMapfileContents, e: raise Exception('Invalid settings file for bugdir %s\n' '(BE version missmatch?)' % self.id.user()) - self._setup_saved_settings() - #self._setup_user_id(self.user_id) + self._setup_saved_settings(settings) self._setup_severities(self.severities) self._setup_status(self.active_status, self.inactive_status) diff --git a/libbe/command/import_xml.py b/libbe/command/import_xml.py index 598ecb8..287d8b7 100644 --- a/libbe/command/import_xml.py +++ b/libbe/command/import_xml.py @@ -115,7 +115,7 @@ class Import_XML (libbe.command.Command): if params['xml-file'] == '-': xml = self.stdin.read().encode(self.stdin.encoding) else: - self._check_restricted_access(storage, params['xml-file']) + self._check_restricted_access(bugdir.storage, params['xml-file']) xml = libbe.util.encoding.get_file_contents( params['xml-file']) diff --git a/libbe/command/init.py b/libbe/command/init.py index 7fdbdae..7b83645 100644 --- a/libbe/command/init.py +++ b/libbe/command/init.py @@ -49,6 +49,9 @@ class Init (libbe.command.Command): BE repository initialized. >>> bd = libbe.bugdir.BugDir(vcs) >>> vcs.disconnect() + >>> vcs.connect() + >>> bugdir = libbe.bugdir.BugDir(vcs, from_storage=True) + >>> vcs.disconnect() >>> vcs.destroy() >>> dir.cleanup() @@ -66,6 +69,9 @@ class Init (libbe.command.Command): Using ... for revision control. BE repository initialized. >>> vcs.disconnect() + >>> vcs.connect() + >>> bugdir = libbe.bugdir.BugDir(vcs, from_storage=True) + >>> vcs.disconnect() >>> vcs.destroy() >>> dir.cleanup() """ @@ -87,8 +93,9 @@ class Init (libbe.command.Command): pass storage.init() storage.connect() + self.ui.storage_callbacks.set_storage(storage) bd = libbe.bugdir.BugDir(storage, from_storage=False) - bd.save() + self.ui.storage_callbacks.set_bugdir(bd) if bd.storage.name is not 'None': print >> self.stdout, \ 'Using %s for revision control.' % storage.name diff --git a/libbe/command/list.py b/libbe/command/list.py index 44be71b..73c60a9 100644 --- a/libbe/command/list.py +++ b/libbe/command/list.py @@ -107,8 +107,10 @@ class List (libbe.command.Command): libbe.command.Option(name='assigned', short_name='a', help='Only show bugs matching ASSIGNED', arg=libbe.command.Argument( - name='assigned', metavar='ASSIGNED', default='all', + name='assigned', metavar='ASSIGNED', default=None, completion_callback=libbe.command.util.complete_assigned)), + libbe.command.Option(name='mine', short_name='m', + help='List bugs assigned to you'), libbe.command.Option(name='extra-strings', short_name='e', help='Only show bugs matching STRINGS, e.g. --extra-strings' ' TAG:working,TAG:xml', @@ -136,7 +138,6 @@ class List (libbe.command.Command): # ("U", "unconfirmed", "List unconfirmed bugs"), # ("o", "open", "List open bugs"), # ("T", "test", "List bugs in testing"), -# ("m", "mine", "List bugs assigned to you")) # for s in bools: # attr = s[1].replace('-','_') # short = "-%c" % s[0] @@ -153,14 +154,14 @@ class List (libbe.command.Command): writeable = bugdir.storage.writeable bugdir.storage.writeable = False cmp_list, status, severity, assigned, extra_strings_regexps = \ - self._parse_params(params) + self._parse_params(bugdir, params) filter = Filter(status, severity, assigned, extra_strings_regexps=extra_strings_regexps) bugs = [bugdir.bug_from_uuid(uuid) for uuid in bugdir.uuids()] bugs = [b for b in bugs if filter(bugdir, b) == True] self.result = bugs if len(bugs) == 0 and params['xml'] == False: - print >> self.stdout, "No matching bugs found" + print >> self.stdout, 'No matching bugs found' # sort bugs bugs = self._sort_bugs(bugs, cmp_list) @@ -174,13 +175,13 @@ class List (libbe.command.Command): bugdir.storage.writeable = writeable return 0 - def _parse_params(self, params): + def _parse_params(self, bugdir, params): cmp_list = [] if params['sort'] != None: for cmp in params['sort'].sort_by.split(','): if cmp not in AVAILABLE_CMPS: raise libbe.command.UserError( - "Invalid sort on '%s'.\nValid sorts:\n %s" + 'Invalid sort on "%s".\nValid sorts:\n %s' % (cmp, '\n '.join(AVAILABLE_CMPS))) cmp_list.append(eval('libbe.bug.cmp_%s' % cmp)) # select status @@ -203,11 +204,14 @@ class List (libbe.command.Command): severity = libbe.command.util.select_values( params['severity'], bug.severity_values) # select assigned - if params['assigned'] == "all": - assigned = "all" + if params['assigned'] == None: + if params['mine'] == True: + assigned = [self._get_user_id()] + else: + assigned = 'all' else: assigned = libbe.command.util.select_values( - params['assigned'], libbe.command.util.assignees()) + params['assigned'], libbe.command.util.assignees(bugdir)) for i in range(len(assigned)): if assigned[i] == '-': assigned[i] = params['user-id'] diff --git a/libbe/comment.py b/libbe/comment.py index f0cc45c..accd4df 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -93,7 +93,7 @@ def save_comments(bug): comment.save() -class Comment(Tree, settings_object.SavedSettingsObject): +class Comment (Tree, settings_object.SavedSettingsObject): """ >>> c = Comment() >>> c.uuid != None @@ -206,8 +206,6 @@ class Comment(Tree, settings_object.SavedSettingsObject): if from_storage == False: if uuid == None: self.uuid = libbe.util.id.uuid_gen() - self.settings = {} - self._setup_saved_settings() self.time = int(time.time()) # only save to second precision self.in_reply_to = in_reply_to self.body = body @@ -589,11 +587,11 @@ class Comment(Tree, settings_object.SavedSettingsObject): settings_mapfile = \ self.storage.get(self.id.storage("values"), default="\n") try: - self.settings = mapfile.parse(settings_mapfile) + settings = mapfile.parse(settings_mapfile) except mapfile.InvalidMapfileContents, e: raise Exception('Invalid settings file for comment %s\n' '(BE version missmatch?)' % self.id.user()) - self._setup_saved_settings() + self._setup_saved_settings(settings) def save_settings(self): mf = mapfile.generate(self._get_saved_settings()) 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. """ diff --git a/libbe/storage/util/properties.py b/libbe/storage/util/properties.py index f48cfa0..55bac85 100644 --- a/libbe/storage/util/properties.py +++ b/libbe/storage/util/properties.py @@ -303,12 +303,14 @@ def cached_property(generator, initVal=None, mutable=False): return funcs return decorator -def primed_property(primer, initVal=None): +def primed_property(primer, initVal=None, unprimeableVal=None): """ Just like a cached_property, except that instead of returning a - new value and running fset to cache it, the primer performs some + new value and running fset to cache it, the primer attempts some background manipulation (e.g. loads data into instance.settings) - such that a _second_ pass through fget succeeds. + such that a _second_ pass through fget succeeds. If the second + pass doesn't succeed (e.g. no readable storage), we give up and + return unprimeableVal. The 'cache' flag becomes a 'prime' flag, with priming taking place whenever ._<name>_prime is True, or is False or missing and @@ -326,6 +328,8 @@ def primed_property(primer, initVal=None): if prime == True or (prime == False and value == initVal): primer(self) value = fget(self) + if prime == False and value == initVal: + return unprimeableVal return value funcs["fget"] = _fget return funcs @@ -543,13 +547,14 @@ if libbe.TESTING == True: def testPrimedLocalProperty(self): class Test(object): def prime(self): - self.settings["PRIMED"] = "initialized" + self.settings["PRIMED"] = self.primeVal @Property - @primed_property(primer=prime, initVal=None) + @primed_property(primer=prime, initVal=None, unprimeableVal=2) @settings_property(name="PRIMED") def x(): return {} def __init__(self): self.settings={} + self.primeVal = "initialized" t = Test() self.failIf("_PRIMED_prime" in dir(t), getattr(t, "_PRIMED_prime", None)) @@ -564,6 +569,10 @@ if libbe.TESTING == True: t._PRIMED_prime = False t.x = 3 self.failUnless(t.x == 3, t.x) + # test unprimableVal + t.x = None + t.primeVal = None + self.failUnless(t.x == 2, t.x) def testChangeHookLocalProperty(self): class Test(object): def _hook(self, old, new): diff --git a/libbe/storage/util/settings_object.py b/libbe/storage/util/settings_object.py index ca94f23..8434952 100644 --- a/libbe/storage/util/settings_object.py +++ b/libbe/storage/util/settings_object.py @@ -40,7 +40,7 @@ class _Token (object): pass class UNPRIMED (_Token): - "Property has not been primed." + "Property has not been primed (loaded)." pass class EMPTY (_Token): @@ -60,13 +60,13 @@ def prop_save_settings(self, old, new): def prop_load_settings(self): """ - The default action undertaken when an UNPRIMED property is accessed. + The default action undertaken when an UNPRIMED property is + accessed. Attempt to run .load_settings(), which calls + ._setup_saved_settings() internally. If .storage is inaccessible, + don't do anything. """ - if self.storage != None and self.storage.is_readable() \ - and self._settings_loaded==False: + if self.storage != None and self.storage.is_readable(): self.load_settings() - else: - self._setup_saved_settings(flag_as_loaded=False) # Some name-mangling routines for pretty printing setting names def setting_name_to_attr_name(self, name): @@ -129,6 +129,12 @@ def versioned_property(name, doc, * you set change_hook and might have mutable property values See the docstrings in libbe.properties for details on how each of these cases are handled. + + The value stored in .settings[name] will be + * no value (or UNPRIMED) if the property has been neither set, + nor loaded as blank. + * EMPTY if the value has been loaded as blank. + * some value if the property has been either loaded or set. """ settings_properties.append(name) if require_save == True: @@ -152,7 +158,8 @@ def versioned_property(name, doc, % (', '.join(allowed)) hooked = change_hook_property(hook=change_hook, mutable=mutable, default=EMPTY) - primed = primed_property(primer=primer, initVal=UNPRIMED) + primed = primed_property(primer=primer, initVal=UNPRIMED, + unprimeableVal=EMPTY) settings = settings_property(name=name, null=UNPRIMED) docp = doc_property(doc=fulldoc) deco = hooked(primed(settings(docp(funcs)))) @@ -181,27 +188,29 @@ class SavedSettingsObject(object): _attr_name_to_setting_name = attr_name_to_setting_name def __init__(self): - self._settings_loaded = False self.storage = None self.settings = {} def load_settings(self): """Load the settings from disk.""" - # Override. Must call ._setup_saved_settings() after loading. - self.settings = {} - self._setup_saved_settings() + # Override. Must call ._setup_saved_settings({}) with + # from-storage settings. + self._setup_saved_settings({}) - def _setup_saved_settings(self, flag_as_loaded=True): + def _setup_saved_settings(self, settings=None): """ - To be run after setting self.settings up from disk. Marks all - settings as primed. + Sets up a settings dict loaded from storage. Fills in + all missing settings entries with EMPTY. """ + if settings == None: + settings = {} for property in self.settings_properties: if property not in self.settings \ or self.settings[property] == UNPRIMED: - self.settings[property] = EMPTY - if flag_as_loaded == True: - self._settings_loaded = True + if property in settings: + self.settings[property] = settings[property] + else: + self.settings[property] = EMPTY def save_settings(self): """Save the settings to disk.""" @@ -220,15 +229,16 @@ class SavedSettingsObject(object): load already. """ settings = {} + for k in self.settings_properties: # force full load + if not k in self.settings or self.settings[k] == UNPRIMED: + value = getattr( + self, self._setting_name_to_attr_name(k)) for k in self.settings_properties: - if k in self.settings and \ - not self.settings[k] in [None, EMPTY]: + if k in self.settings and self.settings[k] != EMPTY: settings[k] = self.settings[k] - else: - value = getattr( + elif k in self.required_saved_properties: + settings[k] = getattr( self, self._setting_name_to_attr_name(k)) - if value not in [None, EMPTY, []]: - settings[k] = value return settings def clear_cached_setting(self, setting=None): @@ -242,134 +252,275 @@ class SavedSettingsObject(object): if libbe.TESTING == True: + import copy + + class TestStorage (list): + def __init__(self): + list.__init__(self) + self.readable = True + self.writeable = True + def is_readable(self): + return self.readable + def is_writeable(self): + return self.writeable + + class TestObject (SavedSettingsObject): + def load_settings(self): + self.load_count += 1 + if len(self.storage) == 0: + settings = {} + else: + settings = copy.deepcopy(self.storage[-1]) + self._setup_saved_settings(settings) + def save_settings(self): + settings = self._get_saved_settings() + self.storage.append(copy.deepcopy(settings)) + def __init__(self): + SavedSettingsObject.__init__(self) + self.load_count = 0 + self.storage = TestStorage() + class SavedSettingsObjectTests(unittest.TestCase): - def testSimpleProperty(self): - """Testing a minimal versioned property""" - class Test(SavedSettingsObject): + def testSimplePropertyDoc(self): + """Testing a minimal versioned property docstring""" + class Test (TestObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property( + name="Content-type", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def content_type(): return {} + expected = "A test property\n\nThis property defaults to None." + self.failUnless(Test.content_type.__doc__ == expected, + Test.content_type.__doc__) + def testSimplePropertyFromMemory(self): + """Testing a minimal versioned property from memory""" + class Test (TestObject): settings_properties = [] required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) + @versioned_property( + name="Content-type", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) t = Test() - # access missing setting - self.failUnless(t._settings_loaded == False, t._settings_loaded) self.failUnless(len(t.settings) == 0, len(t.settings)) + # accessing t.content_type triggers the priming, but + # t.storage.is_readable() == False, so nothing happens. + t.storage.readable = False + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(t.settings == {}, t.settings) + self.failUnless(len(t.settings) == 0, len(t.settings)) + self.failUnless(t.content_type == None, t.content_type) + # accessing t.content_type triggers the priming again, and + # now that t.storage.is_readable() == True, this fills out + # t.settings with EMPTY data. At this point there should + # be one load and no saves. + t.storage.readable = True self.failUnless(t.content_type == None, t.content_type) - # accessing t.content_type triggers the priming, which runs - # t._setup_saved_settings, which fills out t.settings with - # EMPTY data. t._settings_loaded is still false though, since - # the default priming does not do any of the `official' loading - # that occurs in t.load_settings. self.failUnless(len(t.settings) == 1, len(t.settings)) self.failUnless(t.settings["Content-type"] == EMPTY, t.settings["Content-type"]) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - # load settings creates an EMPTY value in the settings array - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) self.failUnless(t.content_type == None, t.content_type) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 0, len(t.storage)) + # an explicit call to load settings forces a reload, + # but nothing else changes. + t.load_settings() self.failUnless(len(t.settings) == 1, len(t.settings)) self.failUnless(t.settings["Content-type"] == EMPTY, t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(t.load_count == 2, t.load_count) + self.failUnless(len(t.storage) == 0, len(t.storage)) # now we set a value t.content_type = 5 self.failUnless(t.settings["Content-type"] == 5, t.settings["Content-type"]) + self.failUnless(t.load_count == 2, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':5}], t.storage) + # getting its value changes nothing self.failUnless(t.content_type == 5, t.content_type) self.failUnless(t.settings["Content-type"] == 5, t.settings["Content-type"]) + self.failUnless(t.load_count == 2, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':5}], t.storage) # now we set another value t.content_type = "text/plain" self.failUnless(t.content_type == "text/plain", t.content_type) self.failUnless(t.settings["Content-type"] == "text/plain", t.settings["Content-type"]) + self.failUnless(t.load_count == 2, t.load_count) + self.failUnless(len(t.storage) == 2, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':5}, + {'Content-type':'text/plain'}], + t.storage) + # t._get_saved_settings() returns a dict of required or + # non-default values. self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/plain"}, t._get_saved_settings()) # now we clear to the post-primed value t.content_type = EMPTY - self.failUnless(t._settings_loaded == True, t._settings_loaded) self.failUnless(t.settings["Content-type"] == EMPTY, t.settings["Content-type"]) self.failUnless(t.content_type == None, t.content_type) self.failUnless(len(t.settings) == 1, len(t.settings)) self.failUnless(t.settings["Content-type"] == EMPTY, t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == {}, + t._get_saved_settings()) + self.failUnless(t.storage == [{'Content-type':5}, + {'Content-type':'text/plain'}, + {}], + t.storage) + def testSimplePropertyFromStorage(self): + """Testing a minimal versioned property from storage""" + class Test (TestObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property( + name="prop-a", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def prop_a(): return {} + @versioned_property( + name="prop-b", + doc="Another test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def prop_b(): return {} + t = Test() + t.storage.append({'prop-a':'saved'}) + # setting prop-b forces a load (to check for changes), + # which also pulls in prop-a. + t.prop_b = 'new-b' + settings = {'prop-b':'new-b', 'prop-a':'saved'} + self.failUnless(t.settings == settings, t.settings) + self.failUnless(t._get_saved_settings() == settings, + t._get_saved_settings()) + # test that _get_saved_settings() works even when settings + # were _not_ loaded beforehand + t = Test() + t.storage.append({'prop-a':'saved'}) + settings ={'prop-a':'saved'} + self.failUnless(t.settings == {}, t.settings) + self.failUnless(t._get_saved_settings() == settings, + t._get_saved_settings()) + def testSimplePropertySetStorageSave(self): + """Set a property, then attach storage and save""" + class Test (TestObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property( + name="prop-a", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def prop_a(): return {} + @versioned_property( + name="prop-b", + doc="Another test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def prop_b(): return {} + t = Test() + storage = t.storage + t.storage = None + t.prop_a = 'text/html' + t.storage = storage + t.save_settings() + self.failUnless(t.prop_a == 'text/html', t.prop_a) + self.failUnless(t.settings == {'prop-a':'text/html', + 'prop-b':EMPTY}, + t.settings) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'prop-a':'text/html'}], + t.storage) def testDefaultingProperty(self): """Testing a defaulting versioned property""" - class Test(SavedSettingsObject): + class Test (TestObject): settings_properties = [] required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) + @versioned_property( + name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(t.settings == {}, t.settings) self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(t._settings_loaded == True, t._settings_loaded) - self.failUnless(t.content_type == "text/plain", t.content_type) - self.failUnless(t.settings["Content-type"] == EMPTY, - t.settings["Content-type"]) - self.failUnless(t._get_saved_settings() == - {"Content-type":"text/plain"}, + self.failUnless(t.settings == {"Content-type":EMPTY}, + t.settings) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 0, len(t.storage)) + self.failUnless(t._get_saved_settings() == {}, t._get_saved_settings()) t.content_type = "text/html" self.failUnless(t.content_type == "text/html", t.content_type) - self.failUnless(t.settings["Content-type"] == "text/html", - t.settings["Content-type"]) + self.failUnless(t.settings == {"Content-type":"text/html"}, + t.settings) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':'text/html'}], + t.storage) self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/html"}, t._get_saved_settings()) def testRequiredDefaultingProperty(self): """Testing a required defaulting versioned property""" - class Test(SavedSettingsObject): + class Test (TestObject): settings_properties = [] required_saved_properties = [] - @versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties, - require_save=True) + @versioned_property( + name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + require_save=True) def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) t = Test() + self.failUnless(t.settings == {}, t.settings) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings == {"Content-type":EMPTY}, + t.settings) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 0, len(t.storage)) self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/plain"}, t._get_saved_settings()) t.content_type = "text/html" + self.failUnless(t.content_type == "text/html", + t.content_type) + self.failUnless(t.settings == {"Content-type":"text/html"}, + t.settings) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':'text/html'}], + t.storage) self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/html"}, t._get_saved_settings()) def testClassVersionedPropertyDefinition(self): """Testing a class-specific _versioned property decorator""" - class Test(SavedSettingsObject): + class Test (TestObject): settings_properties = [] required_saved_properties = [] - def _versioned_property(settings_properties= \ - settings_properties, - required_saved_properties= \ - required_saved_properties, - **kwargs): + def _versioned_property( + settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + **kwargs): if "settings_properties" not in kwargs: kwargs["settings_properties"] = settings_properties if "required_saved_properties" not in kwargs: @@ -377,69 +528,60 @@ if libbe.TESTING == True: required_saved_properties return versioned_property(**kwargs) @_versioned_property(name="Content-type", - doc="A test property", - default="text/plain", - require_save=True) + doc="A test property", + default="text/plain", + require_save=True) def content_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) t = Test() self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/plain"}, t._get_saved_settings()) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 0, len(t.storage)) t.content_type = "text/html" self.failUnless(t._get_saved_settings() == \ {"Content-type":"text/html"}, t._get_saved_settings()) + self.failUnless(t.load_count == 1, t.load_count) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'Content-type':'text/html'}], + t.storage) def testMutableChangeHookedProperty(self): """Testing a mutable change-hooked property""" - SAVES = [] - def prop_log_save_settings(self, old, new, saves=SAVES): - saves.append("'%s' -> '%s'" % (str(old), str(new))) - prop_save_settings(self, old, new) - class Test(SavedSettingsObject): + class Test (TestObject): settings_properties = [] required_saved_properties = [] - @versioned_property(name="List-type", - doc="A test property", - mutable=True, - change_hook=prop_log_save_settings, - settings_properties=settings_properties, - required_saved_properties= \ - required_saved_properties) + @versioned_property( + name="List-type", + doc="A test property", + mutable=True, + change_hook=prop_save_settings, + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) def list_type(): return {} - def __init__(self): - SavedSettingsObject.__init__(self) t = Test() - self.failUnless(t._settings_loaded == False, t._settings_loaded) - t.load_settings() - self.failUnless(SAVES == [], SAVES) - self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(len(t.storage) == 0, len(t.storage)) self.failUnless(t.list_type == None, t.list_type) - self.failUnless(SAVES == [], SAVES) + self.failUnless(len(t.storage) == 0, len(t.storage)) self.failUnless(t.settings["List-type"]==EMPTY, t.settings["List-type"]) t.list_type = [] self.failUnless(t.settings["List-type"] == [], t.settings["List-type"]) - self.failUnless(SAVES == [ - "'<class 'libbe.storage.util.settings_object.EMPTY'>' -> '[]'" - ], SAVES) - t.list_type.append(5) - self.failUnless(SAVES == [ - "'<class 'libbe.storage.util.settings_object.EMPTY'>' -> '[]'", - ], SAVES) + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'List-type':[]}], + t.storage) + t.list_type.append(5) # external modification not detected yet + self.failUnless(len(t.storage) == 1, len(t.storage)) + self.failUnless(t.storage == [{'List-type':[]}], + t.storage) self.failUnless(t.settings["List-type"] == [5], t.settings["List-type"]) - self.failUnless(SAVES == [ # the append(5) has not yet been saved - "'<class 'libbe.storage.util.settings_object.EMPTY'>' -> '[]'", - ], SAVES) - self.failUnless(t.list_type == [5], t.list_type)#get triggers saved - - self.failUnless(SAVES == [ # now the append(5) has been saved. - "'<class 'libbe.storage.util.settings_object.EMPTY'>' -> '[]'", - "'[]' -> '[5]'" - ], SAVES) + self.failUnless(t.list_type == [5], t.list_type)# get triggers save + self.failUnless(len(t.storage) == 2, len(t.storage)) + self.failUnless(t.storage == [{'List-type':[]}, + {'List-type':[5]}], + t.storage) unitsuite = unittest.TestLoader().loadTestsFromTestCase( \ SavedSettingsObjectTests) diff --git a/libbe/storage/vcs/base.py b/libbe/storage/vcs/base.py index 9fc43c1..7d4383f 100644 --- a/libbe/storage/vcs/base.py +++ b/libbe/storage/vcs/base.py @@ -504,6 +504,12 @@ os.listdir(self.get_path("bugs")): """ pass + def _vcs_exists(self, path, revision=None): + """ + Does the path exist in a given revision? (True/False) + """ + raise NotImplementedError + def _vcs_remove(self, path): """ Remove the file at path from version control. Optionally @@ -550,7 +556,7 @@ os.listdir(self.get_path("bugs")): def _vcs_path(self, id, revision): """ - Return the path to object id as of revision. + Return the relative path to object id as of revision. Revision will not be None. """ @@ -694,6 +700,17 @@ os.listdir(self.get_path("bugs")): def _disconnect(self): self._cached_path_id.disconnect() + def path(self, id, revision=None, relpath=True): + if revision == None: + path = self._cached_path_id.path(id) + if relpath == True: + return self._u_rel_path(path) + return path + path = self._vcs_path(id, revision) + if relpath == True: + return path + return os.path.join(self.repo, path) + def _add_path(self, path, directory=False): relpath = self._u_rel_path(path) reldirs = relpath.split(os.path.sep) @@ -716,6 +733,16 @@ os.listdir(self.get_path("bugs")): path = self._cached_path_id.add_id(id, parent) self._add_path(path, **kwargs) + def _exists(self, id, revision=None): + if revision == None: + try: + path = self.path(id, revision, relpath=False) + except InvalidID, e: + return False + return os.path.exists(path) + path = self.path(id, revision, relpath=True) + return self._vcs_exists(relpath, revision) + def _remove(self, id): path = self._cached_path_id.path(id) if os.path.exists(path): @@ -746,15 +773,10 @@ os.listdir(self.get_path("bugs")): self._cached_path_id.remove_id(id) def _ancestors(self, id=None, revision=None): - if revision == None: - id_to_path = self._cached_path_id.path - else: - id_to_path = lambda id : os.path.join( - self.repo, self._vcs_path(id, revision)) if id==None: path = self.be_dir else: - path = id_to_path(id) + path = self.path(id, revision, relpath=False) ancestors = [] while True: if not path.startswith(self.repo + os.path.sep): @@ -769,12 +791,9 @@ os.listdir(self.get_path("bugs")): def _children(self, id=None, revision=None): if revision == None: - id_to_path = self._cached_path_id.path isdir = os.path.isdir listdir = os.listdir else: - id_to_path = lambda id : os.path.join( - self.repo, self._vcs_path(id, revision)) isdir = lambda path : self._vcs_isdir( self._u_rel_path(path), revision) listdir = lambda path : self._vcs_listdir( @@ -782,7 +801,7 @@ os.listdir(self.get_path("bugs")): if id==None: path = self.be_dir else: - path = id_to_path(id) + path = self.path(id, revision, relpath=False) if isdir(path) == False: return [] children = listdir(path) @@ -810,25 +829,18 @@ os.listdir(self.get_path("bugs")): def _get(self, id, default=libbe.util.InvalidObject, revision=None): try: - path = self._cached_path_id.path(id) + relpath = self.path(id, revision, relpath=True) + contents = self._vcs_get_file_contents(relpath, revision) except InvalidID, e: if default == libbe.util.InvalidObject: raise e return default - relpath = self._u_rel_path(path) - try: - contents = self._vcs_get_file_contents(relpath, revision) - except InvalidID, e: - if e.id == None: - e.id = id - if e.revision == None: - e.revision = revision - raise if contents in [libbe.storage.base.InvalidDirectory, - libbe.util.InvalidObject]: - raise InvalidID(id, revision) - elif len(contents) == 0: - return None + libbe.util.InvalidObject] \ + or len(contents) == 0: + if default == libbe.util.InvalidObject: + raise InvalidID(id, revision) + return default return contents def _set(self, id, value): diff --git a/libbe/storage/vcs/bzr.py b/libbe/storage/vcs/bzr.py index e1cd2e5..1db50f8 100644 --- a/libbe/storage/vcs/bzr.py +++ b/libbe/storage/vcs/bzr.py @@ -98,6 +98,13 @@ class Bzr(base.VCS): cmd.outf = StringIO.StringIO() cmd.run(file_list=[path], file_ids_from=self.repo) + def _vcs_exists(self, path, revision=None): + manifest = self._vcs_listdir( + self.repo, revision=revision, recursive=True) + if path in manifest: + return True + return False + def _vcs_remove(self, path): # --force to also remove unversioned files. path = os.path.join(self.repo, path) @@ -131,7 +138,7 @@ class Bzr(base.VCS): if 'not present in revision' in str(e): raise base.InvalidPath(path, root=self.repo, revision=revision) raise - return cmd.outf.getvalue() + return cmd.outf.getvalue() def _vcs_path(self, id, revision): manifest = self._vcs_listdir( diff --git a/test_usage.sh b/test_usage.sh index 13be2ff..9b7dafe 100755 --- a/test_usage.sh +++ b/test_usage.sh @@ -4,8 +4,8 @@ # features work, and gives an example of suggested usage to get people # started. # -# usage: test_usage.sh RCS -# where RCS is one of: +# usage: test_usage.sh VCS +# where VCS is one of: # bzr, git, hg, arch, none # # Note that this script uses the *installed* version of be, not the @@ -18,34 +18,33 @@ set -v # verbose, echo commands to stdout exec 6>&2 # save stderr to file descriptor 6 exec 2>&1 # fd 2 now writes to stdout -ONLY_TEST_COMMIT="true" - if [ $# -gt 1 ] then - echo "usage: test_usage.sh [RCS]" + echo "usage: test_usage.sh [VCS]" echo "" - echo "where RCS is one of" - for RCS in arch bzr darcs git hg none + echo "where VCS is one of" + for VCS in arch bzr darcs git hg none do - echo " $RCS" + echo " $VCS" done exit 1 elif [ $# -eq 0 ] then - for RCS in arch bzr darcs git hg none + for VCS in arch bzr darcs git hg none do - echo -e "\n\nTesting $RCS\n\n" - $0 "$RCS" || exit 1 + echo -e "\n\nTesting $VCS\n\n" + $0 "$VCS" || exit 1 done exit 0 fi -RCS="$1" +VCS="$1" TESTDIR=`mktemp -d /tmp/BEtest.XXXXXXXXXX` cd $TESTDIR -if [ "$RCS" == "arch" ] +# Initialize the VCS repository +if [ "$VCS" == "arch" ] then ID=`tla my-id` ARCH_PARAM_DIR="$HOME/.arch-params" @@ -64,73 +63,79 @@ then sed -i 's/^source .*/source ^[._=a-zA-X0-9].*$/' '{arch}/=tagging-method' echo "tla import -A $ARCH_ARCHIVE --summary 'Began versioning'" tla import -A $ARCH_ARCHIVE --summary 'Began versioning' -elif [ "$RCS" == "bzr" ] +elif [ "$VCS" == "bzr" ] then ID=`bzr whoami` bzr init -elif [ "$RCS" == "darcs" ] +elif [ "$VCS" == "darcs" ] then if [ -z "$DARCS_EMAIL" ]; then export DARCS_EMAIL="J. Doe <jdoe@example.com>" fi ID="$DARCS_EMAIL" darcs init -elif [ "$RCS" == "git" ] +elif [ "$VCS" == "git" ] then NAME=`git config user.name` EMAIL=`git config user.email` ID="$NAME <$EMAIL>" git init -elif [ "$RCS" == "hg" ] +elif [ "$VCS" == "hg" ] then ID=`hg showconfig ui.username` hg init -elif [ "$RCS" == "none" ] +elif [ "$VCS" == "none" ] then ID=`id -nu` else - echo "Unrecognized RCS '$RCS'" + echo "Unrecognized VCS '$VCS'" exit 1 fi + if [ -z "$ID" ] -then # set a default ID +then # set a default ID for VCSs that aren't tracking one yet. ID="John Doe <jdoe@example.com>" fi echo "I am '$ID'" -be init -OUT=`be new 'having too much fun'` +be init # initialize the Bugs Everywhere repository +OUT=`be new 'having too much fun'` # create a new bug echo "$OUT" BUG=`echo "$OUT" | sed -n 's/Created bug with ID //p'` echo "Working with bug: $BUG" be comment $BUG "This is an argument" -be set user_id "$ID" # get tired of guessing user id for none RCS +#be set user_id "$ID" # get tired of guessing user id for none VCS be set # show settings -be comment $BUG:1 "No it isn't" # comment on the first comment +be comment $BUG/ "No it isn't" # comment on the first comment be show $BUG # show details on a given bug -be close $BUG # set bug status to 'closed' +be status closed $BUG # set bug status to 'closed' be comment $BUG "It's closed, but I can still comment." -be open $BUG # set bug status to 'open' +if [ "$VCS" != 'none' ]; then + be commit 'Initial commit' +fi +be status open $BUG # set bug status to 'open' be comment $BUG "Reopend, comment again" -be status $BUG fixed # set bug status to 'fixed' +be status fixed $BUG # set bug status to 'fixed' be list # list all open bugs be list --status fixed # list all fixed bugs -be assign $BUG # assign the bug to yourself -be list -m -s fixed # see fixed bugs assigned to you -be assign $BUG 'Joe' # assign the bug to Joe -be list -a Joe -s fixed # list the fixed bugs assigned to Joe -be assign $BUG none # assign the bug to noone -be diff # see what has changed +be assign - $BUG # assign the bug to yourself +be list -m --status fixed # see fixed bugs assigned to you +be assign 'Joe' $BUG # assign the bug to Joe +be list -a Joe --status fixed # list the fixed bugs assigned to Joe +be assign none $BUG # un-assign the bug +if [ "$VCS" != 'none' ]; then + be diff # see what has changed +fi OUT=`be new 'also having too much fun'` BUGB=`echo "$OUT" | sed -n 's/Created bug with ID //p'` be comment $BUGB "Blissfully unaware of a similar bug" be merge $BUG $BUGB # join BUGB to BUG -be show $BUG # show bug details & comments +be --no-pager show $BUG # show bug details & comments # you can also export/import XML bugs/comments OUT=`be new 'yet more fun'` BUGC=`echo "$OUT" | sed -n 's/Created bug with ID //p'` be comment $BUGC "The ants go marching..." -be show --xml $BUGC | be comment --xml ${BUG}:2 - +be show --xml $BUGC/ | be import-xml --add-only --comment-root $BUG - be remove $BUG # decide that you don't like that bug after all be commit "You can even commit using BE" be commit --allow-empty "And you can add empty commits if you like" @@ -139,7 +144,7 @@ be commit "But this will fail" || echo "Failed" cd / rm -rf $TESTDIR -if [ "$RCS" == "arch" ] +if [ "$VCS" == "arch" ] then # Cleanup everything outside of TESTDIR rm -rf "$ARCH_ARCHIVE_ROOT" |