diff options
Diffstat (limited to 'libbe/bug.py')
-rw-r--r-- | libbe/bug.py | 343 |
1 files changed, 343 insertions, 0 deletions
diff --git a/libbe/bug.py b/libbe/bug.py new file mode 100644 index 0000000..46dd521 --- /dev/null +++ b/libbe/bug.py @@ -0,0 +1,343 @@ +# 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 names +import mapfile +import time +import utility +from rcs import rcs_by_name + + +### 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.")) + + +### 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 __init__(self, path, uuid, rcs_name): + self.path = path + self.uuid = uuid + if uuid is not None: + dict = mapfile.map_load(self.get_path("values")) + else: + dict = {} + + self.rcs_name = rcs_name + + self.summary = dict.get("summary") + self.creator = dict.get("creator") + self.target = dict.get("target") + self.status = dict.get("status", "open") + self.severity = dict.get("severity", "minor") + self.assigned = dict.get("assigned") + self.time = dict.get("time") + if self.time is not None: + self.time = utility.str_to_time(self.time) + + def __repr__(self): + return "Bug(uuid=%r)" % self.uuid + + def get_path(self, file): + return os.path.join(self.path, self.uuid, file) + + def _get_active(self): + return self.status in active_status_values + + active = property(_get_active) + + def add_attr(self, map, name): + value = getattr(self, name) + if value is not None: + map[name] = value + + def save(self): + 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) + path = self.get_path("values") + mapfile.map_save(rcs_by_name(self.rcs_name), path, map) + + def _get_rcs(self): + return rcs_by_name(self.rcs_name) + + rcs = property(_get_rcs) + + def new_comment(self): + if not os.path.exists(self.get_path("comments")): + self.rcs.mkdir(self.get_path("comments")) + comm = Comment(None, self) + comm.uuid = names.uuid() + return comm + + def get_comment(self, uuid): + return Comment(uuid, self) + + def iter_comment_ids(self): + path = self.get_path("comments") + if not os.path.isdir(path): + return + try: + for uuid in os.listdir(path): + if (uuid.startswith('.')): + continue + yield uuid + except OSError, e: + if e.errno != errno.ENOENT: + raise + return + + def list_comments(self): + comments = [Comment(id, self) for id in self.iter_comment_ids()] + comments.sort(cmp_time) + return comments + +def new_bug(dir, uuid=None): + bug = dir.new_bug(uuid) + bug.creator = names.creator() + bug.severity = "minor" + bug.status = "open" + bug.time = time.time() + return bug + +def new_comment(bug, body=None): + comm = bug.new_comment() + comm.From = names.creator() + comm.date = time.time() + comm.body = body + return comm + +def add_headers(obj, map, names): + map_names = {} + for name in names: + map_names[name] = pyname_to_header(name) + add_attrs(obj, map, names, map_names) + +def add_attrs(obj, map, names, map_names=None): + if map_names is None: + map_names = {} + for name in names: + map_names[name] = name + + for name in names: + value = getattr(obj, name) + if value is not None: + map[map_names[name]] = value + + +class Comment(object): + def __init__(self, uuid, bug): + object.__init__(self) + self.uuid = uuid + self.bug = bug + if self.uuid is not None and self.bug is not None: + map = mapfile.map_load(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 = file(self.get_path("body")).read().decode("utf-8") + else: + self.time = None + self.From = None + self.in_reply_to = None + self.content_type = "text/plain" + self.body = None + + def save(self): + map_file = {"Date": utility.time_to_str(self.time)} + add_headers(self, map_file, ("From", "in_reply_to", "content_type")) + if not os.path.exists(self.get_path(None)): + self.bug.rcs.mkdir(self.get_path(None)) + mapfile.map_save(self.bug.rcs, self.get_path("values"), map_file) + self.bug.rcs.set_file_contents(self.get_path("body"), + self.body.encode('utf-8')) + + + def get_path(self, name): + my_dir = os.path.join(self.bug.get_path("comments"), self.uuid) + if name is None: + return my_dir + return os.path.join(my_dir, name) + + +def thread_comments(comments): + child_map = {} + top_comments = [] + for comment in comments: + child_map[comment.uuid] = [] + for comment in comments: + if comment.in_reply_to is None or comment.in_reply_to not in child_map: + top_comments.append(comment) + continue + child_map[comment.in_reply_to].append(comment) + + def recurse_children(comment): + child_list = [] + for child in child_map[comment.uuid]: + child_list.append(recurse_children(child)) + return (comment, child_list) + return [recurse_children(c) for c in top_comments] + +def pyname_to_header(name): + return name.capitalize().replace('_', '-') + + + +class MockBug: + def __init__(self, attr, value): + setattr(self, attr, value) + +# 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. + + >>> attr="severity" + >>> cmp_severity(MockBug(attr,"wishlist"), MockBug(attr,"wishlist")) == 0 + True + >>> cmp_severity(MockBug(attr,"wishlist"), MockBug(attr,"minor")) > 0 + True + >>> cmp_severity(MockBug(attr,"critical"), MockBug(attr,"wishlist")) < 0 + True + """ + 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. + + >>> attr="status" + >>> cmp_status(MockBug(attr,"open"), MockBug(attr,"open")) == 0 + True + >>> cmp_status(MockBug(attr,"open"), MockBug(attr,"closed")) < 0 + True + >>> cmp_status(MockBug(attr,"closed"), MockBug(attr,"open")) > 0 + True + """ + 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" + >>> cmp_attr(MockBug(attr,1), MockBug(attr,2), attr, invert=False) < 0 + True + >>> cmp_attr(MockBug(attr,1), MockBug(attr,2), attr, invert=True) > 0 + True + >>> cmp_attr(MockBug(attr,1), MockBug(attr,1), attr) == 0 + True + """ + 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 |