diff options
author | W. Trevor King <wking@drexel.edu> | 2008-11-24 18:29:16 -0500 |
---|---|---|
committer | W. Trevor King <wking@drexel.edu> | 2008-11-24 18:29:16 -0500 |
commit | a711ecf10df62e30d83c1941065404c53fecd35b (patch) | |
tree | 4111ef606fa52dc7f21ca3eb357ff83fae74fe1e /libbe/bug.py | |
parent | c5d7551e6a6e98bb6da7c7d11360224edfda2f14 (diff) | |
parent | 2c3f6c066ceb03ae3579dff029bf01f0b62c1f82 (diff) | |
download | bugseverywhere-a711ecf10df62e30d83c1941065404c53fecd35b.tar.gz |
Merge from W. Trevor King's tree.
Diffstat (limited to 'libbe/bug.py')
-rw-r--r-- | libbe/bug.py | 351 |
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() |