From: W. Trevor King Date: Mon, 1 Dec 2008 19:42:20 +0000 (-0500) Subject: Added decorator-style properties to libbe/comment.py. X-Git-Tag: 1.0.0~94^2~5 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=a98aafc8572bb826a0fda1b6bca0011fc4ef126a;p=be.git Added decorator-style properties to libbe/comment.py. Also some typo corrections and some reworking of bug/bugdir to better support the lazier loading. --- 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 " >>> 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,