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
32 ### Define and describe valid bug categories
33 # Use a tuple of (category, description) tuples since we don't have
34 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
36 # in order of increasing severity
37 severity_level_def = (
38 ("wishlist","A feature that could improve usefullness, but not a bug."),
39 ("minor","The standard bug level."),
40 ("serious","A bug that requires workarounds."),
41 ("critical","A bug that prevents some features from working at all."),
42 ("fatal","A bug that makes the package unusable."))
44 # in order of increasing resolution
45 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
47 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
48 ("open","A working bug that has not been assigned to a developer."),
49 ("assigned","A working bug that has been assigned to a developer."),
50 ("test","The code has been adjusted, but the fix is still being tested."))
51 inactive_status_def = (
52 ("closed", "The bug is no longer relevant."),
53 ("fixed", "The bug should no longer occur."),
54 ("wontfix","It's not a bug, it's a feature."),
58 ### Convert the description tuples to more useful formats
60 severity_values = tuple([val for val,description in severity_level_def])
61 severity_description = dict(severity_level_def)
63 for i in range(len(severity_values)):
64 severity_index[severity_values[i]] = i
66 active_status_values = tuple(val for val,description in active_status_def)
67 inactive_status_values = tuple(val for val,description in inactive_status_def)
68 status_values = active_status_values + inactive_status_values
69 status_description = dict(active_status_def+inactive_status_def)
71 for i in range(len(status_values)):
72 status_index[status_values[i]] = i
75 # Define an invalid value for our properties, distinct from None,
76 # which shows that a property has been initialized but has no value.
88 There are two formats for time, int and string. Setting either
89 one will adjust the other appropriately. The string form is the
90 one stored in the bug's settings file on disk.
91 >>> print type(b.time)
93 >>> print type(b.time_string)
96 >>> print b.time_string
97 Thu, 01 Jan 1970 00:00:00 +0000
98 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
101 >>> print b.settings["time"]
102 Thu, 01 Jan 1970 00:01:00 +0000
104 def _save_settings(self, old, new):
105 if self.sync_with_disk==True:
107 def _load_settings(self):
108 if self.sync_with_disk==True and self._settings_loaded==False:
111 for property in self.settings_properties:
112 if property not in self.settings:
113 self.settings[property] = EMPTY
115 settings_properties = []
116 required_saved_properties = ['status','severity'] # to protect against future changes in default values
118 def _versioned_property(name, doc, default=None, save=_save_settings, load=_load_settings, setprops=settings_properties, allowed=None):
119 "Combine the common decorators in a single function"
120 setprops.append(name)
121 def decorator(funcs):
123 checked = checked_property(allowed=allowed)
124 defaulting = defaulting_property(default=default, null=EMPTY)
125 change_hook = change_hook_property(hook=save)
126 primed = primed_property(primer=load)
127 settings = settings_property(name=name)
128 docp = doc_property(doc=doc)
129 deco = defaulting(change_hook(primed(settings(docp(funcs)))))
132 return Property(deco)
135 @_versioned_property(name="severity",
136 doc="A measure of the bug's importance",
138 allowed=severity_values)
139 def severity(): return {}
141 @_versioned_property(name="status",
142 doc="The bug's current status",
144 allowed=status_values)
145 def status(): return {}
149 return self.status in active_status_values
151 @_versioned_property(name="target",
152 doc="The deadline for fixing this bug")
153 def target(): return {}
155 @_versioned_property(name="creator",
156 doc="The user who entered the bug into the system")
157 def creator(): return {}
159 @_versioned_property(name="reporter",
160 doc="The user who reported the bug")
161 def reporter(): return {}
163 @_versioned_property(name="assigned",
164 doc="The developer in charge of the bug")
165 def assigned(): return {}
167 @_versioned_property(name="time",
168 doc="An RFC 2822 timestamp for bug creation")
169 def time_string(): return {}
172 if self.time_string == None:
174 return utility.str_to_time(self.time_string)
175 def _set_time(self, value):
176 self.time_string = utility.time_to_str(value)
177 time = property(fget=_get_time,
179 doc="An integer version of .time_string")
181 @_versioned_property(name="summary",
182 doc="A one-line bug description")
183 def summary(): return {}
185 def _get_comment_root(self, load_full=False):
186 if self.sync_with_disk:
187 return comment.loadComments(self, load_full=load_full)
189 return comment.Comment(self, uuid=comment.INVALID_UUID)
192 @cached_property(generator=_get_comment_root)
193 @local_property("comment_root")
194 @doc_property(doc="The trunk of the comment tree")
195 def comment_root(): return {}
198 if hasattr(self.bugdir, "rcs"):
199 return self.bugdir.rcs
202 @cached_property(generator=_get_rcs)
203 @local_property("rcs")
204 @doc_property(doc="A revision control system instance.")
207 def __init__(self, bugdir=None, uuid=None, from_disk=False,
208 load_comments=False, summary=None):
211 self._settings_loaded = False
213 if from_disk == True:
214 self.sync_with_disk = True
216 self.sync_with_disk = False
218 self.uuid = uuid_gen()
219 self.time = int(time.time()) # only save to second precision
221 self.creator = self.rcs.get_user_id()
222 self.summary = summary
225 return "Bug(uuid=%r)" % self.uuid
227 def string(self, shortlist=False, show_comments=False):
228 if self.bugdir == None:
229 shortname = self.uuid
231 shortname = self.bugdir.bug_shortname(self)
232 if shortlist == False:
233 if self.time_string == "":
234 timestring = self.time_string
236 htime = utility.handy_time(self.time)
237 timestring = "%s (%s)" % (htime, self.time_string)
238 info = [("ID", self.uuid),
239 ("Short name", shortname),
240 ("Severity", self.severity),
241 ("Status", self.status),
242 ("Assigned", self.assigned or ""),
243 ("Target", self.target or ""),
244 ("Creator", self.creator or ""),
245 ("Created", timestring)]
246 longest_key_len = max([len(k) for k,v in info])
247 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
248 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
250 statuschar = self.status[0]
251 severitychar = self.severity[0]
252 chars = "%c%c" % (statuschar, severitychar)
253 bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
255 if show_comments == True:
256 # take advantage of the string_thread(auto_name_map=True)
257 # SIDE-EFFECT of sorting by comment time.
258 comout = self.comment_root.string_thread(flatten=False,
260 bug_shortname=shortname)
261 output = bugout + '\n' + comout.rstrip('\n')
267 return self.string(shortlist=True)
269 def __cmp__(self, other):
270 return cmp_full(self, other)
272 def get_path(self, name=None):
273 my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
276 assert name in ["values", "comments"]
277 return os.path.join(my_dir, name)
279 def load_settings(self):
280 self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
281 for property in self.settings_properties:
282 if property not in self.settings:
283 self.settings[property] = EMPTY
284 elif self.settings[property] == None:
285 self.settings[property] = EMPTY
286 self._settings_loaded = True
288 def load_comments(self, load_full=True):
289 if load_full == True:
290 # Force a complete load of the whole comment tree
291 self.comment_root = self._get_comment_root(load_full=True)
293 # Setup for fresh lazy-loading. Clear _comment_root, so
294 # _get_comment_root returns a fresh version. Turn of
295 # syncing temporarily so we don't write our blank comment
297 self.sync_with_disk = False
298 self.comment_root = None
299 self.sync_with_disk = True
301 def save_settings(self):
302 assert self.summary != None, "Can't save blank bug"
304 for k,v in self.settings.items():
305 if (v != None and v != EMPTY):
307 for k in self.required_saved_properties:
308 map[k] = getattr(self, k)
310 self.rcs.mkdir(self.get_path())
311 path = self.get_path("values")
312 mapfile.map_save(self.rcs, path, map)
317 if len(self.comment_root) > 0:
318 self.rcs.mkdir(self.get_path("comments"))
319 comment.saveComments(self)
322 self.comment_root.remove()
323 path = self.get_path()
324 self.rcs.recursive_remove(path)
327 for comment in self.comment_root.traverse():
330 def new_comment(self, body=None):
331 comm = self.comment_root.new_reply(body=body)
334 def comment_from_shortname(self, shortname, *args, **kwargs):
335 return self.comment_root.comment_from_shortname(shortname,
338 def comment_from_uuid(self, uuid):
339 return self.comment_root.comment_from_uuid(uuid)
341 def comment_shortnames(self, shortname=None):
343 SIDE-EFFECT : Comment.comment_shortnames will sort the comment
346 for id, comment in self.comment_root.comment_shortnames(shortname):
350 # The general rule for bug sorting is that "more important" bugs are
351 # less than "less important" bugs. This way sorting a list of bugs
352 # will put the most important bugs first in the list. When relative
353 # importance is unclear, the sorting follows some arbitrary convention
354 # (i.e. dictionary order).
356 def cmp_severity(bug_1, bug_2):
358 Compare the severity levels of two bugs, with more severe bugs
362 >>> bugA.severity = bugB.severity = "wishlist"
363 >>> cmp_severity(bugA, bugB) == 0
365 >>> bugB.severity = "minor"
366 >>> cmp_severity(bugA, bugB) > 0
368 >>> bugA.severity = "critical"
369 >>> cmp_severity(bugA, bugB) < 0
372 if not hasattr(bug_2, "severity") :
374 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
376 def cmp_status(bug_1, bug_2):
378 Compare the status levels of two bugs, with more 'open' bugs
382 >>> bugA.status = bugB.status = "open"
383 >>> cmp_status(bugA, bugB) == 0
385 >>> bugB.status = "closed"
386 >>> cmp_status(bugA, bugB) < 0
388 >>> bugA.status = "fixed"
389 >>> cmp_status(bugA, bugB) > 0
392 if not hasattr(bug_2, "status") :
394 val_2 = status_index[bug_2.status]
395 return cmp(status_index[bug_1.status], status_index[bug_2.status])
397 def cmp_attr(bug_1, bug_2, attr, invert=False):
399 Compare a general attribute between two bugs using the conventional
400 comparison rule for that attribute type. If invert == True, sort
401 *against* that convention.
405 >>> bugA.severity = "critical"
406 >>> bugB.severity = "wishlist"
407 >>> cmp_attr(bugA, bugB, attr) < 0
409 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
411 >>> bugB.severity = "critical"
412 >>> cmp_attr(bugA, bugB, attr) == 0
415 if not hasattr(bug_2, attr) :
418 return -cmp(getattr(bug_1, attr), getattr(bug_2, attr))
420 return cmp(getattr(bug_1, attr), getattr(bug_2, attr))
422 # alphabetical rankings (a < z)
423 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
424 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
425 # chronological rankings (newer < older)
426 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
428 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
429 cmp_time,cmp_creator)):
430 for comparison in cmp_list :
431 val = comparison(bug_1, bug_2)
436 class InvalidValue(ValueError):
437 def __init__(self, name, value):
438 msg = "Cannot assign value %s to %s" % (value, name)
439 Exception.__init__(self, msg)
443 suite = doctest.DocTestSuite()