aboutsummaryrefslogtreecommitdiffstats
path: root/libbe/bug.py
diff options
context:
space:
mode:
authorW. Trevor King <wking@drexel.edu>2008-11-24 18:29:16 -0500
committerW. Trevor King <wking@drexel.edu>2008-11-24 18:29:16 -0500
commita711ecf10df62e30d83c1941065404c53fecd35b (patch)
tree4111ef606fa52dc7f21ca3eb357ff83fae74fe1e /libbe/bug.py
parentc5d7551e6a6e98bb6da7c7d11360224edfda2f14 (diff)
parent2c3f6c066ceb03ae3579dff029bf01f0b62c1f82 (diff)
downloadbugseverywhere-a711ecf10df62e30d83c1941065404c53fecd35b.tar.gz
Merge from W. Trevor King's tree.
Diffstat (limited to 'libbe/bug.py')
-rw-r--r--libbe/bug.py351
1 files changed, 351 insertions, 0 deletions
diff --git a/libbe/bug.py b/libbe/bug.py
new file mode 100644
index 0000000..c75c968
--- /dev/null
+++ b/libbe/bug.py
@@ -0,0 +1,351 @@
+# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
+# <abentley@panoramicfeedback.com>
+#
+# 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.
+#
+# 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
+import os
+import os.path
+import errno
+import time
+import doctest
+
+from beuuid import uuid_gen
+import mapfile
+import comment
+import utility
+
+
+### Define and describe valid bug categories
+# Use a tuple of (category, description) tuples since we don't have
+# ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
+
+# in order of increasing severity
+severity_level_def = (
+ ("wishlist","A feature that could improve usefullness, but not a bug."),
+ ("minor","The standard bug level."),
+ ("serious","A bug that requires workarounds."),
+ ("critical","A bug that prevents some features from working at all."),
+ ("fatal","A bug that makes the package unusable."))
+
+# in order of increasing resolution
+# roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
+active_status_def = (
+ ("unconfirmed","A possible bug which lacks independent existance confirmation."),
+ ("open","A working bug that has not been assigned to a developer."),
+ ("assigned","A working bug that has been assigned to a developer."),
+ ("test","The code has been adjusted, but the fix is still being tested."))
+inactive_status_def = (
+ ("closed", "The bug is no longer relevant."),
+ ("fixed", "The bug should no longer occur."),
+ ("wontfix","It's not a bug, it's a feature."),
+ ("disabled", "?"))
+
+
+### Convert the description tuples to more useful formats
+
+severity_values = tuple([val for val,description in severity_level_def])
+severity_description = dict(severity_level_def)
+severity_index = {}
+for i in range(len(severity_values)):
+ severity_index[severity_values[i]] = i
+
+active_status_values = tuple(val for val,description in active_status_def)
+inactive_status_values = tuple(val for val,description in inactive_status_def)
+status_values = active_status_values + inactive_status_values
+status_description = dict(active_status_def+inactive_status_def)
+status_index = {}
+for i in range(len(status_values)):
+ status_index[status_values[i]] = i
+
+
+def checked_property(name, valid):
+ """
+ Provide access to an attribute name, testing for valid values.
+ """
+ def getter(self):
+ value = getattr(self, "_"+name)
+ if value not in valid:
+ raise InvalidValue(name, value)
+ return value
+
+ def setter(self, value):
+ if value not in valid:
+ raise InvalidValue(name, value)
+ return setattr(self, "_"+name, value)
+ return property(getter, setter)
+
+
+class Bug(object):
+ severity = checked_property("severity", severity_values)
+ status = checked_property("status", status_values)
+
+ def _get_active(self):
+ return self.status in active_status_values
+
+ active = property(_get_active)
+
+ 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
+ if from_disk == True:
+ self._comments_loaded = False
+ self.uuid = uuid
+ 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.uuid = uuid_gen()
+ self.summary = summary
+ 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.comment_root = comment.Comment(self, uuid=comment.INVALID_UUID)
+
+ def __repr__(self):
+ return "Bug(uuid=%r)" % self.uuid
+
+ def string(self, shortlist=False, show_comments=False):
+ if self.bugdir == None:
+ shortname = self.uuid
+ else:
+ shortname = self.bugdir.bug_shortname(self)
+ if shortlist == False:
+ if self.time == None:
+ timestring = ""
+ else:
+ htime = utility.handy_time(self.time)
+ ftime = utility.time_to_str(self.time)
+ timestring = "%s (%s)" % (htime, ftime)
+ info = [("ID", self.uuid),
+ ("Short name", shortname),
+ ("Severity", self.severity),
+ ("Status", self.status),
+ ("Assigned", self.assigned),
+ ("Target", self.target),
+ ("Creator", self.creator),
+ ("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')
+ else:
+ statuschar = self.status[0]
+ severitychar = self.severity[0]
+ chars = "%c%c" % (statuschar, severitychar)
+ bugout = "%s:%s: %s" % (shortname, chars, self.summary.rstrip('\n'))
+
+ if show_comments == True:
+ if self._comments_loaded == False:
+ self.load_comments()
+ comout = self.comment_root.string_thread(auto_name_map=True,
+ bug_shortname=shortname)
+ output = bugout + '\n' + comout.rstrip('\n')
+ else :
+ output = bugout
+ return output
+
+ def __str__(self):
+ return self.string(shortlist=True)
+
+ def __cmp__(self, other):
+ return cmp_full(self, other)
+
+ def get_path(self, name=None):
+ my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
+ if name is None:
+ return my_dir
+ 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_comments(self):
+ self.comment_root = comment.loadComments(self)
+ 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):
+ 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)
+
+ self.rcs.mkdir(self.get_path())
+ path = self.get_path("values")
+ mapfile.map_save(self.rcs, path, map)
+
+ if self._comments_loaded:
+ if len(self.comment_root) > 0:
+ self.rcs.mkdir(self.get_path("comments"))
+ comment.saveComments(self)
+
+ def remove(self):
+ self.load_comments()
+ self.comment_root.remove()
+ path = self.get_path()
+ self.rcs.recursive_remove(path)
+
+ def new_comment(self, body=None):
+ comm = comment.comment_root.new_reply(body=body)
+ return comm
+
+ def comment_from_shortname(self, shortname, *args, **kwargs):
+ return self.comment_root.comment_from_shortname(shortname, *args, **kwargs)
+
+ def comment_from_uuid(self, uuid):
+ return self.comment_root.comment_from_uuid(uuid)
+
+
+# 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
+# (i.e. dictionary order).
+
+def cmp_severity(bug_1, bug_2):
+ """
+ Compare the severity levels of two bugs, with more severe bugs
+ comparing as less.
+ >>> bugA = Bug()
+ >>> bugB = Bug()
+ >>> bugA.severity = bugB.severity = "wishlist"
+ >>> cmp_severity(bugA, bugB) == 0
+ True
+ >>> bugB.severity = "minor"
+ >>> cmp_severity(bugA, bugB) > 0
+ True
+ >>> bugA.severity = "critical"
+ >>> cmp_severity(bugA, bugB) < 0
+ True
+ """
+ if not hasattr(bug_2, "severity") :
+ return 1
+ return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
+
+def cmp_status(bug_1, bug_2):
+ """
+ Compare the status levels of two bugs, with more 'open' bugs
+ comparing as less.
+ >>> bugA = Bug()
+ >>> bugB = Bug()
+ >>> bugA.status = bugB.status = "open"
+ >>> cmp_status(bugA, bugB) == 0
+ True
+ >>> bugB.status = "closed"
+ >>> cmp_status(bugA, bugB) < 0
+ True
+ >>> bugA.status = "fixed"
+ >>> cmp_status(bugA, bugB) > 0
+ True
+ """
+ if not hasattr(bug_2, "status") :
+ return 1
+ val_2 = status_index[bug_2.status]
+ return cmp(status_index[bug_1.status], status_index[bug_2.status])
+
+def cmp_attr(bug_1, bug_2, attr, invert=False):
+ """
+ Compare a general attribute between two bugs using the conventional
+ comparison rule for that attribute type. If invert == True, sort
+ *against* that convention.
+ >>> attr="severity"
+ >>> bugA = Bug()
+ >>> bugB = Bug()
+ >>> bugA.severity = "critical"
+ >>> bugB.severity = "wishlist"
+ >>> cmp_attr(bugA, bugB, attr) < 0
+ True
+ >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
+ True
+ >>> bugB.severity = "critical"
+ >>> cmp_attr(bugA, bugB, attr) == 0
+ True
+ """
+ if not hasattr(bug_2, attr) :
+ return 1
+ if invert == True :
+ return -cmp(getattr(bug_1, attr), getattr(bug_2, attr))
+ else :
+ return cmp(getattr(bug_1, attr), getattr(bug_2, attr))
+
+# alphabetical rankings (a < z)
+cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
+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_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
+ cmp_time,cmp_creator)):
+ for comparison in cmp_list :
+ val = comparison(bug_1, bug_2)
+ if val != 0 :
+ return val
+ return 0
+
+class InvalidValue(ValueError):
+ def __init__(self, name, value):
+ msg = "Cannot assign value %s to %s" % (value, name)
+ Exception.__init__(self, msg)
+ self.name = name
+ self.value = value
+
+suite = doctest.DocTestSuite()