aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--libbe/bug.py26
-rw-r--r--libbe/bugdir.py32
-rw-r--r--libbe/comment.py228
3 files changed, 209 insertions, 77 deletions
diff --git a/libbe/bug.py b/libbe/bug.py
index e04d4ee..5f0429e 100644
--- a/libbe/bug.py
+++ b/libbe/bug.py
@@ -176,15 +176,15 @@ class Bug(object):
self.time_string = utility.time_to_str(value)
time = property(fget=_get_time,
fset=_set_time,
- doc="An integere version of .time_string")
+ doc="An integer version of .time_string")
@_versioned_property(name="summary",
doc="A one-line bug description")
def summary(): return {}
- def _get_comment_root(self):
+ def _get_comment_root(self, load_full=False):
if self.sync_with_disk:
- return comment.loadComments(self)
+ return comment.loadComments(self, load_full=load_full)
else:
return comment.Comment(self, uuid=comment.INVALID_UUID)
@@ -212,7 +212,6 @@ class Bug(object):
self.settings = {}
if from_disk == True:
self.sync_with_disk = True
- #self.load(load_comments=load_comments)
else:
self.sync_with_disk = False
if uuid == None:
@@ -286,13 +285,18 @@ class Bug(object):
self.settings[property] = EMPTY
self._settings_loaded = True
- def load_comments(self):
- # Clear _comment_root, so _get_comment_root returns a fresh
- # version. Turn of syncing temporarily so we don't write our
- # blank comment tree to disk.
- self.sync_with_disk = False
- self._comment_root = None
- self.sync_with_disk = True
+ def load_comments(self, load_full=True):
+ if load_full == True:
+ # Force a complete load of the whole comment tree
+ self.comment_root = self._get_comment_root(load_full=True)
+ else:
+ # Setup for fresh lazy-loading. Clear _comment_root, so
+ # _get_comment_root returns a fresh version. Turn of
+ # syncing temporarily so we don't write our blank comment
+ # tree to disk.
+ self.sync_with_disk = False
+ self.comment_root = None
+ self.sync_with_disk = True
def save_settings(self):
assert self.summary != None, "Can't save blank bug"
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index 6bb6a43..1142e3d 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -409,6 +409,7 @@ only saved if ._save_user_id == True""")
def _clear_bugs(self):
while len(self) > 0:
self.pop()
+ self._bug_map_gen()
def _load_bug(self, uuid):
bg = bug.Bug(bugdir=self, uuid=uuid, from_disk=True)
@@ -567,6 +568,37 @@ class BugDirTestCase(unittest.TestCase):
self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime))
self.bugdir.save()
self.versionTest()
+ def testComments(self):
+ 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()
+ bug = self.bugdir.bug_from_uuid("a")
+ bug.load_comments()
+ self.failUnless(len(bug.comment_root)==1, len(bug.comment_root))
+ for index,comment in enumerate(bug.comments()):
+ if index == 0:
+ repLoaded = comment
+ self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid)
+ self.failUnless(comment.sync_with_disk == True,
+ comment.sync_with_disk)
+ #load_settings()
+ self.failUnless(comment.content_type == "text/plain",
+ comment.content_type)
+ self.failUnless(repLoaded.settings["Content-type"]=="text/plain",
+ repLoaded.settings)
+ self.failUnless(repLoaded.body == "Ants are small.",
+ repLoaded.body)
+ elif index == 1:
+ self.failUnless(comment.in_reply_to == repLoaded.uuid,
+ repLoaded.uuid)
+ self.failUnless(comment.body == "And they have six legs.",
+ comment.body)
+ else:
+ self.failIf(True, "Invalid comment: %d\n%s" % (index, comment))
unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase)
suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()])
diff --git a/libbe/comment.py b/libbe/comment.py
index ab0973d..0fd871c 100644
--- a/libbe/comment.py
+++ b/libbe/comment.py
@@ -23,10 +23,22 @@ import textwrap
import doctest
from beuuid import uuid_gen
+from properties import Property, doc_property, local_property, \
+ defaulting_property, checked_property, cached_property, \
+ primed_property, change_hook_property, settings_property
import mapfile
from tree import Tree
import utility
+
+class InvalidShortname(KeyError):
+ def __init__(self, shortname, shortnames):
+ msg = "Invalid shortname %s\n%s" % (shortname, shortnames)
+ KeyError.__init__(self, msg)
+ self.shortname = shortname
+ self.shortnames = shortnames
+
+
INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!"
def _list_to_root(comments, bug):
@@ -55,7 +67,11 @@ def _list_to_root(comments, bug):
dummy_root.extend(root_comments)
return dummy_root
-def loadComments(bug):
+def loadComments(bug, load_full=False):
+ """
+ Set load_full=True when you want to load the comment completely
+ from disk *now*, rather than waiting and lazy loading as required.
+ """
path = bug.get_path("comments")
if not os.path.isdir(path):
return Comment(bug, uuid=INVALID_UUID)
@@ -64,6 +80,9 @@ def loadComments(bug):
if uuid.startswith('.'):
continue
comm = Comment(bug, uuid, from_disk=True)
+ if load_full == True:
+ comm.load_settings()
+ dummy = comm.body # force the body to load
comments.append(comm)
return _list_to_root(comments, bug)
@@ -73,15 +92,108 @@ def saveComments(bug):
for comment in bug.comment_root.traverse():
comment.save()
-class InvalidShortname(KeyError):
- def __init__(self, shortname, shortnames):
- msg = "Invalid shortname %s\n%s" % (shortname, shortnames)
- KeyError.__init__(self, msg)
- self.shortname = shortname
- self.shortnames = shortnames
+# Define an invalid value for our properties, distinct from None,
+# which shows that a property has been initialized but has no value.
+EMPTY = -1
class Comment(Tree):
+ """
+ >>> c = Comment()
+ >>> c.uuid != None
+ True
+ >>> c.uuid = "some-UUID"
+ >>> print c.content_type
+ text/plain
+ """
+
+ def _save_settings(self, old, new):
+ if self.sync_with_disk==True:
+ self.save_settings()
+ def _load_settings(self):
+ if self.sync_with_disk==True and self._settings_loaded==False:
+ self.load_settings()
+ else:
+ for property in self.settings_properties:
+ if property not in self.settings:
+ self.settings[property] = EMPTY
+
+ settings_properties = []
+ required_saved_properties = ['Content-type'] # to protect against future changes in default values
+ def _setting_name_to_attr_name(self, name):
+ "Helper for looking up default vals for required-saved-properties"
+ return name.lower().replace('-', '_')
+
+ def _versioned_property(name, doc, default=None, save=_save_settings, load=_load_settings, setprops=settings_properties, allowed=None):
+ "Combine the common decorators in a single function"
+ setprops.append(name)
+ def decorator(funcs):
+ if allowed != None:
+ checked = checked_property(allowed=allowed)
+ defaulting = defaulting_property(default=default, null=EMPTY)
+ change_hook = change_hook_property(hook=save)
+ primed = primed_property(primer=load)
+ settings = settings_property(name=name)
+ docp = doc_property(doc=doc)
+ deco = defaulting(change_hook(primed(settings(docp(funcs)))))
+ if allowed != None:
+ deco = checked(deco)
+ return Property(deco)
+ return decorator
+
+ @_versioned_property(name="From",
+ doc="The author of the comment")
+ def From(): return {}
+
+ @_versioned_property(name="In-reply-to",
+ doc="UUID for parent comment or bug")
+ def in_reply_to(): return {}
+
+ @_versioned_property(name="Content-type",
+ doc="Mime type for comment body",
+ default="text/plain")
+ def content_type(): return {}
+
+ @_versioned_property(name="Date",
+ doc="An RFC 2822 timestamp for comment creation")
+ def time_string(): return {}
+
+ def _get_time(self):
+ if self.time_string == None:
+ return None
+ return utility.str_to_time(self.time_string)
+ def _set_time(self, value):
+ self.time_string = utility.time_to_str(value)
+ time = property(fget=_get_time,
+ fset=_set_time,
+ doc="An integer version of .time_string")
+
+ def _get_comment_body(self):
+ if self.rcs != None and self.sync_with_disk == True:
+ import rcs
+ return self.rcs.get_file_contents(self.get_path("body"))
+ def _set_comment_body(self, value, force=False):
+ if (self.rcs != None and self.sync_with_disk == True) or force==True:
+ assert value != None, "Can't save empty comment"
+ self.rcs.set_file_contents(self.get_path("body"), value)
+
+ @Property
+ @change_hook_property(hook=_set_comment_body)
+ @cached_property(generator=_get_comment_body)
+ @local_property("body")
+ @doc_property(doc="The meat of the comment")
+ def body(): return {}
+
+ def _get_rcs(self):
+ if hasattr(self.bug, "rcs"):
+ return self.bug.rcs
+
+ @Property
+ @cached_property(generator=_get_rcs)
+ @local_property("rcs")
+ @doc_property(doc="A revision control system instance.")
+ def rcs(): return {}
+
def __init__(self, bug=None, uuid=None, from_disk=False,
in_reply_to=None, body=None):
"""
@@ -98,25 +210,19 @@ class Comment(Tree):
"""
Tree.__init__(self)
self.bug = bug
- if bug != None:
- self.rcs = bug.rcs
- else:
- self.rcs = None
+ self.uuid = uuid
+ self._settings_loaded = False
+ self.settings = {}
if from_disk == True:
- self.uuid = uuid
- self.load()
+ self.sync_with_disk = True
else:
- if uuid != None:
- self.uuid = uuid
- else:
+ self.sync_with_disk = False
+ if uuid == None:
self.uuid = uuid_gen()
- self.time = time.time()
+ self.time = int(time.time()) # only save to second precision
if self.rcs != None:
self.From = self.rcs.get_user_id()
- else:
- self.From = None
self.in_reply_to = in_reply_to
- self.content_type = "text/plain"
self.body = body
def traverse(self, *args, **kwargs):
@@ -126,22 +232,10 @@ class Comment(Tree):
continue
yield comment
- def _clean_string(self, value):
- """
- >>> comm = Comment()
- >>> comm._clean_string(None)
- ''
- >>> comm._clean_string("abc")
- 'abc'
- """
- if value == None:
- return ""
- return value
-
def string(self, indent=0, shortname=None):
"""
>>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n")
- >>> comm.time = utility.str_to_time("Thu, 01 Jan 1970 00:00:00 +0000")
+ >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000"
>>> print comm.string(indent=2, shortname="com-1")
--------- Comment ---------
Name: com-1
@@ -157,12 +251,12 @@ class Comment(Tree):
lines = []
lines.append("--------- Comment ---------")
lines.append("Name: %s" % shortname)
- lines.append("From: %s" % self._clean_string(self.From))
- lines.append("Date: %s" % utility.time_to_str(self.time))
+ lines.append("From: %s" % (self.From or ""))
+ lines.append("Date: %s" % self.time_string)
lines.append("")
- #lines.append(textwrap.fill(self._clean_string(self.body),
+ #lines.append(textwrap.fill(self.body or "",
# width=(79-indent)))
- lines.extend(self._clean_string(self.body).splitlines())
+ lines.extend((self.body or "").splitlines())
# some comments shouldn't be wrapped...
istring = ' '*indent
@@ -173,7 +267,7 @@ class Comment(Tree):
"""
>>> comm = Comment(bug=None, body="Some insightful remarks")
>>> comm.uuid = "com-1"
- >>> comm.time = utility.str_to_time("Thu, 20 Nov 2008 15:55:11 +0000")
+ >>> comm.time_string = "Thu, 20 Nov 2008 15:55:11 +0000"
>>> comm.From = "Jane Doe <jdoe@example.com>"
>>> print comm
--------- Comment ---------
@@ -192,36 +286,34 @@ class Comment(Tree):
assert name in ["values", "body"]
return os.path.join(my_dir, name)
- def load(self):
- map = mapfile.map_load(self.rcs, self.get_path("values"))
- self.time = utility.str_to_time(map["Date"])
- self.From = map["From"]
- self.in_reply_to = map.get("In-reply-to")
- self.content_type = map.get("Content-type", "text/plain")
- self.body = self.rcs.get_file_contents(self.get_path("body"))
+ def load_settings(self):
+ self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
+ for property in self.settings_properties:
+ if property not in self.settings:
+ self.settings[property] = EMPTY
+ elif self.settings[property] == None:
+ self.settings[property] = EMPTY
+ self._settings_loaded = True
- def save(self):
- assert self.rcs != None
- map_file = {"Date": utility.time_to_str(self.time)}
- self._add_headers(map_file, ("From", "in_reply_to", "content_type"))
+ def save_settings(self):
+ map = {}
+ for k,v in self.settings.items():
+ if (v != None and v != EMPTY):
+ map[k] = v
+ for k in self.required_saved_properties:
+ map[k] = getattr(self, self._setting_name_to_attr_name(k))
+
self.rcs.mkdir(self.get_path())
- mapfile.map_save(self.rcs, self.get_path("values"), map_file)
- self.rcs.set_file_contents(self.get_path("body"), self.body)
-
- def _add_headers(self, map, names):
- map_names = {}
- for name in names:
- map_names[name] = self._pyname_to_header(name)
- self._add_attrs(map, map_names)
-
- def _pyname_to_header(self, name):
- return name.capitalize().replace('_', '-')
-
- def _add_attrs(self, map, map_names):
- for name in map_names.keys():
- value = getattr(self, name)
- if value is not None:
- map[map_names[name]] = value
+ path = self.get_path("values")
+ mapfile.map_save(self.rcs, path, map)
+
+ def save(self):
+ assert self.body != None, "Can't save blank comment"
+ #if self.in_reply_to == None:
+ # raise Exception, str(self)+'\n'+str(self.settings)+'\n'+str(self._settings_loaded)
+ #assert self.in_reply_to != None, "Comment must be a reply to something"
+ self.save_settings()
+ self._set_comment_body(self.body, force=True)
def remove(self):
for comment in self.traverse():
@@ -232,15 +324,19 @@ class Comment(Tree):
if self.uuid != INVALID_UUID:
reply.in_reply_to = self.uuid
self.append(reply)
+ #raise Exception, "adding reply \n%s\n%s" % (self, reply)
def new_reply(self, body=None):
"""
>>> comm = Comment(bug=None, body="Some insightful remarks")
>>> repA = comm.new_reply("Critique original comment")
>>> repB = repA.new_reply("Begin flamewar :p")
+ >>> repB.in_reply_to == repA.uuid
+ True
"""
reply = Comment(self.bug, body=body)
self.add_reply(reply)
+ #raise Exception, "new reply added (%s),\n%s\n%s\n\t--%s--" % (body, self, reply, reply.in_reply_to)
return reply
def string_thread(self, name_map={}, indent=0, flatten=True,