XML output for "be show"
[be.git] / libbe / bug.py
1 # Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
2 # <abentley@panoramicfeedback.com>
3 #
4 #    This program is free software; you can redistribute it and/or modify
5 #    it under the terms of the GNU General Public License as published by
6 #    the Free Software Foundation; either version 2 of the License, or
7 #    (at your option) any later version.
8 #
9 #    This program is distributed in the hope that it will be useful,
10 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #    GNU General Public License for more details.
13 #
14 #    You should have received a copy of the GNU General Public License
15 #    along with this program; if not, write to the Free Software
16 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17 import os
18 import os.path
19 import errno
20 import time
21 import doctest
22
23 from beuuid import uuid_gen
24 import mapfile
25 import comment
26 import utility
27
28
29 ### Define and describe valid bug categories
30 # Use a tuple of (category, description) tuples since we don't have
31 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
32
33 # in order of increasing severity
34 severity_level_def = (
35   ("wishlist","A feature that could improve usefullness, but not a bug."),
36   ("minor","The standard bug level."),
37   ("serious","A bug that requires workarounds."),
38   ("critical","A bug that prevents some features from working at all."),
39   ("fatal","A bug that makes the package unusable."))
40
41 # in order of increasing resolution
42 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
43 active_status_def = (
44   ("unconfirmed","A possible bug which lacks independent existance confirmation."),
45   ("open","A working bug that has not been assigned to a developer."),
46   ("assigned","A working bug that has been assigned to a developer."),
47   ("test","The code has been adjusted, but the fix is still being tested."))
48 inactive_status_def = (
49   ("closed", "The bug is no longer relevant."),
50   ("fixed", "The bug should no longer occur."),
51   ("wontfix","It's not a bug, it's a feature."),
52   ("disabled", "?"))
53
54
55 ### Convert the description tuples to more useful formats
56
57 severity_values = tuple([val for val,description in severity_level_def])
58 severity_description = dict(severity_level_def)
59 severity_index = {}
60 for i in range(len(severity_values)):
61     severity_index[severity_values[i]] = i
62
63 active_status_values = tuple(val for val,description in active_status_def)
64 inactive_status_values = tuple(val for val,description in inactive_status_def)
65 status_values = active_status_values + inactive_status_values
66 status_description = dict(active_status_def+inactive_status_def)
67 status_index = {}
68 for i in range(len(status_values)):
69     status_index[status_values[i]] = i
70
71
72 def checked_property(name, valid):
73     """
74     Provide access to an attribute name, testing for valid values.
75     """
76     def getter(self):
77         value = getattr(self, "_"+name)
78         if value not in valid:
79             raise InvalidValue(name, value)
80         return value
81
82     def setter(self, value):
83         if value not in valid:
84             raise InvalidValue(name, value)
85         return setattr(self, "_"+name, value)
86     return property(getter, setter)
87
88
89 class Bug(object):
90     severity = checked_property("severity", severity_values)
91     status = checked_property("status", status_values)
92
93     def _get_active(self):
94         return self.status in active_status_values
95
96     active = property(_get_active)
97
98     def __init__(self, bugdir=None, uuid=None, from_disk=False,
99                  load_comments=False, summary=None):
100         self.bugdir = bugdir
101         if bugdir != None:
102             self.rcs = bugdir.rcs
103         else:
104             self.rcs = None
105         if from_disk == True:
106             self._comments_loaded = False
107             self.uuid = uuid
108             self.load(load_comments=load_comments)
109         else:
110             # Note: defaults should match those in Bug.load()
111             self._comments_loaded = True
112             if uuid != None:
113                 self.uuid = uuid
114             else:
115                 self.uuid = uuid_gen()
116             self.summary = summary
117             if self.rcs != None:
118                 self.creator = self.rcs.get_user_id()
119             else:
120                 self.creator = None
121             self.target = None
122             self.status = "open"
123             self.severity = "minor"
124             self.assigned = None
125             self.time = int(time.time()) # only save to second precision
126             self.comment_root = comment.Comment(self, uuid=comment.INVALID_UUID)
127
128     def __repr__(self):
129         return "Bug(uuid=%r)" % self.uuid
130
131     def xml(self, show_comments=False):
132         if self.bugdir == None:
133             shortname = self.uuid
134         else:
135             shortname = self.bugdir.bug_shortname(self)
136
137         if self.time == None:
138             timestring = ""
139         else:
140             htime = utility.handy_time(self.time)
141             ftime = utility.time_to_str(self.time)
142             timestring = "%s (%s)" % (htime, ftime)
143
144         info = [("uuid", self.uuid),
145                 ("short-name", shortname),
146                 ("severity", self.severity),
147                 ("status", self.status),
148                 ("assigned", self.assigned),
149                 ("target", self.target),
150                 ("creator", self.creator),
151                 ("created", timestring),
152                 ("summary", self.summary)]
153         ret = '<bug>\n'
154         for (k,v) in info:
155             if v is not None:
156                 ret += '  <%s>%s</%s>\n' % (k,v,k)
157
158         if show_comments:
159             if self._comments_loaded == False:
160                 self.load_comments()
161             comout = self.comment_root.xml_thread(auto_name_map=True,
162                                                   bug_shortname=shortname)
163             ret += comout
164
165         ret += '</bug>'
166         return ret
167
168     def string(self, shortlist=False, show_comments=False):
169         if self.bugdir == None:
170             shortname = self.uuid
171         else:
172             shortname = self.bugdir.bug_shortname(self)
173         if shortlist == False:
174             if self.time == None:
175                 timestring = ""
176             else:
177                 htime = utility.handy_time(self.time)
178                 ftime = utility.time_to_str(self.time)
179                 timestring = "%s (%s)" % (htime, ftime)
180             info = [("ID", self.uuid),
181                     ("Short name", shortname),
182                     ("Severity", self.severity),
183                     ("Status", self.status),
184                     ("Assigned", self.assigned),
185                     ("Target", self.target),
186                     ("Creator", self.creator),
187                     ("Created", timestring)]
188             newinfo = []
189             for k,v in info:
190                 if v == None:
191                     newinfo.append((k,""))
192                 else:
193                     newinfo.append((k,v))
194             info = newinfo
195             longest_key_len = max([len(k) for k,v in info])
196             infolines = ["  %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
197             bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
198         else:
199             statuschar = self.status[0]
200             severitychar = self.severity[0]
201             chars = "%c%c" % (statuschar, severitychar)
202             bugout = "%s:%s: %s" % (shortname, chars, self.summary.rstrip('\n'))
203         
204         if show_comments == True:
205             if self._comments_loaded == False:
206                 self.load_comments()
207             comout = self.comment_root.string_thread(auto_name_map=True,
208                                                      bug_shortname=shortname)
209             output = bugout + '\n' + comout.rstrip('\n')
210         else :
211             output = bugout
212         return output
213
214     def __str__(self):
215         return self.string(shortlist=True)
216
217     def __cmp__(self, other):
218         return cmp_full(self, other)
219
220     def get_path(self, name=None):
221         my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
222         if name is None:
223             return my_dir
224         assert name in ["values", "comments"]
225         return os.path.join(my_dir, name)
226
227     def load(self, load_comments=False):
228         map = mapfile.map_load(self.rcs, self.get_path("values"))
229         self.summary = map.get("summary")
230         self.creator = map.get("creator")
231         self.target = map.get("target")
232         self.status = map.get("status", "open")
233         self.severity = map.get("severity", "minor")
234         self.assigned = map.get("assigned")
235         self.time = map.get("time")
236         if self.time is not None:
237             self.time = utility.str_to_time(self.time)
238
239         if load_comments == True:
240             self.load_comments()
241
242     def load_comments(self):
243         self.comment_root = comment.loadComments(self)
244         self._comments_loaded = True
245
246     def comments(self):
247         if self._comments_loaded == False:
248             self.load_comments()
249         for comment in self.comment_root.traverse():
250             yield comment
251
252     def _add_attr(self, map, name):
253         value = getattr(self, name)
254         if value is not None:
255             map[name] = value
256
257     def save(self):
258         assert self.summary != None, "Can't save blank bug"
259         map = {}
260         self._add_attr(map, "assigned")
261         self._add_attr(map, "summary")
262         self._add_attr(map, "creator")
263         self._add_attr(map, "target")
264         self._add_attr(map, "status")
265         self._add_attr(map, "severity")
266         if self.time is not None:
267             map["time"] = utility.time_to_str(self.time)
268
269         self.rcs.mkdir(self.get_path())
270         path = self.get_path("values")
271         mapfile.map_save(self.rcs, path, map)
272
273         if self._comments_loaded:
274             if len(self.comment_root) > 0:
275                 self.rcs.mkdir(self.get_path("comments"))
276                 comment.saveComments(self)
277
278     def remove(self):
279         self.load_comments()
280         self.comment_root.remove()
281         path = self.get_path()
282         self.rcs.recursive_remove(path)
283     
284     def new_comment(self, body=None):
285         comm = comment.comment_root.new_reply(body=body)
286         return comm
287
288     def comment_from_shortname(self, shortname, *args, **kwargs):
289         return self.comment_root.comment_from_shortname(shortname, *args, **kwargs)
290
291     def comment_from_uuid(self, uuid):
292         return self.comment_root.comment_from_uuid(uuid)
293
294
295 # the general rule for bug sorting is that "more important" bugs are
296 # less than "less important" bugs.  This way sorting a list of bugs
297 # will put the most important bugs first in the list.  When relative
298 # importance is unclear, the sorting follows some arbitrary convention
299 # (i.e. dictionary order).
300
301 def cmp_severity(bug_1, bug_2):
302     """
303     Compare the severity levels of two bugs, with more severe bugs
304     comparing as less.
305     >>> bugA = Bug()
306     >>> bugB = Bug()
307     >>> bugA.severity = bugB.severity = "wishlist"
308     >>> cmp_severity(bugA, bugB) == 0
309     True
310     >>> bugB.severity = "minor"
311     >>> cmp_severity(bugA, bugB) > 0
312     True
313     >>> bugA.severity = "critical"
314     >>> cmp_severity(bugA, bugB) < 0
315     True
316     """
317     if not hasattr(bug_2, "severity") :
318         return 1
319     return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
320
321 def cmp_status(bug_1, bug_2):
322     """
323     Compare the status levels of two bugs, with more 'open' bugs
324     comparing as less.
325     >>> bugA = Bug()
326     >>> bugB = Bug()
327     >>> bugA.status = bugB.status = "open"
328     >>> cmp_status(bugA, bugB) == 0
329     True
330     >>> bugB.status = "closed"
331     >>> cmp_status(bugA, bugB) < 0
332     True
333     >>> bugA.status = "fixed"
334     >>> cmp_status(bugA, bugB) > 0
335     True
336     """
337     if not hasattr(bug_2, "status") :
338         return 1
339     val_2 = status_index[bug_2.status]
340     return cmp(status_index[bug_1.status], status_index[bug_2.status])
341
342 def cmp_attr(bug_1, bug_2, attr, invert=False):
343     """
344     Compare a general attribute between two bugs using the conventional
345     comparison rule for that attribute type.  If invert == True, sort
346     *against* that convention.
347     >>> attr="severity"
348     >>> bugA = Bug()
349     >>> bugB = Bug()
350     >>> bugA.severity = "critical"
351     >>> bugB.severity = "wishlist"
352     >>> cmp_attr(bugA, bugB, attr) < 0
353     True
354     >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
355     True
356     >>> bugB.severity = "critical"
357     >>> cmp_attr(bugA, bugB, attr) == 0
358     True
359     """
360     if not hasattr(bug_2, attr) :
361         return 1
362     if invert == True :
363         return -cmp(getattr(bug_1, attr), getattr(bug_2, attr))
364     else :
365         return cmp(getattr(bug_1, attr), getattr(bug_2, attr))
366
367 # alphabetical rankings (a < z)
368 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
369 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
370 # chronological rankings (newer < older)
371 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
372
373 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
374                                      cmp_time,cmp_creator)):
375     for comparison in cmp_list :
376         val = comparison(bug_1, bug_2)
377         if val != 0 :
378             return val
379     return 0
380
381 class InvalidValue(ValueError):
382     def __init__(self, name, value):
383         msg = "Cannot assign value %s to %s" % (value, name)
384         Exception.__init__(self, msg)
385         self.name = name
386         self.value = value
387
388 suite = doctest.DocTestSuite()