aboutsummaryrefslogtreecommitdiffstats
path: root/libbe
diff options
context:
space:
mode:
Diffstat (limited to 'libbe')
-rw-r--r--libbe/bug.py343
-rw-r--r--libbe/bugdir.py262
-rw-r--r--libbe/cmdutil.py4
-rw-r--r--libbe/mapfile.py24
-rw-r--r--libbe/tests.py6
5 files changed, 380 insertions, 259 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
diff --git a/libbe/bugdir.py b/libbe/bugdir.py
index bcc163c..7570bb3 100644
--- a/libbe/bugdir.py
+++ b/libbe/bugdir.py
@@ -23,6 +23,7 @@ import mapfile
import time
import utility
from rcs import rcs_by_name
+from bug import Bug
class NoBugDir(Exception):
def __init__(self, path):
@@ -108,7 +109,8 @@ def create_bug_dir(path, rcs):
raise
rcs.mkdir(os.path.join(root, "bugs"))
set_version(root, rcs)
- map_save(rcs, os.path.join(root, "settings"), {"rcs_name": rcs.name})
+ mapfile.map_save(rcs,
+ os.path.join(root, "settings"), {"rcs_name": rcs.name})
return BugDir(os.path.join(path, ".be"))
@@ -137,8 +139,8 @@ class BugDir:
self.dir = dir
self.bugs_path = os.path.join(self.dir, "bugs")
try:
- self.settings = map_load(os.path.join(self.dir, "settings"))
- except NoSuchFile:
+ self.settings = mapfile.map_load(os.path.join(self.dir, "settings"))
+ except mapfile.NoSuchFile:
self.settings = {"rcs_name": "None"}
rcs_name = setting_property("rcs_name", ("None", "bzr", "git", "Arch", "hg"))
@@ -147,7 +149,8 @@ class BugDir:
target = setting_property("target")
def save_settings(self):
- map_save(self.rcs, os.path.join(self.dir, "settings"), self.settings)
+ mapfile.map_save(self.rcs,
+ os.path.join(self.dir, "settings"), self.settings)
def get_rcs(self):
if self._rcs is not None and self.rcs_name == self._rcs.name:
@@ -188,258 +191,9 @@ class BugDir:
bug.uuid = uuid
return bug
-class InvalidValue(Exception):
+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
-
-
-def checked_property(name, valid):
- 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)
-
-severity_levels = ("wishlist", "minor", "serious", "critical", "fatal")
-active_status = ("open", "in-progress", "waiting", "new", "verified")
-inactive_status = ("closed", "disabled", "fixed", "wontfix", "waiting")
-
-severity_value = {}
-for i in range(len(severity_levels)):
- severity_value[severity_levels[i]] = i
-
-class Bug(object):
- status = checked_property("status", (None,)+active_status+inactive_status)
- severity = checked_property("severity", (None, "wishlist", "minor",
- "serious", "critical", "fatal"))
-
- def __init__(self, path, uuid, rcs_name):
- self.path = path
- self.uuid = uuid
- if uuid is not None:
- dict = 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")
- self.severity = dict.get("severity")
- 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
-
- 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")
- 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_date)
- return comments
-
-def cmp_date(comm1, comm2):
- return cmp(comm1.date, comm2.date)
-
-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:
- mapfile = map_load(self.get_path("values"))
- self.date = utility.str_to_time(mapfile["Date"])
- self.From = mapfile["From"]
- self.in_reply_to = mapfile.get("In-reply-to")
- self.content_type = mapfile.get("Content-type", "text/plain")
- self.body = file(self.get_path("body")).read().decode("utf-8")
- else:
- self.date = 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.date)}
- 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))
- 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('_', '-')
-
-
-def map_save(rcs, path, map):
- """Save the map as a mapfile to the specified path"""
- add = not os.path.exists(path)
- output = file(path, "wb")
- mapfile.generate(output, map)
- if add:
- rcs.add_id(path)
-
-class NoSuchFile(Exception):
- def __init__(self, pathname):
- Exception.__init__(self, "No such file: %s" % pathname)
-
-
-def map_load(path):
- try:
- return mapfile.parse(file(path, "rb"))
- except IOError, e:
- if e.errno != errno.ENOENT:
- raise e
- raise NoSuchFile(path)
-
-
-class MockBug:
- def __init__(self, severity):
- self.severity = severity
-
-def cmp_severity(bug_1, bug_2):
- """
- Compare the severity levels of two bugs, with more sever bugs comparing
- as less.
-
- >>> cmp_severity(MockBug(None), MockBug(None))
- 0
- >>> cmp_severity(MockBug("wishlist"), MockBug(None)) < 0
- True
- >>> cmp_severity(MockBug(None), MockBug("wishlist")) > 0
- True
- >>> cmp_severity(MockBug("critical"), MockBug("wishlist")) < 0
- True
- """
- val_1 = severity_value.get(bug_1.severity)
- val_2 = severity_value.get(bug_2.severity)
- return -cmp(val_1, val_2)
diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py
index 5c9f5b1..6fb915a 100644
--- a/libbe/cmdutil.py
+++ b/libbe/cmdutil.py
@@ -132,7 +132,7 @@ def iter_comment_name(bug, unique_name):
(This is a user-friendly id, not the comment uuid)
"""
def key(comment):
- return comment.date
+ return comment.time
for num, comment in enumerate(sorted(bug.list_comments(), key=key)):
yield ("%s:%d" % (unique_name, num+1), comment)
@@ -194,7 +194,7 @@ def print_threaded_comments(comments, name_map, indent=""):
print >> s, "--------- Comment ---------"
print >> s, "Name: %s" % name_map[comment.uuid]
print >> s, "From: %s" % comment.From
- print >> s, "Date: %s\n" % utility.time_to_str(comment.date)
+ print >> s, "Date: %s\n" % utility.time_to_str(comment.time)
print >> s, comment.body.rstrip('\n')
s.seek(0)
diff --git a/libbe/mapfile.py b/libbe/mapfile.py
index 6a304fd..95c3169 100644
--- a/libbe/mapfile.py
+++ b/libbe/mapfile.py
@@ -14,7 +14,10 @@
# 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.path
+import errno
import utility
+
class IllegalKey(Exception):
def __init__(self, key):
Exception.__init__(self, 'Illegal key "%s"' % key)
@@ -99,6 +102,27 @@ def parse(f):
result[name] = value
return result
+def map_save(rcs, path, map):
+ """Save the map as a mapfile to the specified path"""
+ add = not os.path.exists(path)
+ output = file(path, "wb")
+ generate(output, map)
+ if add:
+ rcs.add_id(path)
+
+class NoSuchFile(Exception):
+ def __init__(self, pathname):
+ Exception.__init__(self, "No such file: %s" % pathname)
+
+
+def map_load(path):
+ try:
+ return parse(file(path, "rb"))
+ except IOError, e:
+ if e.errno != errno.ENOENT:
+ raise e
+ raise NoSuchFile(path)
+
def split_diff3(this, other, f):
"""Split a file or string with diff3 conflicts into two files.
diff --git a/libbe/tests.py b/libbe/tests.py
index a7d925d..461e6e8 100644
--- a/libbe/tests.py
+++ b/libbe/tests.py
@@ -18,7 +18,7 @@ import tempfile
import shutil
import os
import os.path
-from libbe import bugdir, arch
+from libbe import bugdir, bug, arch
cleanable = []
def clean_up():
global cleanable
@@ -47,8 +47,8 @@ def bug_arch_dir():
def simple_bug_dir():
dir = bug_arch_dir()
- bug_a = bugdir.new_bug(dir, "a")
- bug_b = bugdir.new_bug(dir, "b")
+ bug_a = bug.new_bug(dir, "a")
+ bug_b = bug.new_bug(dir, "b")
bug_b.status = "closed"
bug_a.save()
bug_b.save()