aboutsummaryrefslogtreecommitdiffstats
path: root/libbe
diff options
context:
space:
mode:
Diffstat (limited to 'libbe')
-rw-r--r--libbe/arch.py41
-rw-r--r--libbe/beuuid.py22
-rw-r--r--libbe/bug.py75
-rw-r--r--libbe/bugdir.py135
-rw-r--r--libbe/bzr.py54
-rw-r--r--libbe/cmdutil.py30
-rw-r--r--libbe/comment.py62
-rw-r--r--libbe/config.py22
-rw-r--r--libbe/darcs.py51
-rw-r--r--libbe/diff.py71
-rw-r--r--libbe/editor.py25
-rw-r--r--libbe/encoding.py23
-rw-r--r--libbe/git.py37
-rw-r--r--libbe/hg.py33
-rw-r--r--libbe/mapfile.py22
-rw-r--r--libbe/plugin.py22
-rw-r--r--libbe/properties.py80
-rw-r--r--libbe/rcs.py95
-rw-r--r--libbe/settings_object.py46
-rw-r--r--libbe/tree.py56
-rw-r--r--libbe/utility.py43
21 files changed, 634 insertions, 411 deletions
diff --git a/libbe/arch.py b/libbe/arch.py
index 8f7603b..2f45aa9 100644
--- a/libbe/arch.py
+++ b/libbe/arch.py
@@ -3,19 +3,19 @@
# James Rowe <jnrowe@ukfsn.org>
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import codecs
import os
@@ -260,16 +260,17 @@ class Arch(RCS):
else:
status,output,error = \
self._u_invoke_client("get", revision,directory)
- def _rcs_commit(self, commitfile):
+ def _rcs_commit(self, commitfile, allow_empty=False):
+ if allow_empty == False:
+ # arch applies empty commits without complaining, so check first
+ status,output,error = self._u_invoke_client("changes",expect=(0,1))
+ if status == 0:
+ raise rcs.EmptyCommit()
summary,body = self._u_parse_commitfile(commitfile)
- #status,output,error = self._invoke_client("make-log")
- if body == None:
- status,output,error \
- = self._u_invoke_client("commit","--summary",summary)
- else:
- status,output,error \
- = self._u_invoke_client("commit","--summary",summary,
- "--log-message",body)
+ args = ["commit", "--summary", summary]
+ if body != None:
+ args.extend(["--log-message",body])
+ status,output,error = self._u_invoke_client(*args)
revision = None
revline = re.compile("[*] committed (.*)")
match = revline.search(output)
diff --git a/libbe/beuuid.py b/libbe/beuuid.py
index 020ea9f..bc47208 100644
--- a/libbe/beuuid.py
+++ b/libbe/beuuid.py
@@ -1,18 +1,18 @@
# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
"""
Backwards compatibility support for Python 2.4. Once people give up
on 2.4 ;), the uuid call should be merged into bugdir.py
diff --git a/libbe/bug.py b/libbe/bug.py
index 8cb8a0a..c1e5481 100644
--- a/libbe/bug.py
+++ b/libbe/bug.py
@@ -2,19 +2,19 @@
# Thomas Habets <thomas@habets.pp.se>
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import os
import os.path
import errno
@@ -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):
@@ -490,8 +494,25 @@ cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
# chronological rankings (newer < older)
cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
+def cmp_comments(bug_1, bug_2):
+ """
+ Compare two bugs' comments lists. Doesn't load any new comments,
+ so you should call each bug's .load_comments() first if you want a
+ full comparison.
+ """
+ comms_1 = sorted(bug_1.comments(), key = lambda comm : comm.uuid)
+ comms_2 = sorted(bug_2.comments(), key = lambda comm : comm.uuid)
+ result = cmp(len(comms_1), len(comms_2))
+ if result != 0:
+ return result
+ for c_1,c_2 in zip(comms_1, comms_2):
+ result = cmp(c_1, c_2)
+ if result != 0:
+ return result
+ return 0
+
DEFAULT_CMP_FULL_CMP_LIST = \
- (cmp_status,cmp_severity,cmp_assigned,cmp_time,cmp_creator)
+ (cmp_status,cmp_severity,cmp_assigned,cmp_time,cmp_creator,cmp_comments)
class BugCompoundComparator (object):
def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index fed9aa3..6e020ee 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -4,19 +4,19 @@
# Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import os
import os.path
import errno
@@ -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/bzr.py b/libbe/bzr.py
index 56a1648..d7cd1e5 100644
--- a/libbe/bzr.py
+++ b/libbe/bzr.py
@@ -3,19 +3,19 @@
# Marien Zwart <marienz@gentoo.org>
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import os
import re
@@ -71,9 +71,21 @@ class Bzr(RCS):
else:
self._u_invoke_client("branch", "--revision", revision,
".", directory)
- def _rcs_commit(self, commitfile):
- status,output,error = self._u_invoke_client("commit", "--unchanged",
- "--file", commitfile)
+ def _rcs_commit(self, commitfile, allow_empty=False):
+ args = ["commit", "--file", commitfile]
+ if allow_empty == True:
+ args.append("--unchanged")
+ status,output,error = self._u_invoke_client(*args)
+ else:
+ kwargs = {"expect":(0,3)}
+ status,output,error = self._u_invoke_client(*args, **kwargs)
+ if status != 0:
+ strings = ["ERROR: no changes to commit.", # bzr 1.3.1
+ "ERROR: No changes to commit."] # bzr 1.15.1
+ if self._u_any_in_string(strings, error) == True:
+ raise rcs.EmptyCommit()
+ else:
+ raise rcs.CommandError(args, status, error)
revision = None
revline = re.compile("Committed revision (.*)[.]")
match = revline.search(error)
@@ -81,20 +93,6 @@ class Bzr(RCS):
assert len(match.groups()) == 1
revision = match.groups()[0]
return revision
- def postcommit(self):
- try:
- self._u_invoke_client('merge')
- except rcs.CommandError, e:
- if ('No merge branch known or specified' in e.err_str or
- 'No merge location known or specified' in e.err_str):
- pass
- else:
- self._u_invoke_client('revert', '--no-backup',
- directory=directory)
- self._u_invoke_client('resolve', '--all', directory=directory)
- raise
- if len(self._u_invoke_client('status', directory=directory)[1]) > 0:
- self.commit('Merge from upstream')
rcs.make_rcs_testcase_subclasses(Bzr, sys.modules[__name__])
diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py
index 7589241..853a75a 100644
--- a/libbe/cmdutil.py
+++ b/libbe/cmdutil.py
@@ -2,19 +2,19 @@
# Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import glob
import optparse
import os
@@ -73,8 +73,10 @@ def get_command(command_name):
def execute(cmd, args):
enc = encoding.get_encoding()
cmd = get_command(cmd)
- cmd.execute([a.decode(enc) for a in args])
- return 0
+ ret = cmd.execute([a.decode(enc) for a in args])
+ if ret == None:
+ ret = 0
+ return ret
def help(cmd=None, parser=None):
if cmd != None:
@@ -163,7 +165,7 @@ def default_complete(options, args, parser, bugid_args={}):
"""
for option,value in option_value_pairs(options, parser):
if value == "--complete":
- raise cmdutil.GetCompletions()
+ raise GetCompletions()
if len(bugid_args.keys()) > 0:
max_pos_arg = max(bugid_args.keys())
else:
diff --git a/libbe/comment.py b/libbe/comment.py
index 68deaf3..3249e8b 100644
--- a/libbe/comment.py
+++ b/libbe/comment.py
@@ -3,20 +3,19 @@
# Thomas Habets <thomas@habets.pp.se>
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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
+# 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.
import base64
import os
import os.path
@@ -124,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
@@ -131,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()
@@ -220,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):
"""
@@ -250,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):
@@ -259,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):
"""
@@ -431,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)
@@ -462,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/config.py b/libbe/config.py
index fafb4f0..5e343b9 100644
--- a/libbe/config.py
+++ b/libbe/config.py
@@ -1,19 +1,19 @@
# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import ConfigParser
import codecs
import locale
diff --git a/libbe/darcs.py b/libbe/darcs.py
index 43af99a..e7132c0 100644
--- a/libbe/darcs.py
+++ b/libbe/darcs.py
@@ -1,18 +1,18 @@
# Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import codecs
import os
@@ -131,24 +131,25 @@ class Darcs(RCS):
RCS._rcs_duplicate_repo(self, directory, revision)
else:
self._u_invoke_client("put", "--to-patch", revision, directory)
- def _rcs_commit(self, commitfile):
+ def _rcs_commit(self, commitfile, allow_empty=False):
id = self.get_user_id()
if '@' not in id:
id = "%s <%s@invalid.com>" % (id, id)
- # Darcs doesn't like commitfiles without trailing endlines.
- f = codecs.open(commitfile, 'r', self.encoding)
- contents = f.read()
- f.close()
- if contents[-1] != '\n':
- f = codecs.open(commitfile, 'a', self.encoding)
- f.write('\n')
- f.close()
- status,output,error = self._u_invoke_client('record', '--all',
- '--author', id,
- '--logfile', commitfile)
+ args = ['record', '--all', '--author', id, '--logfile', commitfile]
+ status,output,error = self._u_invoke_client(*args)
+ empty_strings = ["No changes!"]
revision = None
-
- revline = re.compile("Finished recording patch '(.*)'")
+ if self._u_any_in_string(empty_strings, output) == True:
+ if allow_empty == False:
+ raise rcs.EmptyCommit()
+ else: # we need a extra call to get the current revision
+ args = ["changes", "--last=1", "--xml"]
+ status,output,error = self._u_invoke_client(*args)
+ revline = re.compile("[ \t]*<name>(.*)</name>")
+ # note that darcs does _not_ make an empty revision.
+ # this returns the last non-empty revision id...
+ else:
+ revline = re.compile("Finished recording patch '(.*)'")
match = revline.search(output)
assert match != None, output+error
assert len(match.groups()) == 1
diff --git a/libbe/diff.py b/libbe/diff.py
index 13efc9f..ba48efc 100644
--- a/libbe/diff.py
+++ b/libbe/diff.py
@@ -1,25 +1,25 @@
# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
"""Compare two bug trees"""
from libbe import cmdutil, bugdir, bug
from libbe.utility import time_to_str
import doctest
-def diff(old_bugdir, new_bugdir):
+def bug_diffs(old_bugdir, new_bugdir):
added = []
removed = []
modified = []
@@ -27,6 +27,8 @@ def diff(old_bugdir, new_bugdir):
old_bug = old_bugdir.bug_from_uuid(uuid)
try:
new_bug = new_bugdir.bug_from_uuid(uuid)
+ old_bug.load_comments()
+ new_bug.load_comments()
if old_bug != new_bug:
modified.append((old_bug, new_bug))
except KeyError:
@@ -37,26 +39,34 @@ def diff(old_bugdir, new_bugdir):
added.append(new_bug)
return (removed, modified, added)
-def diff_report(diff_data, bug_dir):
- (removed, modified, added) = diff_data
+def diff_report(bug_diffs_data, old_bugdir, new_bugdir):
+ bugs_removed,bugs_modified,bugs_added = bug_diffs_data
def modified_cmp(left, right):
return bug.cmp_severity(left[1], right[1])
- added.sort(bug.cmp_severity)
- removed.sort(bug.cmp_severity)
- modified.sort(modified_cmp)
+ bugs_added.sort(bug.cmp_severity)
+ bugs_removed.sort(bug.cmp_severity)
+ bugs_modified.sort(modified_cmp)
lines = []
- if len(added) > 0:
+ if old_bugdir.settings != new_bugdir.settings:
+ bugdir_settings = sorted(new_bugdir.settings_properties)
+ bugdir_settings.remove("rcs_name") # tweaked by bugdir.duplicate_bugdir
+ change_list = change_lines(old_bugdir, new_bugdir, bugdir_settings)
+ if len(change_list) > 0:
+ lines.append("Modified bug directory:")
+ change_strings = ["%s: %s -> %s" % f for f in change_list]
+ lines.extend(change_strings)
+ lines.append("")
+ if len(bugs_added) > 0:
lines.append("New bug reports:")
- for bg in added:
+ for bg in bugs_added:
lines.extend(bg.string(shortlist=True).splitlines())
lines.append("")
-
- if len(modified) > 0:
+ if len(bugs_modified) > 0:
printed = False
- for old_bug, new_bug in modified:
- change_str = bug_changes(old_bug, new_bug, bug_dir)
+ for old_bug, new_bug in bugs_modified:
+ change_str = bug_changes(old_bug, new_bug)
if change_str is None:
continue
if not printed:
@@ -65,14 +75,13 @@ def diff_report(diff_data, bug_dir):
lines.extend(change_str.splitlines())
if printed == True:
lines.append("")
-
- if len(removed) > 0:
+ if len(bugs_removed) > 0:
lines.append("Removed bug reports:")
- for bg in removed:
+ for bg in bugs_removed:
lines.extend(bg.string(shortlist=True).splitlines())
lines.append("")
- return '\n'.join(lines)
+ return "\n".join(lines).rstrip("\n")
def change_lines(old, new, attributes):
change_list = []
@@ -86,13 +95,13 @@ def change_lines(old, new, attributes):
else:
return None
-def bug_changes(old, new, bugs):
- change_list = change_lines(old, new, ("time", "creator", "severity",
- "target", "summary", "status", "assigned"))
+def bug_changes(old, new):
+ bug_settings = sorted(new.settings_properties)
+ change_list = change_lines(old, new, bug_settings)
+ change_strings = ["%s: %s -> %s" % f for f in change_list]
old_comment_ids = [c.uuid for c in old.comments()]
new_comment_ids = [c.uuid for c in new.comments()]
- change_strings = ["%s: %s -> %s" % f for f in change_list]
for comment_id in new_comment_ids:
if comment_id not in old_comment_ids:
summary = comment_summary(new.comment_from_uuid(comment_id), "new")
diff --git a/libbe/editor.py b/libbe/editor.py
index 4bab0fa..93144b8 100644
--- a/libbe/editor.py
+++ b/libbe/editor.py
@@ -1,20 +1,19 @@
# Bugs Everywhere, a distributed bugtracker
# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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
+# 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.
import codecs
import locale
@@ -32,7 +31,7 @@ class CantFindEditor(Exception):
Exception.__init__(self, "Can't find editor to get string from")
def editor_string(comment=None, encoding=None):
- """Invokes the editor, and returns the user_produced text as a string
+ """Invokes the editor, and returns the user-produced text as a string
>>> if "EDITOR" in os.environ:
... del os.environ["EDITOR"]
diff --git a/libbe/encoding.py b/libbe/encoding.py
index 84c360a..d603602 100644
--- a/libbe/encoding.py
+++ b/libbe/encoding.py
@@ -1,20 +1,19 @@
# Bugs Everywhere, a distributed bugtracker
# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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
+# 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.
import codecs
import locale
import sys
diff --git a/libbe/git.py b/libbe/git.py
index 31bbe32..2f9ffa9 100644
--- a/libbe/git.py
+++ b/libbe/git.py
@@ -2,19 +2,19 @@
# Chris Ball <cjb@laptop.org>
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import os
import re
@@ -93,9 +93,18 @@ class Git(RCS):
#self._u_invoke_client("archive", revision, directory) # makes tarball
self._u_invoke_client("clone", "--no-checkout",".",directory)
self._u_invoke_client("checkout", revision, directory=directory)
- def _rcs_commit(self, commitfile):
- status,output,error = self._u_invoke_client('commit', '-a',
- '-F', commitfile)
+ def _rcs_commit(self, commitfile, allow_empty=False):
+ args = ['commit', '--all', '--file', commitfile]
+ if allow_empty == True:
+ args.append("--allow-empty")
+ status,output,error = self._u_invoke_client(*args)
+ else:
+ kwargs = {"expect":(0,1)}
+ status,output,error = self._u_invoke_client(*args, **kwargs)
+ strings = ["nothing to commit",
+ "nothing added to commit"]
+ if self._u_any_in_string(strings, output) == True:
+ raise rcs.EmptyCommit()
revision = None
revline = re.compile("(.*) (.*)[:\]] (.*)")
match = revline.search(output)
diff --git a/libbe/hg.py b/libbe/hg.py
index 30b0470..a20eeb5 100644
--- a/libbe/hg.py
+++ b/libbe/hg.py
@@ -2,19 +2,19 @@
# Ben Finney <ben+python@benfinney.id.au>
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import os
import re
@@ -58,7 +58,7 @@ class Hg(RCS):
def _rcs_add(self, path):
self._u_invoke_client("add", path)
def _rcs_remove(self, path):
- self._u_invoke_client("rm", path)
+ self._u_invoke_client("rm", "--force", path)
def _rcs_update(self, path):
pass
def _rcs_get_file_contents(self, path, revision=None, binary=False):
@@ -73,8 +73,13 @@ class Hg(RCS):
return RCS._rcs_duplicate_repo(self, directory, revision)
else:
self._u_invoke_client("archive", "--rev", revision, directory)
- def _rcs_commit(self, commitfile):
- self._u_invoke_client('commit', '--logfile', commitfile)
+ def _rcs_commit(self, commitfile, allow_empty=False):
+ args = ['commit', '--logfile', commitfile]
+ status,output,error = self._u_invoke_client(*args)
+ if allow_empty == False:
+ strings = ["nothing changed"]
+ if self._u_any_in_string(strings, output) == True:
+ raise rcs.EmptyCommit()
status,output,error = self._u_invoke_client('identify')
revision = None
revline = re.compile("(.*) tip")
diff --git a/libbe/mapfile.py b/libbe/mapfile.py
index b183bfe..b959d76 100644
--- a/libbe/mapfile.py
+++ b/libbe/mapfile.py
@@ -1,19 +1,19 @@
# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import yaml
import os.path
import errno
diff --git a/libbe/plugin.py b/libbe/plugin.py
index a21ba91..0545fd7 100644
--- a/libbe/plugin.py
+++ b/libbe/plugin.py
@@ -2,19 +2,19 @@
# Marien Zwart <marienz@gentoo.org>
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import os
import os.path
import sys
diff --git a/libbe/properties.py b/libbe/properties.py
index 37204d6..144220b 100644
--- a/libbe/properties.py
+++ b/libbe/properties.py
@@ -1,18 +1,19 @@
# Bugs Everywhere - a distributed bugtracker
# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
#
-# 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 3 of the License, or
-# (at your option) any later version.
+# 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.
+# 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, see <http://www.gnu.org/licenses/>.
+# 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.
"""
This module provides a series of useful decorators for defining
@@ -51,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)
@@ -76,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__"):
@@ -165,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__"):
@@ -180,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
@@ -260,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.
@@ -269,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__"):
@@ -295,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.
@@ -330,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__"):
@@ -338,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)
@@ -361,7 +381,7 @@ def change_hook_property(hook, mutable=False):
funcs["fset"] = _fset
return funcs
return decorator
-
+
class DecoratorTests(unittest.TestCase):
def testLocalDoc(self):
@@ -405,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'
@@ -574,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 =
@@ -589,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 844920a..294b8e0 100644
--- a/libbe/rcs.py
+++ b/libbe/rcs.py
@@ -4,19 +4,19 @@
# Chris Ball <cjb@laptop.org>
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
from subprocess import Popen, PIPE
import codecs
@@ -61,10 +61,13 @@ def installed_rcs():
class CommandError(Exception):
- def __init__(self, err_str, status):
- Exception.__init__(self, "Command failed (%d): %s" % (status, err_str))
- self.err_str = err_str
+ def __init__(self, command, status, err_str):
+ strerror = ["Command failed (%d):\n %s\n" % (status, err_str),
+ "while executing\n %s" % command]
+ Exception.__init__(self, "\n".join(strerror))
+ self.command = command
self.status = status
+ self.err_str = err_str
class SettingIDnotSupported(NotImplementedError):
pass
@@ -86,6 +89,10 @@ class NoSuchFile(Exception):
path = os.path.abspath(os.path.join(root, pathname))
Exception.__init__(self, "No such file: %s" % path)
+class EmptyCommit(Exception):
+ def __init__(self):
+ Exception.__init__(self, "No changes to commit")
+
def new():
return RCS()
@@ -186,7 +193,7 @@ class RCS(object):
if binary == False:
f = codecs.open(os.path.join(self.rootdir, path), "r", self.encoding)
else:
- f = open(path, "rb")
+ f = open(os.path.join(self.rootdir, path), "rb")
contents = f.read()
f.close()
return contents
@@ -197,11 +204,14 @@ class RCS(object):
dir specifies a directory to create the duplicate in.
"""
shutil.copytree(self.rootdir, directory, True)
- def _rcs_commit(self, commitfile):
+ def _rcs_commit(self, commitfile, allow_empty=False):
"""
Commit the current working directory, using the contents of
commitfile as the comment. Return the name of the old
- revision.
+ revision (or None if commits are not supported).
+
+ If allow_empty == False, raise EmptyCommit if there are no
+ changes to commit.
"""
return None
def installed(self):
@@ -329,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):
@@ -341,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.
@@ -364,30 +380,55 @@ class RCS(object):
shutil.rmtree(self._duplicateBasedir)
self._duplicateBasedir = None
self._duplicateDirname = None
- def commit(self, summary, body=None):
+ def commit(self, summary, body=None, allow_empty=False):
"""
Commit the current working directory, with a commit message
string summary and body. Return the name of the old revision
(or None if versioning is not supported).
+
+ If allow_empty == False (the default), raise EmptyCommit if
+ there are no changes to commit.
"""
+ summary = summary.strip()+'\n'
if body is not None:
- summary += '\n' + body
+ summary += '\n' + body.strip() + '\n'
descriptor, filename = tempfile.mkstemp()
revision = None
try:
temp_file = os.fdopen(descriptor, 'wb')
temp_file.write(summary)
temp_file.flush()
- revision = self._rcs_commit(filename)
+ self.precommit()
+ revision = self._rcs_commit(filename, allow_empty=allow_empty)
temp_file.close()
+ self.postcommit()
finally:
os.remove(filename)
return revision
- def precommit(self, directory):
+ def precommit(self):
+ """
+ Executed before all attempted commits.
+ """
pass
- def postcommit(self, directory):
+ def postcommit(self):
+ """
+ Only executed after successful commits.
+ """
pass
+ def _u_any_in_string(self, list, string):
+ """
+ Return True if any of the strings in list are in string.
+ Otherwise return False.
+ """
+ for list_string in list:
+ if list_string in string:
+ return True
+ return False
def _u_invoke(self, args, stdin=None, expect=(0,), cwd=None):
+ """
+ expect should be a tuple of allowed exit codes. cwd should be
+ the directory from which the command will be executed.
+ """
if cwd == None:
cwd = self.rootdir
if self.verboseInvoke == True:
@@ -400,15 +441,13 @@ class RCS(object):
q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE,
shell=True, cwd=cwd)
except OSError, e :
- strerror = "%s\nwhile executing %s" % (e.args[1], args)
- raise CommandError(strerror, e.args[0])
+ raise CommandError(args, e.args[0], e)
output, error = q.communicate(input=stdin)
status = q.wait()
if self.verboseInvoke == True:
print >> sys.stderr, "%d\n%s%s" % (status, output, error)
if status not in expect:
- strerror = "%s\nwhile executing %s\n%s" % (args[1], args, error)
- raise CommandError(strerror, status)
+ raise CommandError(args, status, error)
return status, output, error
def _u_invoke_client(self, *args, **kwargs):
directory = kwargs.get('directory',None)
diff --git a/libbe/settings_object.py b/libbe/settings_object.py
index 1dadd0a..dde247f 100644
--- a/libbe/settings_object.py
+++ b/libbe/settings_object.py
@@ -1,18 +1,19 @@
# Bugs Everywhere - a distributed bugtracker
# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
#
-# 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 3 of the License, or
-# (at your option) any later version.
+# 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.
+# 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, see <http://www.gnu.org/licenses/>.
+# 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.
"""
This module provides a base class implementing settings-dict based
@@ -95,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
@@ -103,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:
@@ -127,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,
@@ -179,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
@@ -207,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:
@@ -391,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/tree.py b/libbe/tree.py
index 54b927e..45ae085 100644
--- a/libbe/tree.py
+++ b/libbe/tree.py
@@ -1,20 +1,19 @@
# Bugs Everywhere, a distributed bugtracker
# Copyright (C) 2008-2009 W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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
+# 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.
import doctest
@@ -36,7 +35,7 @@ class Tree(list):
>>> a = Tree(); a.n = "a"
>>> a.append(c)
>>> a.append(b)
-
+
>>> a.branch_len()
5
>>> a.sort(key=lambda node : -node.branch_len())
@@ -45,7 +44,7 @@ class Tree(list):
>>> a.sort(key=lambda node : node.branch_len())
>>> "".join([node.n for node in a.traverse()])
'abdgcefhi'
- >>> "".join([node.n for node in a.traverse(depthFirst=False)])
+ >>> "".join([node.n for node in a.traverse(depth_first=False)])
'abcdefghi'
>>> for depth,node in a.thread():
... print "%*s" % (2*depth+1, node.n)
@@ -69,7 +68,18 @@ class Tree(list):
f
h
i
+ >>> a.has_descendant(g)
+ True
+ >>> c.has_descendant(g)
+ False
+ >>> a.has_descendant(a)
+ False
+ >>> a.has_descendant(a, match_self=True)
+ True
"""
+ def __eq__(self, other):
+ return id(self) == id(other)
+
def branch_len(self):
"""
Exhaustive search every time == SLOW.
@@ -98,11 +108,11 @@ class Tree(list):
for child in self:
child.sort(*args, **kwargs)
- def traverse(self, depthFirst=True):
+ def traverse(self, depth_first=True):
"""
Note: you might want to sort() your tree first.
"""
- if depthFirst == True:
+ if depth_first == True:
yield self
for child in self:
for descendant in child.traverse():
@@ -120,7 +130,7 @@ class Tree(list):
When flatten==False, the depth of any node is one greater than
the depth of its parent. That way the inheritance is
explicit, but you can end up with highly indented threads.
-
+
When flatten==True, the depth of any node is only greater than
the depth of its parent when there is a branch, and the node
is not the last child. This can lead to ancestry ambiguity,
@@ -139,8 +149,8 @@ class Tree(list):
stack = [] # ancestry of the current node
if flatten == True:
depthDict = {}
-
- for node in self.traverse(depthFirst=True):
+
+ for node in self.traverse(depth_first=True):
while len(stack) > 0 \
and id(node) not in [id(c) for c in stack[-1]]:
stack.pop(-1)
@@ -158,4 +168,12 @@ class Tree(list):
yield (depth,node)
stack.append(node)
+ def has_descendant(self, descendant, depth_first=True, match_self=False):
+ if descendant == self:
+ return match_self
+ for d in self.traverse(depth_first):
+ if descendant == d:
+ return True
+ return False
+
suite = doctest.DocTestSuite()
diff --git a/libbe/utility.py b/libbe/utility.py
index e16b94a..3df06b4 100644
--- a/libbe/utility.py
+++ b/libbe/utility.py
@@ -1,19 +1,19 @@
# Copyright (C) 2005-2009 Aaron Bentley and Panometrics, Inc.
# W. Trevor King <wking@drexel.edu>
#
-# 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 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.
+# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# 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.
import calendar
import codecs
import os
@@ -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()