Split Bug and Comment class out to bug.py from bugdir.py
authorW. Trevor King <wking@drexel.edu>
Sat, 15 Nov 2008 00:25:44 +0000 (19:25 -0500)
committerW. Trevor King <wking@drexel.edu>
Sat, 15 Nov 2008 00:25:44 +0000 (19:25 -0500)
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.

17 files changed:
.be/bugs/11e3dddb-9da4-4aa2-af0a-53338fd0d96a/values
.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/values
be
becommands/assign.py
becommands/comment.py
becommands/help.py
becommands/list.py
becommands/new.py
becommands/severity.py
becommands/show.py
becommands/status.py [moved from becommands/inprogress.py with 50% similarity]
becommands/upgrade.py
libbe/bug.py [new file with mode: 0644]
libbe/bugdir.py
libbe/cmdutil.py
libbe/mapfile.py
libbe/tests.py

index 68c357f94722f1efaf95b004d3d9d92c8447bb50..1388ccb6f87abf66a3af65525ead3f53e71e145e 100644 (file)
@@ -15,7 +15,7 @@ severity=minor
 
 
 
-status=disabled
+status=closed
 
 
 
index 480386b693db178d73fe7390c5b7e614368adce2..823e2bc692baa2633e33ba5c4891f4ea46afabeb 100644 (file)
@@ -15,7 +15,7 @@ severity=minor
 
 
 
-status=disabled
+status=closed
 
 
 
diff --git a/be b/be
index 11eff7435c8c1afd7a93358ada451afc23e2ddbd..9be1804716be24a9586919fbb368df1d5fb0405f 100755 (executable)
--- a/be
+++ b/be
@@ -23,13 +23,13 @@ from libbe import names, plugin, cmdutil
 import sys
 import os
 import becommands.severity
+import becommands.status
 import becommands.list
 import becommands.show
 import becommands.set_root
 import becommands.new
 import becommands.close
 import becommands.open
-import becommands.inprogress
 __doc__ = """Bugs Everywhere - Distributed bug tracking
 
 Supported becommands
@@ -40,10 +40,12 @@ Supported becommands
     close: close a bug
      open: re-open a bug
  severity: %s
+   status: %s
 
 Unimplemented becommands
   comment: append a comment to a bug
-""" % becommands.severity.__desc__
+""" % (becommands.severity.__desc__,
+       becommands.status.__desc__)
 
 
 
index 38ece52cdefdd6b07384337e8e1784a9337087a8..d595c1dd9758e1d50edbdb0055b101ea49e940e4 100644 (file)
@@ -15,7 +15,7 @@
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 """Assign an individual or group to fix a bug"""
-from libbe import bugdir, cmdutil, names 
+from libbe import cmdutil, names 
 __desc__ = __doc__
 
 def execute(args):
index e3a1d93799b8979da1134ef6d90000e89233de15..d214a1977fb05122dc66dbf3560338d650d6e107 100644 (file)
@@ -15,7 +15,8 @@
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 """Add a comment to a bug"""
-from libbe import bugdir, cmdutil, names, utility
+from libbe import cmdutil, names, utility
+from libbe.bug import new_comment
 import os
 def execute(args):
     """
@@ -62,7 +63,7 @@ def execute(args):
         if not body.endswith('\n'):
             body+='\n'
 
-    comment = bugdir.new_comment(bug, body)
+    comment = new_comment(bug, body)
     if parent_comment is not None:
         comment.in_reply_to = parent_comment.uuid
     comment.save()
index aa4aa64d027acf68ece0440f54de2434a1d5b9be..45d35ca17fccfabb009f0674d9e2a98249f64f97 100644 (file)
@@ -15,7 +15,7 @@
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 """Print help for given subcommand"""
-from libbe import bugdir, cmdutil, names, utility
+from libbe import cmdutil, names, utility
 
 def execute(args):
     """
index cf06ccd1c0534a1cf321603e9d1507222b9c0d72..93518405dd038879592c564c770a43acfe00e12d 100644 (file)
@@ -15,7 +15,8 @@
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 """List bugs"""
-from libbe import bugdir, cmdutil, names
+from libbe import cmdutil, names
+from libbe.bug import cmp_severity, cmp_time
 import os
 def execute(args):
     options, args = get_parser().parse_args(args)
@@ -75,10 +76,8 @@ def execute(args):
             other_bugs.append(bug)
 
     def list_bugs(cur_bugs, title, no_target=False):
-        def cmp_date(bug1, bug2):
-            return -cmp(bug1.time, bug2.time)
-        cur_bugs.sort(cmp_date)
-        cur_bugs.sort(bugdir.cmp_severity)
+        cur_bugs.sort(cmp_time)
+        cur_bugs.sort(cmp_severity)
         if len(cur_bugs) > 0:
             print cmdutil.underlined(title)
             for bug in cur_bugs:
index 7bd2382cd57b52471440375c43e59025b4ec7f66..b22dd0a1f76fcbc2f58a5177258621401f941414 100644 (file)
@@ -15,7 +15,9 @@
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 """Create a new bug"""
-from libbe import bugdir, cmdutil, names, utility
+from libbe import cmdutil, names, utility
+from libbe.bug import new_bug
+
 def execute(args):
     """
     >>> import os, time
@@ -41,7 +43,7 @@ def execute(args):
     if len(args) != 1:
         raise cmdutil.UserError("Please supply a summary message")
     dir = cmdutil.bug_tree()
-    bug = bugdir.new_bug(dir)
+    bug = new_bug(dir)
     bug.summary = args[0]
     bug.save()
     bugs = (dir.list())
index af99bf7f5130c91bf89eab2d13823e65b43d4992..1a68c317a734369a45d985a3a1bae763a8a45485 100644 (file)
@@ -15,8 +15,9 @@
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 """Show or change a bug's severity level"""
-from libbe import bugdir
 from libbe import cmdutil 
+from libbe.bug import severity_values, severity_description
+
 __desc__ = __doc__
 
 def execute(args):
@@ -46,7 +47,7 @@ def execute(args):
     elif len(args) == 2:
         try:
             bug.severity = args[1]
-        except bugdir.InvalidValue, e:
+        except ValueError, e:
             if e.name != "severity":
                 raise
             raise cmdutil.UserError ("Invalid severity level: %s" % e.value)
@@ -56,19 +57,21 @@ def get_parser():
     parser = cmdutil.CmdOptionParser("be severity bug-id [severity]")
     return parser
 
-longhelp="""
-Show or change a bug's severity level.  
+longhelp=["""
+Show or change a bug's severity level.
 
 If no severity is specified, the current value is printed.  If a severity level
 is specified, it will be assigned to the bug.
 
 Severity levels are:
-wishlist: A feature that could improve usefulness, 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.
-"""
+"""]
+longest_severity_len = max([len(s) for s in severity_values])
+for severity in severity_values :
+    description = severity_description[severity]
+    s = "%*s : %s\n" % (longest_severity_len, severity, description)
+    longhelp.append(s)
+longhelp = ''.join(longhelp)
+
 
 def help():
     return get_parser().help_str() + longhelp
index 8e83a1f9a50fa96f5c149899dbdc5c9899dffb30..e75c1ac1a32b79ac61282d9763837e06d195a9ac 100644 (file)
@@ -15,7 +15,8 @@
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 """Show a particular bug"""
-from libbe import bugdir, cmdutil, utility
+from libbe import cmdutil, utility
+from libbe.bug import thread_comments
 import os
 
 def execute(args):
@@ -37,7 +38,7 @@ def execute(args):
     for c_name, comment in cmdutil.iter_comment_name(bug, unique_name):
         name_map[comment.uuid] = c_name
         comments.append(comment)
-    threaded = bugdir.thread_comments(comments)
+    threaded = thread_comments(comments)
     cmdutil.print_threaded_comments(threaded, name_map)
 
 def get_parser():
similarity index 50%
rename from becommands/inprogress.py
rename to becommands/status.py
index 05da971a9ea7668edc4fc65a1ff36b0bbdbd1357..b57db4e3c6cd3c8dd1a62d5816b084e3b00343dc 100644 (file)
 #    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
-"""Bug fixing in progress"""
+"""Show or change a bug's status"""
 from libbe import cmdutil
+from libbe.bug import status_values, status_description
+__desc__ = __doc__
+
 def execute(args):
     """
     >>> from libbe import tests
     >>> import os
     >>> dir = tests.simple_bug_dir()
     >>> os.chdir(dir.dir)
-    >>> dir.get_bug("a").status
-    u'open'
     >>> execute(["a"])
-    >>> dir.get_bug("a").status
-    u'in-progress'
+    open
+    >>> execute(["a", "closed"])
+    >>> execute(["a"])
+    closed
+    >>> execute(["a", "none"])
+    Traceback (most recent call last):
+    UserError: Invalid status: none
     >>> tests.clean_up()
     """
     options, args = get_parser().parse_args(args)
-    if len(args) !=1:
-        raise cmdutil.UserError("Please specify a bug id.")
+    assert(len(args) in (0, 1, 2))
+    if len(args) == 0:
+        print help()
+        return
     bug = cmdutil.get_bug(args[0])
-    bug.status = "in-progress"
-    bug.save()
+    if len(args) == 1:
+        print bug.status
+    elif len(args) == 2:
+        try:
+            bug.status = args[1]
+        except ValueError, e:
+            if e.name != "status":
+                raise
+            raise cmdutil.UserError ("Invalid status: %s" % e.value)
+        bug.save()
 
 def get_parser():
-    parser = cmdutil.CmdOptionParser("be inprogress BUG-ID")
+    parser = cmdutil.CmdOptionParser("be status bug-id [status]")
     return parser
 
-longhelp="""
-Mark a bug as 'in-progress'.
-"""
+longhelp=["""
+Show or change a bug's severity level.
+
+If no severity is specified, the current value is printed.  If a severity level
+is specified, it will be assigned to the bug.
+
+Severity levels are:
+"""]
+longest_status_len = max([len(s) for s in status_values])
+for status in status_values :
+    description = status_description[status]
+    s = "%*s : %s\n" % (longest_status_len, status, description)
+    longhelp.append(s)
+longhelp = ''.join(longhelp)
 
 def help():
     return get_parser().help_str() + longhelp
index 3dcb4ebf35b805496e98edd5765334d1bbf975df..8f7c3a4e58929e0870747b89ab516165eb7fcf5c 100644 (file)
@@ -18,6 +18,7 @@
 import os.path
 import errno
 from libbe import bugdir, rcs, cmdutil
+from libbe.bug import Bug
 
 def execute(args):
     options, args = get_parser().parse_args(args)
@@ -25,7 +26,7 @@ def execute(args):
     for uuid in root.list_uuids():
         old_bug = OldBug(root.bugs_path, uuid)
         
-        new_bug = bugdir.Bug(root.bugs_path, None)
+        new_bug = Bug(root.bugs_path, None)
         new_bug.uuid = old_bug.uuid
         new_bug.summary = old_bug.summary
         new_bug.creator = old_bug.creator
diff --git a/libbe/bug.py b/libbe/bug.py
new file mode 100644 (file)
index 0000000..46dd521
--- /dev/null
@@ -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
index bcc163cd5a110193efd8f14fe49c94dedfe236dc..7570bb37f9a632b58f571d28e915b45e059af3ae 100644 (file)
@@ -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)
index 5c9f5b1789343fd511166fda1c630bf7b1862a99..6fb915a5bcac47b53a4d0ff638a3cf6acea9beed 100644 (file)
@@ -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)
index 6a304fdad6a14f921124208cbd30d0b0ed298a17..95c31695e2147f461282bd6bd3f23fdab970c303 100644 (file)
 #    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.
index a7d925dea17a66685344a32254f5c787231ab563..461e6e8358810dd5618df13680303793d4ff5dc2 100644 (file)
@@ -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()