1 # Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
2 # <abentley@panoramicfeedback.com>
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.
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.
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
22 import xml.sax.saxutils
25 from beuuid import uuid_gen
26 from properties import Property, doc_property, local_property, \
27 defaulting_property, checked_property, cached_property, \
28 primed_property, change_hook_property, settings_property
29 import settings_object
35 ### Define and describe valid bug categories
36 # Use a tuple of (category, description) tuples since we don't have
37 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
39 # in order of increasing severity. (name, description) pairs
41 ("wishlist","A feature that could improve usefulness, but not a bug."),
42 ("minor","The standard bug level."),
43 ("serious","A bug that requires workarounds."),
44 ("critical","A bug that prevents some features from working at all."),
45 ("fatal","A bug that makes the package unusable."))
47 # in order of increasing resolution
48 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
50 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
51 ("open","A working bug that has not been assigned to a developer."),
52 ("assigned","A working bug that has been assigned to a developer."),
53 ("test","The code has been adjusted, but the fix is still being tested."))
54 inactive_status_def = (
55 ("closed", "The bug is no longer relevant."),
56 ("fixed", "The bug should no longer occur."),
57 ("wontfix","It's not a bug, it's a feature."))
60 ### Convert the description tuples to more useful formats
63 severity_description = {}
65 def load_severities(severity_def):
66 global severity_values
67 global severity_description
69 if severity_def == settings_object.EMPTY:
71 severity_values = tuple([val for val,description in severity_def])
72 severity_description = dict(severity_def)
74 for i,severity in enumerate(severity_values):
75 severity_index[severity] = i
76 load_severities(severity_def)
78 active_status_values = []
79 inactive_status_values = []
81 status_description = {}
83 def load_status(active_status_def, inactive_status_def):
84 global active_status_values
85 global inactive_status_values
87 global status_description
89 if active_status_def == settings_object.EMPTY:
90 active_status_def = globals()["active_status_def"]
91 if inactive_status_def == settings_object.EMPTY:
92 inactive_status_def = globals()["inactive_status_def"]
93 active_status_values = tuple([val for val,description in active_status_def])
94 inactive_status_values = tuple([val for val,description in inactive_status_def])
95 status_values = active_status_values + inactive_status_values
96 status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
98 for i,status in enumerate(status_values):
99 status_index[status] = i
100 load_status(active_status_def, inactive_status_def)
103 class Bug(settings_object.SavedSettingsObject):
111 There are two formats for time, int and string. Setting either
112 one will adjust the other appropriately. The string form is the
113 one stored in the bug's settings file on disk.
114 >>> print type(b.time)
116 >>> print type(b.time_string)
119 >>> print b.time_string
120 Thu, 01 Jan 1970 00:00:00 +0000
121 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
124 >>> print b.settings["time"]
125 Thu, 01 Jan 1970 00:01:00 +0000
127 settings_properties = []
128 required_saved_properties = []
129 _prop_save_settings = settings_object.prop_save_settings
130 _prop_load_settings = settings_object.prop_load_settings
131 def _versioned_property(settings_properties=settings_properties,
132 required_saved_properties=required_saved_properties,
134 if "settings_properties" not in kwargs:
135 kwargs["settings_properties"] = settings_properties
136 if "required_saved_properties" not in kwargs:
137 kwargs["required_saved_properties"]=required_saved_properties
138 return settings_object.versioned_property(**kwargs)
140 @_versioned_property(name="severity",
141 doc="A measure of the bug's importance",
143 check_fn=lambda s: s in severity_values,
145 def severity(): return {}
147 @_versioned_property(name="status",
148 doc="The bug's current status",
150 check_fn=lambda s: s in status_values,
152 def status(): return {}
156 return self.status in active_status_values
158 @_versioned_property(name="target",
159 doc="The deadline for fixing this bug")
160 def target(): return {}
162 @_versioned_property(name="creator",
163 doc="The user who entered the bug into the system")
164 def creator(): return {}
166 @_versioned_property(name="reporter",
167 doc="The user who reported the bug")
168 def reporter(): return {}
170 @_versioned_property(name="assigned",
171 doc="The developer in charge of the bug")
172 def assigned(): return {}
174 @_versioned_property(name="time",
175 doc="An RFC 2822 timestamp for bug creation")
176 def time_string(): return {}
179 if self.time_string == None or self.time_string == settings_object.EMPTY:
181 return utility.str_to_time(self.time_string)
182 def _set_time(self, value):
183 self.time_string = utility.time_to_str(value)
184 time = property(fget=_get_time,
186 doc="An integer version of .time_string")
188 def _extra_strings_check_fn(value):
189 "Require an iterable full of strings"
190 if value == settings_object.EMPTY:
192 elif not hasattr(value, "__iter__"):
195 if type(x) not in types.StringTypes:
198 def _extra_strings_change_hook(self, old, new):
199 self.extra_strings.sort() # to make merging easier
200 self._prop_save_settings(old, new)
201 @_versioned_property(name="extra_strings",
202 doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
204 check_fn=_extra_strings_check_fn,
205 change_hook=_extra_strings_change_hook,
207 def extra_strings(): return {}
209 @_versioned_property(name="summary",
210 doc="A one-line bug description")
211 def summary(): return {}
213 def _get_comment_root(self, load_full=False):
214 if self.sync_with_disk:
215 return comment.loadComments(self, load_full=load_full)
217 return comment.Comment(self, uuid=comment.INVALID_UUID)
220 @cached_property(generator=_get_comment_root)
221 @local_property("comment_root")
222 @doc_property(doc="The trunk of the comment tree")
223 def comment_root(): return {}
226 if hasattr(self.bugdir, "rcs"):
227 return self.bugdir.rcs
230 @cached_property(generator=_get_rcs)
231 @local_property("rcs")
232 @doc_property(doc="A revision control system instance.")
235 def __init__(self, bugdir=None, uuid=None, from_disk=False,
236 load_comments=False, summary=None):
237 settings_object.SavedSettingsObject.__init__(self)
240 if from_disk == True:
241 self.sync_with_disk = True
243 self.sync_with_disk = False
245 self.uuid = uuid_gen()
246 self.time = int(time.time()) # only save to second precision
248 self.creator = self.rcs.get_user_id()
249 self.summary = summary
252 return "Bug(uuid=%r)" % self.uuid
254 def _setting_attr_string(self, setting):
255 value = getattr(self, setting)
256 if value == settings_object.EMPTY:
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)]
284 if v is not settings_object.EMPTY:
285 ret += ' <%s>%s</%s>\n' % (k,xml.sax.saxutils.escape(v),k)
287 if show_comments == True:
288 comout = self.comment_root.xml_thread(auto_name_map=True,
289 bug_shortname=shortname)
295 def string(self, shortlist=False, show_comments=False):
296 if self.bugdir == None:
297 shortname = self.uuid
299 shortname = self.bugdir.bug_shortname(self)
300 if shortlist == False:
301 if self.time == None:
304 htime = utility.handy_time(self.time)
305 timestring = "%s (%s)" % (htime, self.time_string)
306 info = [("ID", self.uuid),
307 ("Short name", shortname),
308 ("Severity", self.severity),
309 ("Status", self.status),
310 ("Assigned", self._setting_attr_string("assigned")),
311 ("Target", self._setting_attr_string("target")),
312 ("Reporter", self._setting_attr_string("reporter")),
313 ("Creator", self._setting_attr_string("creator")),
314 ("Created", timestring)]
315 longest_key_len = max([len(k) for k,v in info])
316 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
317 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
319 statuschar = self.status[0]
320 severitychar = self.severity[0]
321 chars = "%c%c" % (statuschar, severitychar)
322 bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
324 if show_comments == True:
325 # take advantage of the string_thread(auto_name_map=True)
326 # SIDE-EFFECT of sorting by comment time.
327 comout = self.comment_root.string_thread(flatten=False,
329 bug_shortname=shortname)
330 output = bugout + '\n' + comout.rstrip('\n')
336 return self.string(shortlist=True)
338 def __cmp__(self, other):
339 return cmp_full(self, other)
341 def get_path(self, name=None):
342 my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
345 assert name in ["values", "comments"]
346 return os.path.join(my_dir, name)
348 def load_settings(self):
349 self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
350 self._setup_saved_settings()
352 def load_comments(self, load_full=True):
353 if load_full == True:
354 # Force a complete load of the whole comment tree
355 self.comment_root = self._get_comment_root(load_full=True)
357 # Setup for fresh lazy-loading. Clear _comment_root, so
358 # _get_comment_root returns a fresh version. Turn of
359 # syncing temporarily so we don't write our blank comment
361 self.sync_with_disk = False
362 self.comment_root = None
363 self.sync_with_disk = True
365 def save_settings(self):
366 assert self.summary != None, "Can't save blank bug"
368 self.rcs.mkdir(self.get_path())
369 path = self.get_path("values")
370 mapfile.map_save(self.rcs, path, self._get_saved_settings())
375 if len(self.comment_root) > 0:
376 self.rcs.mkdir(self.get_path("comments"))
377 comment.saveComments(self)
380 self.comment_root.remove()
381 path = self.get_path()
382 self.rcs.recursive_remove(path)
385 for comment in self.comment_root.traverse():
388 def new_comment(self, body=None):
389 comm = self.comment_root.new_reply(body=body)
392 def comment_from_shortname(self, shortname, *args, **kwargs):
393 return self.comment_root.comment_from_shortname(shortname,
396 def comment_from_uuid(self, uuid):
397 return self.comment_root.comment_from_uuid(uuid)
399 def comment_shortnames(self, shortname=None):
401 SIDE-EFFECT : Comment.comment_shortnames will sort the comment
404 for id, comment in self.comment_root.comment_shortnames(shortname):
408 # The general rule for bug sorting is that "more important" bugs are
409 # less than "less important" bugs. This way sorting a list of bugs
410 # will put the most important bugs first in the list. When relative
411 # importance is unclear, the sorting follows some arbitrary convention
412 # (i.e. dictionary order).
414 def cmp_severity(bug_1, bug_2):
416 Compare the severity levels of two bugs, with more severe bugs
420 >>> bugA.severity = bugB.severity = "wishlist"
421 >>> cmp_severity(bugA, bugB) == 0
423 >>> bugB.severity = "minor"
424 >>> cmp_severity(bugA, bugB) > 0
426 >>> bugA.severity = "critical"
427 >>> cmp_severity(bugA, bugB) < 0
430 if not hasattr(bug_2, "severity") :
432 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
434 def cmp_status(bug_1, bug_2):
436 Compare the status levels of two bugs, with more 'open' bugs
440 >>> bugA.status = bugB.status = "open"
441 >>> cmp_status(bugA, bugB) == 0
443 >>> bugB.status = "closed"
444 >>> cmp_status(bugA, bugB) < 0
446 >>> bugA.status = "fixed"
447 >>> cmp_status(bugA, bugB) > 0
450 if not hasattr(bug_2, "status") :
452 val_2 = status_index[bug_2.status]
453 return cmp(status_index[bug_1.status], status_index[bug_2.status])
455 def cmp_attr(bug_1, bug_2, attr, invert=False):
457 Compare a general attribute between two bugs using the conventional
458 comparison rule for that attribute type. If invert == True, sort
459 *against* that convention.
463 >>> bugA.severity = "critical"
464 >>> bugB.severity = "wishlist"
465 >>> cmp_attr(bugA, bugB, attr) < 0
467 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
469 >>> bugB.severity = "critical"
470 >>> cmp_attr(bugA, bugB, attr) == 0
473 if not hasattr(bug_2, attr) :
475 val_1 = getattr(bug_1, attr)
476 val_2 = getattr(bug_2, attr)
477 if val_1 == settings_object.EMPTY: val_1 = None
478 if val_2 == settings_object.EMPTY: val_2 = None
481 return -cmp(val_1, val_2)
483 return cmp(val_1, val_2)
485 # alphabetical rankings (a < z)
486 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
487 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
488 # chronological rankings (newer < older)
489 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
491 DEFAULT_CMP_FULL_CMP_LIST = \
492 (cmp_status,cmp_severity,cmp_assigned,cmp_time,cmp_creator)
494 class BugCompoundComparator (object):
495 def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
496 self.cmp_list = cmp_list
497 def __call__(self, bug_1, bug_2):
498 for comparison in self.cmp_list :
499 val = comparison(bug_1, bug_2)
504 cmp_full = BugCompoundComparator()
507 # define some bonus cmp_* functions
508 def cmp_last_modified(bug_1, bug_2):
510 Like cmp_time(), but use most recent comment instead of bug
511 creation for the timestamp.
513 def last_modified(bug):
515 for comment in bug.comment_root.traverse():
516 if comment.time > time:
519 val_1 = last_modified(bug_1)
520 val_2 = last_modified(bug_2)
521 return -cmp(val_1, val_2)
524 suite = doctest.DocTestSuite()