# 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 _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
def _set_comment_root(self, comment_root):
self._comment_root = comment_root
_comment_root = None
comment_root = property(_get_comment_root, _set_comment_root,
doc="The trunk of the comment tree")
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
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()
# take advantage of the string_thread(auto_name_map=True)
# SIDE-EFFECT of sorting by bug time.
comout = self.comment_root.string_thread(flatten=False,
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):
# 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):
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 len(self.comment_root) > 0:
self.rcs.mkdir(self.get_path("comments"))
comment.saveComments(self)
def remove(self):
self.comment_root.remove()
path = self.get_path()
self.rcs.recursive_remove(path)
def new_comment(self, body=None):
comm = self.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()