1 # Copyright (C) 2008-2009 Chris Ball <cjb@laptop.org>
2 # Thomas Habets <thomas@habets.pp.se>
3 # W. Trevor King <wking@drexel.edu>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 import xml.sax.saxutils
26 from beuuid import uuid_gen
27 from properties import Property, doc_property, local_property, \
28 defaulting_property, checked_property, cached_property, \
29 primed_property, change_hook_property, settings_property
30 import settings_object
36 ### Define and describe valid bug categories
37 # Use a tuple of (category, description) tuples since we don't have
38 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
40 # in order of increasing severity. (name, description) pairs
42 ("wishlist","A feature that could improve usefulness, but not a bug."),
43 ("minor","The standard bug level."),
44 ("serious","A bug that requires workarounds."),
45 ("critical","A bug that prevents some features from working at all."),
46 ("fatal","A bug that makes the package unusable."))
48 # in order of increasing resolution
49 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
51 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
52 ("open","A working bug that has not been assigned to a developer."),
53 ("assigned","A working bug that has been assigned to a developer."),
54 ("test","The code has been adjusted, but the fix is still being tested."))
55 inactive_status_def = (
56 ("closed", "The bug is no longer relevant."),
57 ("fixed", "The bug should no longer occur."),
58 ("wontfix","It's not a bug, it's a feature."))
61 ### Convert the description tuples to more useful formats
64 severity_description = {}
66 def load_severities(severity_def):
67 global severity_values
68 global severity_description
70 if severity_def == None:
72 severity_values = tuple([val for val,description in severity_def])
73 severity_description = dict(severity_def)
75 for i,severity in enumerate(severity_values):
76 severity_index[severity] = i
77 load_severities(severity_def)
79 active_status_values = []
80 inactive_status_values = []
82 status_description = {}
84 def load_status(active_status_def, inactive_status_def):
85 global active_status_values
86 global inactive_status_values
88 global status_description
90 if active_status_def == None:
91 active_status_def = globals()["active_status_def"]
92 if inactive_status_def == None:
93 inactive_status_def = globals()["inactive_status_def"]
94 active_status_values = tuple([val for val,description in active_status_def])
95 inactive_status_values = tuple([val for val,description in inactive_status_def])
96 status_values = active_status_values + inactive_status_values
97 status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
99 for i,status in enumerate(status_values):
100 status_index[status] = i
101 load_status(active_status_def, inactive_status_def)
104 class Bug(settings_object.SavedSettingsObject):
112 There are two formats for time, int and string. Setting either
113 one will adjust the other appropriately. The string form is the
114 one stored in the bug's settings file on disk.
115 >>> print type(b.time)
117 >>> print type(b.time_string)
120 >>> print b.time_string
121 Thu, 01 Jan 1970 00:00:00 +0000
122 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
125 >>> print b.settings["time"]
126 Thu, 01 Jan 1970 00:01:00 +0000
128 settings_properties = []
129 required_saved_properties = []
130 _prop_save_settings = settings_object.prop_save_settings
131 _prop_load_settings = settings_object.prop_load_settings
132 def _versioned_property(settings_properties=settings_properties,
133 required_saved_properties=required_saved_properties,
135 if "settings_properties" not in kwargs:
136 kwargs["settings_properties"] = settings_properties
137 if "required_saved_properties" not in kwargs:
138 kwargs["required_saved_properties"]=required_saved_properties
139 return settings_object.versioned_property(**kwargs)
141 @_versioned_property(name="severity",
142 doc="A measure of the bug's importance",
144 check_fn=lambda s: s in severity_values,
146 def severity(): return {}
148 @_versioned_property(name="status",
149 doc="The bug's current status",
151 check_fn=lambda s: s in status_values,
153 def status(): return {}
157 return self.status in active_status_values
159 @_versioned_property(name="target",
160 doc="The deadline for fixing this bug")
161 def target(): return {}
163 @_versioned_property(name="creator",
164 doc="The user who entered the bug into the system")
165 def creator(): return {}
167 @_versioned_property(name="reporter",
168 doc="The user who reported the bug")
169 def reporter(): return {}
171 @_versioned_property(name="assigned",
172 doc="The developer in charge of the bug")
173 def assigned(): return {}
175 @_versioned_property(name="time",
176 doc="An RFC 2822 timestamp for bug creation")
177 def time_string(): return {}
180 if self.time_string == None:
182 return utility.str_to_time(self.time_string)
183 def _set_time(self, value):
184 self.time_string = utility.time_to_str(value)
185 time = property(fget=_get_time,
187 doc="An integer version of .time_string")
189 def _extra_strings_check_fn(value):
190 "Require an iterable full of strings"
191 if value == settings_object.EMPTY:
193 elif not hasattr(value, "__iter__"):
196 if type(x) not in types.StringTypes:
199 def _extra_strings_change_hook(self, old, new):
200 self.extra_strings.sort() # to make merging easier
201 self._prop_save_settings(old, new)
202 @_versioned_property(name="extra_strings",
203 doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
205 check_fn=_extra_strings_check_fn,
206 change_hook=_extra_strings_change_hook,
208 def extra_strings(): return {}
210 @_versioned_property(name="summary",
211 doc="A one-line bug description")
212 def summary(): return {}
214 def _get_comment_root(self, load_full=False):
215 if self.sync_with_disk:
216 return comment.loadComments(self, load_full=load_full)
218 return comment.Comment(self, uuid=comment.INVALID_UUID)
221 @cached_property(generator=_get_comment_root)
222 @local_property("comment_root")
223 @doc_property(doc="The trunk of the comment tree")
224 def comment_root(): return {}
227 if hasattr(self.bugdir, "rcs"):
228 return self.bugdir.rcs
231 @cached_property(generator=_get_rcs)
232 @local_property("rcs")
233 @doc_property(doc="A revision control system instance.")
236 def __init__(self, bugdir=None, uuid=None, from_disk=False,
237 load_comments=False, summary=None):
238 settings_object.SavedSettingsObject.__init__(self)
241 if from_disk == True:
242 self.sync_with_disk = True
244 self.sync_with_disk = False
246 self.uuid = uuid_gen()
247 self.time = int(time.time()) # only save to second precision
249 self.creator = self.rcs.get_user_id()
250 self.summary = summary
253 return "Bug(uuid=%r)" % self.uuid
255 def _setting_attr_string(self, setting):
256 value = getattr(self, setting)
261 def xml(self, show_comments=False):
262 if self.bugdir == None:
263 shortname = self.uuid
265 shortname = self.bugdir.bug_shortname(self)
267 if self.time == None:
270 timestring = utility.time_to_str(self.time)
272 info = [("uuid", self.uuid),
273 ("short-name", shortname),
274 ("severity", self.severity),
275 ("status", self.status),
276 ("assigned", self.assigned),
277 ("target", self.target),
278 ("reporter", self.reporter),
279 ("creator", self.creator),
280 ("created", timestring),
281 ("summary", self.summary)]
285 ret += ' <%s>%s</%s>\n' % (k,xml.sax.saxutils.escape(v),k)
286 for estr in self.extra_strings:
287 ret += ' <extra-string>%s</extra-string>\n' % estr
288 if show_comments == True:
289 comout = self.comment_root.xml_thread(auto_name_map=True,
290 bug_shortname=shortname)
296 def string(self, shortlist=False, show_comments=False):
297 if self.bugdir == None:
298 shortname = self.uuid
300 shortname = self.bugdir.bug_shortname(self)
301 if shortlist == False:
302 if self.time == None:
305 htime = utility.handy_time(self.time)
306 timestring = "%s (%s)" % (htime, self.time_string)
307 info = [("ID", self.uuid),
308 ("Short name", shortname),
309 ("Severity", self.severity),
310 ("Status", self.status),
311 ("Assigned", self._setting_attr_string("assigned")),
312 ("Target", self._setting_attr_string("target")),
313 ("Reporter", self._setting_attr_string("reporter")),
314 ("Creator", self._setting_attr_string("creator")),
315 ("Created", timestring)]
316 longest_key_len = max([len(k) for k,v in info])
317 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
318 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
320 statuschar = self.status[0]
321 severitychar = self.severity[0]
322 chars = "%c%c" % (statuschar, severitychar)
323 bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
325 if show_comments == True:
326 # take advantage of the string_thread(auto_name_map=True)
327 # SIDE-EFFECT of sorting by comment time.
328 comout = self.comment_root.string_thread(flatten=False,
330 bug_shortname=shortname)
331 output = bugout + '\n' + comout.rstrip('\n')
337 return self.string(shortlist=True)
339 def __cmp__(self, other):
340 return cmp_full(self, other)
342 def get_path(self, name=None):
343 my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
346 assert name in ["values", "comments"]
347 return os.path.join(my_dir, name)
349 def load_settings(self):
350 self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
351 self._setup_saved_settings()
353 def load_comments(self, load_full=True):
354 if load_full == True:
355 # Force a complete load of the whole comment tree
356 self.comment_root = self._get_comment_root(load_full=True)
358 # Setup for fresh lazy-loading. Clear _comment_root, so
359 # _get_comment_root returns a fresh version. Turn of
360 # syncing temporarily so we don't write our blank comment
362 self.sync_with_disk = False
363 self.comment_root = None
364 self.sync_with_disk = True
366 def save_settings(self):
367 assert self.summary != None, "Can't save blank bug"
369 self.rcs.mkdir(self.get_path())
370 path = self.get_path("values")
371 mapfile.map_save(self.rcs, path, self._get_saved_settings())
376 if len(self.comment_root) > 0:
377 self.rcs.mkdir(self.get_path("comments"))
378 comment.saveComments(self)
381 self.comment_root.remove()
382 path = self.get_path()
383 self.rcs.recursive_remove(path)
386 for comment in self.comment_root.traverse():
389 def new_comment(self, body=None):
390 comm = self.comment_root.new_reply(body=body)
393 def comment_from_shortname(self, shortname, *args, **kwargs):
394 return self.comment_root.comment_from_shortname(shortname,
397 def comment_from_uuid(self, uuid):
398 return self.comment_root.comment_from_uuid(uuid)
400 def comment_shortnames(self, shortname=None):
402 SIDE-EFFECT : Comment.comment_shortnames will sort the comment
405 for id, comment in self.comment_root.comment_shortnames(shortname):
409 # The general rule for bug sorting is that "more important" bugs are
410 # less than "less important" bugs. This way sorting a list of bugs
411 # will put the most important bugs first in the list. When relative
412 # importance is unclear, the sorting follows some arbitrary convention
413 # (i.e. dictionary order).
415 def cmp_severity(bug_1, bug_2):
417 Compare the severity levels of two bugs, with more severe bugs
421 >>> bugA.severity = bugB.severity = "wishlist"
422 >>> cmp_severity(bugA, bugB) == 0
424 >>> bugB.severity = "minor"
425 >>> cmp_severity(bugA, bugB) > 0
427 >>> bugA.severity = "critical"
428 >>> cmp_severity(bugA, bugB) < 0
431 if not hasattr(bug_2, "severity") :
433 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
435 def cmp_status(bug_1, bug_2):
437 Compare the status levels of two bugs, with more 'open' bugs
441 >>> bugA.status = bugB.status = "open"
442 >>> cmp_status(bugA, bugB) == 0
444 >>> bugB.status = "closed"
445 >>> cmp_status(bugA, bugB) < 0
447 >>> bugA.status = "fixed"
448 >>> cmp_status(bugA, bugB) > 0
451 if not hasattr(bug_2, "status") :
453 val_2 = status_index[bug_2.status]
454 return cmp(status_index[bug_1.status], status_index[bug_2.status])
456 def cmp_attr(bug_1, bug_2, attr, invert=False):
458 Compare a general attribute between two bugs using the conventional
459 comparison rule for that attribute type. If invert == True, sort
460 *against* that convention.
464 >>> bugA.severity = "critical"
465 >>> bugB.severity = "wishlist"
466 >>> cmp_attr(bugA, bugB, attr) < 0
468 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
470 >>> bugB.severity = "critical"
471 >>> cmp_attr(bugA, bugB, attr) == 0
474 if not hasattr(bug_2, attr) :
476 val_1 = getattr(bug_1, attr)
477 val_2 = getattr(bug_2, attr)
478 if val_1 == None: val_1 = None
479 if val_2 == None: val_2 = None
482 return -cmp(val_1, val_2)
484 return cmp(val_1, val_2)
486 # alphabetical rankings (a < z)
487 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
488 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
489 # chronological rankings (newer < older)
490 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
492 DEFAULT_CMP_FULL_CMP_LIST = \
493 (cmp_status,cmp_severity,cmp_assigned,cmp_time,cmp_creator)
495 class BugCompoundComparator (object):
496 def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
497 self.cmp_list = cmp_list
498 def __call__(self, bug_1, bug_2):
499 for comparison in self.cmp_list :
500 val = comparison(bug_1, bug_2)
505 cmp_full = BugCompoundComparator()
508 # define some bonus cmp_* functions
509 def cmp_last_modified(bug_1, bug_2):
511 Like cmp_time(), but use most recent comment instead of bug
512 creation for the timestamp.
514 def last_modified(bug):
516 for comment in bug.comment_root.traverse():
517 if comment.time > time:
520 val_1 = last_modified(bug_1)
521 val_2 = last_modified(bug_2)
522 return -cmp(val_1, val_2)
525 suite = doctest.DocTestSuite()