aboutsummaryrefslogtreecommitdiffstats
path: root/libbe
diff options
context:
space:
mode:
Diffstat (limited to 'libbe')
-rw-r--r--libbe/bug.py34
-rw-r--r--libbe/bugdir.py113
-rw-r--r--libbe/comment.py39
-rw-r--r--libbe/properties.py59
-rw-r--r--libbe/rcs.py10
-rw-r--r--libbe/settings_object.py25
-rw-r--r--libbe/utility.py21
7 files changed, 216 insertions, 85 deletions
diff --git a/libbe/bug.py b/libbe/bug.py
index f5f479c..f3448e2 100644
--- a/libbe/bug.py
+++ b/libbe/bug.py
@@ -177,7 +177,7 @@ class Bug(settings_object.SavedSettingsObject):
def time_string(): return {}
def _get_time(self):
- if self.time_string in [None, settings_object.EMPTY]:
+ if self.time_string == None:
return None
return utility.str_to_time(self.time_string)
def _set_time(self, value):
@@ -187,15 +187,8 @@ class Bug(settings_object.SavedSettingsObject):
doc="An integer version of .time_string")
def _extra_strings_check_fn(value):
- "Require an iterable full of strings"
- if value == settings_object.EMPTY:
- return True
- elif not hasattr(value, "__iter__"):
- return False
- for x in value:
- if type(x) not in types.StringTypes:
- return False
- return True
+ return utility.iterable_full_of_strings(value, \
+ alternative=settings_object.EMPTY)
def _extra_strings_change_hook(self, old, new):
self.extra_strings.sort() # to make merging easier
self._prop_save_settings(old, new)
@@ -252,12 +245,16 @@ class Bug(settings_object.SavedSettingsObject):
def __repr__(self):
return "Bug(uuid=%r)" % self.uuid
+ def set_sync_with_disk(self, value):
+ self.sync_with_disk = value
+ for comment in self.comments():
+ comment.set_sync_with_disk(value)
+
def _setting_attr_string(self, setting):
value = getattr(self, setting)
- if value in [None, settings_object.EMPTY]:
+ if value == None:
return ""
- else:
- return str(value)
+ return str(value)
def xml(self, show_comments=False):
if self.bugdir == None:
@@ -372,10 +369,17 @@ class Bug(settings_object.SavedSettingsObject):
mapfile.map_save(self.rcs, path, self._get_saved_settings())
def save(self):
+ """
+ Save any loaded contents to disk. Because of lazy loading of
+ comments, this is actually not too inefficient.
+
+ However, if self.sync_with_disk = True, then any changes are
+ automatically written to disk as soon as they happen, so
+ calling this method will just waste time (unless something
+ else has been messing with your on-disk files).
+ """
self.save_settings()
-
if len(self.comment_root) > 0:
- self.rcs.mkdir(self.get_path("comments"))
comment.saveComments(self)
def remove(self):
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index 764e449..6e020ee 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -51,7 +51,7 @@ class NoRootEntry(Exception):
class AlreadyInitialized(Exception):
def __init__(self, path):
self.path = path
- Exception.__init__(self,
+ Exception.__init__(self,
"Specified root is already initialized: %s" % path)
class MultipleBugMatches(ValueError):
@@ -70,7 +70,7 @@ class BugDir (list, settings_object.SavedSettingsObject):
"""
Sink to existing root
======================
-
+
Consider the following usage case:
You have a bug directory rooted in
/path/to/source
@@ -85,23 +85,35 @@ class BugDir (list, settings_object.SavedSettingsObject):
/path/to/source/GUI/.be miss
/path/to/source/.be hit!
So it still roots itself appropriately without much work for you.
-
+
File-system access
==================
-
- When rooted in non-bugdir directory, BugDirs live completely in
- memory until the first call to .save(). This creates a '.be'
- sub-directory containing configurations options, bugs, comments,
- etc. Once this sub-directory has been created (possibly by
- another BugDir instance) any changes to the BugDir in memory will
- be flushed to the file system automatically. However, the BugDir
- will only load information from the file system when it loads new
- bugs/comments that it doesn't already have in memory, or when it
- explicitly asked to do so (e.g. .load() or __init__(from_disk=True)).
-
+
+ BugDirs live completely in memory when .sync_with_disk is False.
+ This is the default configuration setup by BugDir(from_disk=False).
+ If .sync_with_disk == True (e.g. BugDir(from_disk=True)), then
+ any changes to the BugDir will be immediately written to disk.
+
+ If you want to change .sync_with_disk, we suggest you use
+ .set_sync_with_disk(), which propogates the new setting through to
+ all bugs/comments/etc. that have been loaded into memory. If
+ you've been living in memory and want to move to
+ .sync_with_disk==True, but you're not sure if anything has been
+ changed in memoryy, a call to save() is a safe move.
+
+ Regardless of .sync_with_disk, a call to .save() will write out
+ all the contents that the BugDir instance has loaded into memory.
+ If sync_with_disk has been True over the course of all interesting
+ changes, this .save() call will be a waste of time.
+
+ The BugDir will only load information from the file system when it
+ loads new bugs/comments that it doesn't already have in memory, or
+ when it explicitly asked to do so (e.g. .load() or
+ __init__(from_disk=True)).
+
Allow RCS initialization
========================
-
+
This one is for testing purposes. Setting it to True allows the
BugDir to search for an installed RCS backend and initialize it in
the root directory. This is a convenience option for supporting
@@ -109,7 +121,7 @@ class BugDir (list, settings_object.SavedSettingsObject):
Disable encoding manipulation
=============================
-
+
This one is for testing purposed. You might have non-ASCII
Unicode in your bugs, comments, files, etc. BugDir instances try
and support your preferred encoding scheme (e.g. "utf-8") when
@@ -141,10 +153,11 @@ class BugDir (list, settings_object.SavedSettingsObject):
def _guess_encoding(self):
return encoding.get_encoding()
def _check_encoding(value):
- if value != None and value != settings_object.EMPTY:
+ if value != None:
return encoding.known_encoding(value)
def _setup_encoding(self, new_encoding):
- if new_encoding != None and new_encoding != settings_object.EMPTY:
+ # change hook called before generator.
+ if new_encoding not in [None, settings_object.EMPTY]:
if self._manipulate_encodings == True:
encoding.set_IO_stream_encodings(new_encoding)
def _set_encoding(self, old_encoding, new_encoding):
@@ -159,7 +172,7 @@ class BugDir (list, settings_object.SavedSettingsObject):
def encoding(): return {}
def _setup_user_id(self, user_id):
- self.rcs.user_id = user_id
+ self.rcs.user_id = user_id
def _guess_user_id(self):
return self.rcs.get_user_id()
def _set_user_id(self, old_user_id, new_user_id):
@@ -214,7 +227,21 @@ settings easy. Don't set this attribute. Set .rcs instead, and
if uuid not in map:
map[uuid] = None
self._bug_map_value = map # ._bug_map_value used by @local_property
-
+
+ def _extra_strings_check_fn(value):
+ return utility.iterable_full_of_strings(value, \
+ alternative=settings_object.EMPTY)
+ def _extra_strings_change_hook(self, old, new):
+ self.extra_strings.sort() # to make merging easier
+ self._prop_save_settings(old, new)
+ @_versioned_property(name="extra_strings",
+ doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
+ default=[],
+ check_fn=_extra_strings_check_fn,
+ change_hook=_extra_strings_change_hook,
+ mutable=True)
+ def extra_strings(): return {}
+
@Property
@primed_property(primer=_bug_map_gen)
@local_property("bug_map")
@@ -222,7 +249,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and
def _bug_map(): return {}
def _setup_severities(self, severities):
- if severities != None and severities != settings_object.EMPTY:
+ if severities not in [None, settings_object.EMPTY]:
bug.load_severities(severities)
def _set_severities(self, old_severities, new_severities):
self._setup_severities(new_severities)
@@ -269,7 +296,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and
# get a temporary rcs until we've loaded settings
self.sync_with_disk = False
self.rcs = self._guess_rcs()
-
+
if from_disk == True:
self.sync_with_disk = True
self.load()
@@ -283,6 +310,11 @@ settings easy. Don't set this attribute. Set .rcs instead, and
self.rcs = rcs
self._setup_user_id(self.user_id)
+ def set_sync_with_disk(self, value):
+ self.sync_with_disk = value
+ for bug in self:
+ bug.set_sync_with_disk(value)
+
def _find_root(self, path):
"""
Search for an existing bug database dir and it's ancestors and
@@ -301,7 +333,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and
if beroot == None:
raise NoBugDir(path)
return beroot
-
+
def get_version(self, path=None, use_none_rcs=False):
if use_none_rcs == True:
RCS = rcs.rcs_by_name("None")
@@ -316,6 +348,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and
return tree_version
def set_version(self):
+ self.rcs.mkdir(self.get_path())
self.rcs.set_file_contents(self.get_path("version"),
TREE_VERSION_STRING)
@@ -347,7 +380,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and
if not os.path.exists(self.get_path()):
raise NoBugDir(self.get_path())
self.load_settings()
-
+
self.rcs = rcs.rcs_by_name(self.rcs_name)
self._setup_user_id(self.user_id)
@@ -358,10 +391,17 @@ settings easy. Don't set this attribute. Set .rcs instead, and
self._load_bug(uuid)
def save(self):
- self.rcs.mkdir(self.get_path())
+ """
+ Save any loaded contents to disk. Because of lazy loading of
+ bugs and comments, this is actually not too inefficient.
+
+ However, if self.sync_with_disk = True, then any changes are
+ automatically written to disk as soon as they happen, so
+ calling this method will just waste time (unless something
+ else has been messing with your on-disk files).
+ """
self.set_version()
self.save_settings()
- self.rcs.mkdir(self.get_path("bugs"))
for bug in self:
bug.save()
@@ -377,7 +417,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and
allow_no_rcs = not self.rcs.path_in_root(settings_path)
# allow_no_rcs=True should only be for the special case of
# configuring duplicate bugdir settings
-
+
try:
settings = mapfile.map_load(self.rcs, settings_path, allow_no_rcs)
except rcs.NoSuchFile:
@@ -392,6 +432,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and
allow_no_rcs = not self.rcs.path_in_root(settings_path)
# allow_no_rcs=True should only be for the special case of
# configuring duplicate bugdir settings
+ self.rcs.mkdir(self.get_path(), allow_no_rcs)
mapfile.map_save(self.rcs, settings_path, settings, allow_no_rcs)
def duplicate_bugdir(self, revision):
@@ -442,6 +483,9 @@ settings easy. Don't set this attribute. Set .rcs instead, and
def new_bug(self, uuid=None, summary=None):
bg = bug.Bug(bugdir=self, uuid=uuid, summary=summary)
+ bg.set_sync_with_disk(self.sync_with_disk)
+ if bg.sync_with_disk == True:
+ bg.save()
self.append(bg)
self._bug_map_gen()
return bg
@@ -455,7 +499,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and
Generate short names from uuids. Picks the minimum number of
characters (>=3) from the beginning of the uuid such that the
short names are unique.
-
+
Obviously, as the number of bugs in the database grows, these
short names will cease to be unique. The complete uuid should be
used for long term reference.
@@ -502,7 +546,7 @@ settings easy. Don't set this attribute. Set .rcs instead, and
if bug_uuid not in self._bug_map:
return False
return True
-
+
def simple_bug_dir():
"""
@@ -591,14 +635,17 @@ class BugDirTestCase(unittest.TestCase):
self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime))
self.bugdir.save()
self.versionTest()
- def testComments(self):
+ def testComments(self, sync_with_disk=False):
+ if sync_with_disk == True:
+ self.bugdir.set_sync_with_disk(True)
self.bugdir.new_bug(uuid="a", summary="Ant")
bug = self.bugdir.bug_from_uuid("a")
comm = bug.comment_root
rep = comm.new_reply("Ants are small.")
rep.new_reply("And they have six legs.")
- self.bugdir.save()
- self.bugdir._clear_bugs()
+ if sync_with_disk == False:
+ self.bugdir.save()
+ self.bugdir._clear_bugs()
bug = self.bugdir.bug_from_uuid("a")
bug.load_comments()
self.failUnless(len(bug.comment_root)==1, len(bug.comment_root))
@@ -622,6 +669,8 @@ class BugDirTestCase(unittest.TestCase):
comment.body)
else:
self.failIf(True, "Invalid comment: %d\n%s" % (index, comment))
+ def testSyncedComments(self):
+ self.testComments(sync_with_disk=True)
unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase)
suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()])
diff --git a/libbe/comment.py b/libbe/comment.py
index e2f4ba7..3249e8b 100644
--- a/libbe/comment.py
+++ b/libbe/comment.py
@@ -123,6 +123,7 @@ def loadComments(bug, load_full=False):
if uuid.startswith('.'):
continue
comm = Comment(bug, uuid, from_disk=True)
+ comm.set_sync_with_disk(bug.sync_with_disk)
if load_full == True:
comm.load_settings()
dummy = comm.body # force the body to load
@@ -130,8 +131,6 @@ def loadComments(bug, load_full=False):
return list_to_root(comments, bug)
def saveComments(bug):
- path = bug.get_path("comments")
- bug.rcs.mkdir(path)
for comment in bug.comment_root.traverse():
comment.save()
@@ -219,6 +218,20 @@ class Comment(Tree, settings_object.SavedSettingsObject):
@doc_property(doc="A revision control system instance.")
def rcs(): return {}
+ def _extra_strings_check_fn(value):
+ return utility.iterable_full_of_strings(value, \
+ alternative=settings_object.EMPTY)
+ def _extra_strings_change_hook(self, old, new):
+ self.extra_strings.sort() # to make merging easier
+ self._prop_save_settings(old, new)
+ @_versioned_property(name="extra_strings",
+ doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
+ default=[],
+ check_fn=_extra_strings_check_fn,
+ change_hook=_extra_strings_change_hook,
+ mutable=True)
+ def extra_strings(): return {}
+
def __init__(self, bug=None, uuid=None, from_disk=False,
in_reply_to=None, body=None):
"""
@@ -249,6 +262,9 @@ class Comment(Tree, settings_object.SavedSettingsObject):
self.in_reply_to = in_reply_to
self.body = body
+ def set_sync_with_disk(self, value):
+ self.sync_with_disk = True
+
def traverse(self, *args, **kwargs):
"""Avoid working with the possible dummy root comment"""
for comment in Tree.traverse(self, *args, **kwargs):
@@ -258,10 +274,9 @@ class Comment(Tree, settings_object.SavedSettingsObject):
def _setting_attr_string(self, setting):
value = getattr(self, setting)
- if value in [None, settings_object.EMPTY]:
+ if value == None:
return ""
- else:
- return str(value)
+ return str(value)
def xml(self, indent=0, shortname=None):
"""
@@ -430,13 +445,19 @@ class Comment(Tree, settings_object.SavedSettingsObject):
self._setup_saved_settings()
def save_settings(self):
- parent_dir = os.path.dirname(self.get_path())
- self.rcs.mkdir(parent_dir)
self.rcs.mkdir(self.get_path())
path = self.get_path("values")
mapfile.map_save(self.rcs, path, self._get_saved_settings())
def save(self):
+ """
+ Save any loaded contents to disk.
+
+ However, if self.sync_with_disk = True, then any changes are
+ automatically written to disk as soon as they happen, so
+ calling this method will just waste time (unless something
+ else has been messing with your on-disk files).
+ """
assert self.body != None, "Can't save blank comment"
self.save_settings()
self._set_comment_body(new=self.body, force=True)
@@ -461,6 +482,10 @@ class Comment(Tree, settings_object.SavedSettingsObject):
True
"""
reply = Comment(self.bug, body=body)
+ if self.bug != None:
+ reply.set_sync_with_disk(self.bug.sync_with_disk)
+ if reply.sync_with_disk == True:
+ reply.save()
self.add_reply(reply)
return reply
diff --git a/libbe/properties.py b/libbe/properties.py
index e9affcb..144220b 100644
--- a/libbe/properties.py
+++ b/libbe/properties.py
@@ -52,7 +52,7 @@ def Property(funcs):
args["fset"] = funcs.get("fset", None)
args["fdel"] = funcs.get("fdel", None)
args["doc"] = funcs.get("doc", None)
-
+
#print "Creating a property with"
#for key, val in args.items(): print key, value
return property(**args)
@@ -77,6 +77,9 @@ def local_property(name, null=None, mutable_null=False):
Define get/set access to per-parent-instance local storage. Uses
._<name>_value to store the value for a particular owner instance.
If the ._<name>_value attribute does not exist, returns null.
+
+ If mutable_null == True, we only release deepcopies of the null to
+ the outside world.
"""
def decorator(funcs):
if hasattr(funcs, "__call__"):
@@ -166,11 +169,16 @@ def _cmp_cached_mutable_property(self, cacher_name, property_name, value):
def defaulting_property(default=None, null=None,
- default_mutable=False,
- null_mutable=False):
+ mutable_default=False):
"""
Define a default value for get access to a property.
If the stored value is null, then default is returned.
+
+ If mutable_default == True, we only release deepcopies of the
+ default to the outside world.
+
+ null should never escape to the outside world, so don't worry
+ about it being a mutable.
"""
def decorator(funcs):
if hasattr(funcs, "__call__"):
@@ -181,17 +189,14 @@ def defaulting_property(default=None, null=None,
def _fget(self):
value = fget(self)
if value == null:
- if default_mutable == True:
+ if mutable_default == True:
return copy.deepcopy(default)
else:
return default
return value
def _fset(self, value):
if value == default:
- if null_mutable == True:
- value = copy.deepcopy(null)
- else:
- value = null
+ value = null
fset(self, value)
funcs["fget"] = _fget
funcs["fset"] = _fset
@@ -261,7 +266,7 @@ def cached_property(generator, initVal=None, mutable=False):
If the input value is no longer initVal (e.g. a value has been
loaded from disk or set with fset), that value overrides any
cached value, and this property has no effect.
-
+
When the cache flag is False and the stored value is initVal, the
generator is not cached, but is called on every fget.
@@ -270,7 +275,7 @@ def cached_property(generator, initVal=None, mutable=False):
In the case that mutable == True, all caching is disabled and the
generator is called whenever the cached value would otherwise be
- used. This avoids uncertainties in the value of stored mutables.
+ used.
"""
def decorator(funcs):
if hasattr(funcs, "__call__"):
@@ -296,7 +301,7 @@ def cached_property(generator, initVal=None, mutable=False):
def primed_property(primer, initVal=None):
"""
- Just like a generator_property, except that instead of returning a
+ Just like a cached_property, except that instead of returning a
new value and running fset to cache it, the primer performs some
background manipulation (e.g. loads data into instance.settings)
such that a _second_ pass through fget succeeds.
@@ -331,6 +336,17 @@ def change_hook_property(hook, mutable=False):
called _after_ the new value has been stored, allowing you to
change the stored value if you want.
+ In the case of mutables, things are slightly trickier. Because
+ the property-owning class has no way of knowing when the value
+ changes. We work around this by caching a private deepcopy of the
+ mutable value, and checking for changes whenever the property is
+ set (obviously) or retrieved (to check for external changes). So
+ long as you're conscientious about accessing the property after
+ making external modifications, mutability woln't be a problem.
+ t.x.append(5) # external modification
+ t.x # dummy access notices change and triggers hook
+ See testChangeHookMutableProperty for an example of the expected
+ behavior.
"""
def decorator(funcs):
if hasattr(funcs, "__call__"):
@@ -339,7 +355,10 @@ def change_hook_property(hook, mutable=False):
fset = funcs.get("fset")
name = funcs.get("name", "<unknown>")
def _fget(self, new_value=None, from_fset=False): # only used if mutable == True
- value = fget(self)
+ if from_fset == True:
+ value = new_value # compare new value with cached
+ else:
+ value = fget(self) # compare current value with cached
if _cmp_cached_mutable_property(self, "change hook property", name, value) != 0:
# there has been a change, cache new value
old_value = _get_cached_mutable_property(self, "change hook property", name)
@@ -362,7 +381,7 @@ def change_hook_property(hook, mutable=False):
funcs["fset"] = _fset
return funcs
return decorator
-
+
class DecoratorTests(unittest.TestCase):
def testLocalDoc(self):
@@ -406,7 +425,7 @@ class DecoratorTests(unittest.TestCase):
@local_property(name="DEFAULT", null=5)
def x(): return {}
t = Test()
- self.failUnless(t.x == 5, str(t.x))
+ self.failUnless(t.x == 5, str(t.x))
t.x = 'x'
self.failUnless(t.x == 'y', str(t.x))
t.x = 'y'
@@ -575,14 +594,17 @@ class DecoratorTests(unittest.TestCase):
t.x = []
self.failUnless(t.old == None, t.old)
self.failUnless(t.new == [], t.new)
+ self.failUnless(t.hook_calls == 1, t.hook_calls)
a = t.x
a.append(5)
t.x = a
self.failUnless(t.old == [], t.old)
self.failUnless(t.new == [5], t.new)
+ self.failUnless(t.hook_calls == 2, t.hook_calls)
t.x = []
self.failUnless(t.old == [5], t.old)
self.failUnless(t.new == [], t.new)
+ self.failUnless(t.hook_calls == 3, t.hook_calls)
# now append without reassigning. this doesn't trigger the
# change, since we don't ever set t.x, only get it and mess
# with it. It does, however, update our t.new, since t.new =
@@ -590,25 +612,26 @@ class DecoratorTests(unittest.TestCase):
t.x.append(5)
self.failUnless(t.old == [5], t.old)
self.failUnless(t.new == [5], t.new)
+ self.failUnless(t.hook_calls == 3, t.hook_calls)
# however, the next t.x get _will_ notice the change...
a = t.x
self.failUnless(t.old == [], t.old)
self.failUnless(t.new == [5], t.new)
- self.failUnless(t.hook_calls == 6, t.hook_calls)
+ self.failUnless(t.hook_calls == 4, t.hook_calls)
t.x.append(6) # this append(6) is not noticed yet
self.failUnless(t.old == [], t.old)
self.failUnless(t.new == [5,6], t.new)
- self.failUnless(t.hook_calls == 6, t.hook_calls)
+ self.failUnless(t.hook_calls == 4, t.hook_calls)
# this append(7) is not noticed, but the t.x get causes the
# append(6) to be noticed
t.x.append(7)
self.failUnless(t.old == [5], t.old)
self.failUnless(t.new == [5,6,7], t.new)
- self.failUnless(t.hook_calls == 7, t.hook_calls)
+ self.failUnless(t.hook_calls == 5, t.hook_calls)
a = t.x # now the append(7) is noticed
self.failUnless(t.old == [5,6], t.old)
self.failUnless(t.new == [5,6,7], t.new)
- self.failUnless(t.hook_calls == 8, t.hook_calls)
+ self.failUnless(t.hook_calls == 6, t.hook_calls)
suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests)
diff --git a/libbe/rcs.py b/libbe/rcs.py
index 3bf8c9d..1e1cfa7 100644
--- a/libbe/rcs.py
+++ b/libbe/rcs.py
@@ -339,11 +339,15 @@ class RCS(object):
self.add(path)
else:
self.update(path)
- def mkdir(self, path, allow_no_rcs=False):
+ def mkdir(self, path, allow_no_rcs=False, check_parents=True):
"""
Create (if neccessary) a directory at path under version
control.
"""
+ if check_parents == True:
+ parent = os.path.dirname(path)
+ if not os.path.exists(parent): # recurse through parents
+ self.mkdir(parent, allow_no_rcs, check_parents)
if not os.path.exists(path):
os.mkdir(path)
if self._use_rcs(path, allow_no_rcs):
@@ -351,7 +355,9 @@ class RCS(object):
else:
assert os.path.isdir(path)
if self._use_rcs(path, allow_no_rcs):
- self.update(path)
+ #self.update(path)# Don't update directories. Changing files
+ pass # underneath them should be sufficient.
+
def duplicate_repo(self, revision=None):
"""
Get the repository as it was in a given revision.
diff --git a/libbe/settings_object.py b/libbe/settings_object.py
index 9bc0a2f..dde247f 100644
--- a/libbe/settings_object.py
+++ b/libbe/settings_object.py
@@ -96,7 +96,7 @@ def versioned_property(name, doc,
require_save=False):
"""
Combine the common decorators in a single function.
-
+
Use zero or one (but not both) of default or generator, since a
working default will keep the generator from functioning. Use the
default if you know what you want the default value to be at
@@ -104,22 +104,29 @@ def versioned_property(name, doc,
determine a valid default at run time. If both default and
generator are None, then the property will be a defaulting
property which defaults to None.
-
+
allowed and check_fn have a similar relationship, although you can
use both of these if you want. allowed compares the proposed
value against a list determined at 'coding time' and check_fn
allows more flexible comparisons to take place at run time.
-
+
Set require_save to True if you want to save the default/generated
value for a property, to protect against future changes. E.g., we
currently expect all comments to be 'text/plain' but in the future
we may want to default to 'text/html'. If we don't want the old
comments to be interpreted as 'text/html', we would require that
the content type be saved.
-
+
change_hook, primer, settings_properties, and
required_saved_properties are only options to get their defaults
into our local scope. Don't mess with them.
+
+ Set mutable=True if:
+ * default is a mutable
+ * your generator function may return mutables
+ * 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.
"""
settings_properties.append(name)
if require_save == True:
@@ -128,7 +135,7 @@ def versioned_property(name, doc,
fulldoc = doc
if default != None or generator == None:
defaulting = defaulting_property(default=default, null=EMPTY,
- default_mutable=mutable)
+ mutable_default=mutable)
fulldoc += "\n\nThis property defaults to %s." % default
if generator != None:
cached = cached_property(generator=generator, initVal=EMPTY,
@@ -180,7 +187,7 @@ class SavedSettingsObject(object):
# Override. Must call ._setup_saved_settings() after loading.
self.settings = {}
self._setup_saved_settings()
-
+
def _setup_saved_settings(self, flag_as_loaded=True):
"""
To be run after setting self.settings up from disk. Marks all
@@ -208,7 +215,7 @@ class SavedSettingsObject(object):
for k in self.required_saved_properties:
settings[k] = getattr(self, self._setting_name_to_attr_name(k))
return settings
-
+
def clear_cached_setting(self, setting=None):
"If setting=None, clear *all* cached settings"
if setting != None:
@@ -392,19 +399,17 @@ class SavedSettingsObjectTests(unittest.TestCase):
self.failUnless(SAVES == [
"'None' -> '<class 'libbe.settings_object.EMPTY'>'",
"'<class 'libbe.settings_object.EMPTY'>' -> '[]'",
- "'<class 'libbe.settings_object.EMPTY'>' -> '[]'" # <- TODO. Where did this come from?
], SAVES)
self.failUnless(t.settings["List-type"] == [5],t.settings["List-type"])
self.failUnless(SAVES == [ # the append(5) has not yet been saved
"'None' -> '<class 'libbe.settings_object.EMPTY'>'",
"'<class 'libbe.settings_object.EMPTY'>' -> '[]'",
- "'<class 'libbe.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.
"'None' -> '<class 'libbe.settings_object.EMPTY'>'",
"'<class 'libbe.settings_object.EMPTY'>' -> '[]'",
- "'<class 'libbe.settings_object.EMPTY'>' -> '[]'",
"'[]' -> '[5]'"
], SAVES)
diff --git a/libbe/utility.py b/libbe/utility.py
index f27d7eb..3df06b4 100644
--- a/libbe/utility.py
+++ b/libbe/utility.py
@@ -23,7 +23,6 @@ import time
import types
import doctest
-
def search_parent_directories(path, filename):
"""
Find the file (or directory) named filename in path or in any
@@ -106,5 +105,25 @@ def time_to_gmtime(str_time):
time_val = str_to_time(str_time)
return time_to_str(time_val)
+def iterable_full_of_strings(value, alternative=None):
+ """
+ Require an iterable full of strings.
+ >>> iterable_full_of_strings([])
+ True
+ >>> iterable_full_of_strings(["abc", "def", u"hij"])
+ True
+ >>> iterable_full_of_strings(["abc", None, u"hij"])
+ False
+ >>> iterable_full_of_strings(None, alternative=None)
+ True
+ """
+ if value == alternative:
+ return True
+ elif not hasattr(value, "__iter__"):
+ return False
+ for x in value:
+ if type(x) not in types.StringTypes:
+ return False
+ return True
suite = doctest.DocTestSuite()