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
23 from beuuid import uuid_gen
24 from properties import Property, doc_property, local_property, \
25 defaulting_property, checked_property, cached_property, \
26 primed_property, change_hook_property, settings_property
27 import settings_object
33 ### Define and describe valid bug categories
34 # Use a tuple of (category, description) tuples since we don't have
35 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
37 # in order of increasing severity. (name, description) pairs
39 ("wishlist","A feature that could improve usefulness, but not a bug."),
40 ("minor","The standard bug level."),
41 ("serious","A bug that requires workarounds."),
42 ("critical","A bug that prevents some features from working at all."),
43 ("fatal","A bug that makes the package unusable."))
45 # in order of increasing resolution
46 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
48 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
49 ("open","A working bug that has not been assigned to a developer."),
50 ("assigned","A working bug that has been assigned to a developer."),
51 ("test","The code has been adjusted, but the fix is still being tested."))
52 inactive_status_def = (
53 ("closed", "The bug is no longer relevant."),
54 ("fixed", "The bug should no longer occur."),
55 ("wontfix","It's not a bug, it's a feature."))
58 ### Convert the description tuples to more useful formats
61 severity_description = {}
63 def load_severities(severity_def):
64 global severity_values
65 global severity_description
67 if severity_def == settings_object.EMPTY:
69 severity_values = tuple([val for val,description in severity_def])
70 severity_description = dict(severity_def)
72 for i,severity in enumerate(severity_values):
73 severity_index[severity] = i
74 load_severities(severity_def)
76 active_status_values = []
77 inactive_status_values = []
79 status_description = {}
81 def load_status(active_status_def, inactive_status_def):
82 global active_status_values
83 global inactive_status_values
85 global status_description
87 if active_status_def == settings_object.EMPTY:
88 active_status_def = globals()["active_status_def"]
89 if inactive_status_def == settings_object.EMPTY:
90 inactive_status_def = globals()["inactive_status_def"]
91 active_status_values = tuple([val for val,description in active_status_def])
92 inactive_status_values = tuple([val for val,description in inactive_status_def])
93 status_values = active_status_values + inactive_status_values
94 status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
96 for i,status in enumerate(status_values):
97 status_index[status] = i
98 load_status(active_status_def, inactive_status_def)
101 class Bug(settings_object.SavedSettingsObject):
109 There are two formats for time, int and string. Setting either
110 one will adjust the other appropriately. The string form is the
111 one stored in the bug's settings file on disk.
112 >>> print type(b.time)
114 >>> print type(b.time_string)
117 >>> print b.time_string
118 Thu, 01 Jan 1970 00:00:00 +0000
119 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
122 >>> print b.settings["time"]
123 Thu, 01 Jan 1970 00:01:00 +0000
125 settings_properties = []
126 required_saved_properties = []
127 _prop_save_settings = settings_object.prop_save_settings
128 _prop_load_settings = settings_object.prop_load_settings
129 def _versioned_property(settings_properties=settings_properties,
130 required_saved_properties=required_saved_properties,
132 if "settings_properties" not in kwargs:
133 kwargs["settings_properties"] = settings_properties
134 if "required_saved_properties" not in kwargs:
135 kwargs["required_saved_properties"]=required_saved_properties
136 return settings_object.versioned_property(**kwargs)
138 @_versioned_property(name="severity",
139 doc="A measure of the bug's importance",
141 check_fn=lambda s: s in severity_values,
143 def severity(): return {}
145 @_versioned_property(name="status",
146 doc="The bug's current status",
148 check_fn=lambda s: s in status_values,
150 def status(): return {}
154 return self.status in active_status_values
156 @_versioned_property(name="target",
157 doc="The deadline for fixing this bug")
158 def target(): return {}
160 @_versioned_property(name="creator",
161 doc="The user who entered the bug into the system")
162 def creator(): return {}
164 @_versioned_property(name="reporter",
165 doc="The user who reported the bug")
166 def reporter(): return {}
168 @_versioned_property(name="assigned",
169 doc="The developer in charge of the bug")
170 def assigned(): return {}
172 @_versioned_property(name="time",
173 doc="An RFC 2822 timestamp for bug creation")
174 def time_string(): return {}
177 if self.time_string == None or self.time_string == settings_object.EMPTY:
179 return utility.str_to_time(self.time_string)
180 def _set_time(self, value):
181 self.time_string = utility.time_to_str(value)
182 time = property(fget=_get_time,
184 doc="An integer version of .time_string")
186 @_versioned_property(name="summary",
187 doc="A one-line bug description")
188 def summary(): return {}
190 def _get_comment_root(self, load_full=False):
191 if self.sync_with_disk:
192 return comment.loadComments(self, load_full=load_full)
194 return comment.Comment(self, uuid=comment.INVALID_UUID)
197 @cached_property(generator=_get_comment_root)
198 @local_property("comment_root")
199 @doc_property(doc="The trunk of the comment tree")
200 def comment_root(): return {}
203 if hasattr(self.bugdir, "rcs"):
204 return self.bugdir.rcs
207 @cached_property(generator=_get_rcs)
208 @local_property("rcs")
209 @doc_property(doc="A revision control system instance.")
212 def __init__(self, bugdir=None, uuid=None, from_disk=False,
213 load_comments=False, summary=None):
214 settings_object.SavedSettingsObject.__init__(self)
217 if from_disk == True:
218 self.sync_with_disk = True
220 self.sync_with_disk = False
222 self.uuid = uuid_gen()
223 self.time = int(time.time()) # only save to second precision
225 self.creator = self.rcs.get_user_id()
226 self.summary = summary
229 return "Bug(uuid=%r)" % self.uuid
231 def _setting_attr_string(self, setting):
232 value = getattr(self, setting)
233 if value == settings_object.EMPTY:
238 def xml(self, show_comments=False):
239 if self.bugdir == None:
240 shortname = self.uuid
242 shortname = self.bugdir.bug_shortname(self)
244 if self.time == None:
247 htime = utility.handy_time(self.time)
248 ftime = utility.time_to_str(self.time)
249 timestring = "%s (%s)" % (htime, ftime)
251 info = [("uuid", self.uuid),
252 ("short-name", shortname),
253 ("severity", self.severity),
254 ("status", self.status),
255 ("assigned", self.assigned),
256 ("target", self.target),
257 ("reporter", self.reporter),
258 ("creator", self.creator),
259 ("created", timestring),
260 ("summary", self.summary)]
264 ret += ' <%s>%s</%s>\n' % (k,v,k)
266 if show_comments == True:
267 comout = self.comment_root.xml_thread(auto_name_map=True,
268 bug_shortname=shortname)
274 def string(self, shortlist=False, show_comments=False):
275 if self.bugdir == None:
276 shortname = self.uuid
278 shortname = self.bugdir.bug_shortname(self)
279 if shortlist == False:
280 if self.time_string == "":
281 timestring = self.time_string
283 htime = utility.handy_time(self.time)
284 timestring = "%s (%s)" % (htime, self.time_string)
285 info = [("ID", self.uuid),
286 ("Short name", shortname),
287 ("Severity", self.severity),
288 ("Status", self.status),
289 ("Assigned", self._setting_attr_string("assigned")),
290 ("Target", self._setting_attr_string("target")),
291 ("Reporter", self._setting_attr_string("reporter")),
292 ("Creator", self._setting_attr_string("creator")),
293 ("Created", timestring)]
294 longest_key_len = max([len(k) for k,v in info])
295 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
296 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
298 statuschar = self.status[0]
299 severitychar = self.severity[0]
300 chars = "%c%c" % (statuschar, severitychar)
301 bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
303 if show_comments == True:
304 # take advantage of the string_thread(auto_name_map=True)
305 # SIDE-EFFECT of sorting by comment time.
306 comout = self.comment_root.string_thread(flatten=False,
308 bug_shortname=shortname)
309 output = bugout + '\n' + comout.rstrip('\n')
315 return self.string(shortlist=True)
317 def __cmp__(self, other):
318 return cmp_full(self, other)
320 def get_path(self, name=None):
321 my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
324 assert name in ["values", "comments"]
325 return os.path.join(my_dir, name)
327 def load_settings(self):
328 self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
329 self._setup_saved_settings()
331 def load_comments(self, load_full=True):
332 if load_full == True:
333 # Force a complete load of the whole comment tree
334 self.comment_root = self._get_comment_root(load_full=True)
336 # Setup for fresh lazy-loading. Clear _comment_root, so
337 # _get_comment_root returns a fresh version. Turn of
338 # syncing temporarily so we don't write our blank comment
340 self.sync_with_disk = False
341 self.comment_root = None
342 self.sync_with_disk = True
344 def save_settings(self):
345 assert self.summary != None, "Can't save blank bug"
347 self.rcs.mkdir(self.get_path())
348 path = self.get_path("values")
349 mapfile.map_save(self.rcs, path, self._get_saved_settings())
354 if len(self.comment_root) > 0:
355 self.rcs.mkdir(self.get_path("comments"))
356 comment.saveComments(self)
359 self.comment_root.remove()
360 path = self.get_path()
361 self.rcs.recursive_remove(path)
364 for comment in self.comment_root.traverse():
367 def new_comment(self, body=None):
368 comm = self.comment_root.new_reply(body=body)
371 def comment_from_shortname(self, shortname, *args, **kwargs):
372 return self.comment_root.comment_from_shortname(shortname,
375 def comment_from_uuid(self, uuid):
376 return self.comment_root.comment_from_uuid(uuid)
378 def comment_shortnames(self, shortname=None):
380 SIDE-EFFECT : Comment.comment_shortnames will sort the comment
383 for id, comment in self.comment_root.comment_shortnames(shortname):
387 # The general rule for bug sorting is that "more important" bugs are
388 # less than "less important" bugs. This way sorting a list of bugs
389 # will put the most important bugs first in the list. When relative
390 # importance is unclear, the sorting follows some arbitrary convention
391 # (i.e. dictionary order).
393 def cmp_severity(bug_1, bug_2):
395 Compare the severity levels of two bugs, with more severe bugs
399 >>> bugA.severity = bugB.severity = "wishlist"
400 >>> cmp_severity(bugA, bugB) == 0
402 >>> bugB.severity = "minor"
403 >>> cmp_severity(bugA, bugB) > 0
405 >>> bugA.severity = "critical"
406 >>> cmp_severity(bugA, bugB) < 0
409 if not hasattr(bug_2, "severity") :
411 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
413 def cmp_status(bug_1, bug_2):
415 Compare the status levels of two bugs, with more 'open' bugs
419 >>> bugA.status = bugB.status = "open"
420 >>> cmp_status(bugA, bugB) == 0
422 >>> bugB.status = "closed"
423 >>> cmp_status(bugA, bugB) < 0
425 >>> bugA.status = "fixed"
426 >>> cmp_status(bugA, bugB) > 0
429 if not hasattr(bug_2, "status") :
431 val_2 = status_index[bug_2.status]
432 return cmp(status_index[bug_1.status], status_index[bug_2.status])
434 def cmp_attr(bug_1, bug_2, attr, invert=False):
436 Compare a general attribute between two bugs using the conventional
437 comparison rule for that attribute type. If invert == True, sort
438 *against* that convention.
442 >>> bugA.severity = "critical"
443 >>> bugB.severity = "wishlist"
444 >>> cmp_attr(bugA, bugB, attr) < 0
446 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
448 >>> bugB.severity = "critical"
449 >>> cmp_attr(bugA, bugB, attr) == 0
452 if not hasattr(bug_2, attr) :
454 val_1 = getattr(bug_1, attr)
455 val_2 = getattr(bug_2, attr)
456 if val_1 == settings_object.EMPTY: val_1 = None
457 if val_2 == settings_object.EMPTY: val_2 = None
460 return -cmp(val_1, val_2)
462 return cmp(val_1, val_2)
464 # alphabetical rankings (a < z)
465 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
466 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
467 # chronological rankings (newer < older)
468 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
470 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
471 cmp_time,cmp_creator)):
472 for comparison in cmp_list :
473 val = comparison(bug_1, bug_2)
478 class InvalidValue(ValueError):
479 def __init__(self, name, value):
480 msg = "Cannot assign value %s to %s" % (value, name)
481 Exception.__init__(self, msg)
485 suite = doctest.DocTestSuite()