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):
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)
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)
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):
"""
"""
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):
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
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
"""
>>> 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 ---------
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():
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,