From 87e356c9208e955fcf6c20c0b271db87bdd48014 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Fri, 14 Nov 2008 19:25:44 -0500 Subject: Split Bug and Comment class out to bug.py from bugdir.py Comment should probably have it's own file too... I also tried to clean up the interface for setting status and severity. Both attributes involve selecting strings from predefined lists. The lists of valid strings (and descriptions of each string) are now defined in bug.py. The bug.py lists are then used to generate appropriate help strings in becommands/status.py and severity.py. This should make it easier to keep the help strings in synch with the validation information. The original status strings weren't documented, and I didn't know what they all ment, so I elimanted some of them. 'in-progress' and 'disabled' are no longer with us. Of course, it would be simple to add them back in if people don't agree with me on that. Due to the loss of 'disabled' I had to change the status of two bugs (11e and 597) to 'closed'. I removed becommands/inprogress.py as well. It's functionality was replaced by the more general status.py command, which mimics the severity.py command. --- libbe/bug.py | 343 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 libbe/bug.py (limited to 'libbe/bug.py') 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. +# +# +# 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 -- cgit