aboutsummaryrefslogtreecommitdiffstats
path: root/libbe/bug.py
diff options
context:
space:
mode:
Diffstat (limited to 'libbe/bug.py')
-rw-r--r--libbe/bug.py273
1 files changed, 168 insertions, 105 deletions
diff --git a/libbe/bug.py b/libbe/bug.py
index f47bcba..e04d4ee 100644
--- a/libbe/bug.py
+++ b/libbe/bug.py
@@ -21,6 +21,9 @@ import time
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
import comment
import utility
@@ -69,76 +72,155 @@ for i in range(len(status_values)):
status_index[status_values[i]] = i
-def checked_property(name, valid):
+# 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 Bug(object):
"""
- Provide access to an attribute name, testing for valid values.
+ >>> b = Bug()
+ >>> print b.status
+ open
+ >>> print b.severity
+ minor
+
+ There are two formats for time, int and string. Setting either
+ one will adjust the other appropriately. The string form is the
+ one stored in the bug's settings file on disk.
+ >>> print type(b.time)
+ <type 'int'>
+ >>> print type(b.time_string)
+ <type 'str'>
+ >>> b.time = 0
+ >>> print b.time_string
+ Thu, 01 Jan 1970 00:00:00 +0000
+ >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
+ >>> b.time
+ 60
+ >>> print b.settings["time"]
+ Thu, 01 Jan 1970 00:01:00 +0000
"""
- def getter(self):
- value = getattr(self, "_"+name)
- if value not in valid:
- raise InvalidValue(name, value)
- return value
+ 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 = ['status','severity'] # to protect against future changes in default values
+
+ 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="severity",
+ doc="A measure of the bug's importance",
+ default="minor",
+ allowed=severity_values)
+ def severity(): return {}
+
+ @_versioned_property(name="status",
+ doc="The bug's current status",
+ default="open",
+ allowed=status_values)
+ def status(): return {}
+
+ @property
+ def active(self):
+ return self.status in active_status_values
- def setter(self, value):
- if value not in valid:
- raise InvalidValue(name, value)
- return setattr(self, "_"+name, value)
- return property(getter, setter)
+ @_versioned_property(name="target",
+ doc="The deadline for fixing this bug")
+ def target(): return {}
+ @_versioned_property(name="creator",
+ doc="The user who entered the bug into the system")
+ def creator(): return {}
-class Bug(object):
- severity = checked_property("severity", severity_values)
- status = checked_property("status", status_values)
+ @_versioned_property(name="reporter",
+ doc="The user who reported the bug")
+ def reporter(): return {}
- def _get_active(self):
- return self.status in active_status_values
+ @_versioned_property(name="assigned",
+ doc="The developer in charge of the bug")
+ def assigned(): return {}
+
+ @_versioned_property(name="time",
+ doc="An RFC 2822 timestamp for bug creation")
+ def time_string(): return {}
- active = property(_get_active)
+ 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 integere version of .time_string")
+
+ @_versioned_property(name="summary",
+ doc="A one-line bug description")
+ def summary(): return {}
def _get_comment_root(self):
- if self._comment_root == None:
- if self._comments_loaded == True:
- self._comment_root = comment.loadComments(self)
- else:
- self._comment_root = comment.Comment(self,
- uuid=comment.INVALID_UUID)
- return self._comment_root
+ if self.sync_with_disk:
+ return comment.loadComments(self)
+ else:
+ return comment.Comment(self, uuid=comment.INVALID_UUID)
+
+ @Property
+ @cached_property(generator=_get_comment_root)
+ @local_property("comment_root")
+ @doc_property(doc="The trunk of the comment tree")
+ def comment_root(): return {}
- def _set_comment_root(self, comment_root):
- self._comment_root = comment_root
+ def _get_rcs(self):
+ if hasattr(self.bugdir, "rcs"):
+ return self.bugdir.rcs
- _comment_root = None
- comment_root = property(_get_comment_root, _set_comment_root,
- doc="The trunk of the comment tree")
+ @Property
+ @cached_property(generator=_get_rcs)
+ @local_property("rcs")
+ @doc_property(doc="A revision control system instance.")
+ def rcs(): return {}
def __init__(self, bugdir=None, uuid=None, from_disk=False,
load_comments=False, summary=None):
self.bugdir = bugdir
- if bugdir != None:
- self.rcs = bugdir.rcs
- else:
- self.rcs = None
+ self.uuid = uuid
+ self._settings_loaded = False
+ self.settings = {}
if from_disk == True:
- self._comments_loaded = False
- self.uuid = uuid
- self.load(load_comments=load_comments)
+ self.sync_with_disk = True
+ #self.load(load_comments=load_comments)
else:
- # Note: defaults should match those in Bug.load()
- self._comments_loaded = True
- if uuid != None:
- self.uuid = uuid
- else:
+ self.sync_with_disk = False
+ if uuid == None:
self.uuid = uuid_gen()
- self.summary = summary
+ self.time = int(time.time()) # only save to second precision
if self.rcs != None:
self.creator = self.rcs.get_user_id()
- else:
- self.creator = None
- self.target = None
- self.status = "open"
- self.severity = "minor"
- self.assigned = None
- self.time = int(time.time()) # only save to second precision
+ self.summary = summary
def __repr__(self):
return "Bug(uuid=%r)" % self.uuid
@@ -149,27 +231,19 @@ class Bug(object):
else:
shortname = self.bugdir.bug_shortname(self)
if shortlist == False:
- if self.time == None:
- timestring = ""
+ if self.time_string == "":
+ timestring = self.time_string
else:
htime = utility.handy_time(self.time)
- ftime = utility.time_to_str(self.time)
- timestring = "%s (%s)" % (htime, ftime)
+ timestring = "%s (%s)" % (htime, self.time_string)
info = [("ID", self.uuid),
("Short name", shortname),
("Severity", self.severity),
("Status", self.status),
- ("Assigned", self.assigned),
- ("Target", self.target),
- ("Creator", self.creator),
+ ("Assigned", self.assigned or ""),
+ ("Target", self.target or ""),
+ ("Creator", self.creator or ""),
("Created", timestring)]
- newinfo = []
- for k,v in info:
- if v == None:
- newinfo.append((k,""))
- else:
- newinfo.append((k,v))
- info = newinfo
longest_key_len = max([len(k) for k,v in info])
infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
@@ -180,8 +254,6 @@ class Bug(object):
bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
if show_comments == True:
- if self._comments_loaded == False:
- self.load_comments()
# take advantage of the string_thread(auto_name_map=True)
# SIDE-EFFECT of sorting by comment time.
comout = self.comment_root.string_thread(flatten=False,
@@ -205,52 +277,38 @@ class Bug(object):
assert name in ["values", "comments"]
return os.path.join(my_dir, name)
- def load(self, load_comments=False):
- map = mapfile.map_load(self.rcs, self.get_path("values"))
- self.summary = map.get("summary")
- self.creator = map.get("creator")
- self.target = map.get("target")
- self.status = map.get("status", "open")
- self.severity = map.get("severity", "minor")
- self.assigned = map.get("assigned")
- self.time = map.get("time")
- if self.time is not None:
- self.time = utility.str_to_time(self.time)
-
- if load_comments == True:
- self.load_comments()
+ 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 load_comments(self):
- # clear _comment_root, so _get_comment_root returns a fresh version
- self._comment_root = None
- self._comments_loaded = True
-
- def comments(self):
- if self._comments_loaded == False:
- self.load_comments()
- for comment in self.comment_root.traverse():
- yield comment
-
- def _add_attr(self, map, name):
- value = getattr(self, name)
- if value is not None:
- map[name] = value
-
- def save(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 save_settings(self):
assert self.summary != None, "Can't save blank bug"
map = {}
- self._add_attr(map, "assigned")
- self._add_attr(map, "summary")
- self._add_attr(map, "creator")
- self._add_attr(map, "target")
- self._add_attr(map, "status")
- self._add_attr(map, "severity")
- if self.time is not None:
- map["time"] = utility.time_to_str(self.time)
+ 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, k)
self.rcs.mkdir(self.get_path())
path = self.get_path("values")
mapfile.map_save(self.rcs, path, map)
+
+ def save(self):
+ self.save_settings()
if len(self.comment_root) > 0:
self.rcs.mkdir(self.get_path("comments"))
@@ -261,6 +319,10 @@ class Bug(object):
path = self.get_path()
self.rcs.recursive_remove(path)
+ def comments(self):
+ for comment in self.comment_root.traverse():
+ yield comment
+
def new_comment(self, body=None):
comm = self.comment_root.new_reply(body=body)
return comm
@@ -280,7 +342,8 @@ class Bug(object):
for id, comment in self.comment_root.comment_shortnames(shortname):
yield (id, comment)
-# the general rule for bug sorting is that "more important" bugs are
+
+# The general rule for bug sorting is that "more important" bugs are
# less than "less important" bugs. This way sorting a list of bugs
# will put the most important bugs first in the list. When relative
# importance is unclear, the sorting follows some arbitrary convention