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 integere 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):
186 if self.sync_with_disk:
187 return comment.loadComments(self)
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
215 #self.load(load_comments=load_comments)
217 self.sync_with_disk = False
219 self.uuid = uuid_gen()
220 self.time = int(time.time()) # only save to second precision
222 self.creator = self.rcs.get_user_id()
223 self.summary = summary
226 return "Bug(uuid=%r)" % self.uuid
228 def string(self, shortlist=False, show_comments=False):
229 if self.bugdir == None:
230 shortname = self.uuid
232 shortname = self.bugdir.bug_shortname(self)
233 if shortlist == False:
234 if self.time_string == "":
235 timestring = self.time_string
237 htime = utility.handy_time(self.time)
238 timestring = "%s (%s)" % (htime, self.time_string)
239 info = [("ID", self.uuid),
240 ("Short name", shortname),
241 ("Severity", self.severity),
242 ("Status", self.status),
243 ("Assigned", self.assigned or ""),
244 ("Target", self.target or ""),
245 ("Creator", self.creator or ""),
246 ("Created", timestring)]
247 longest_key_len = max([len(k) for k,v in info])
248 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
249 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
251 statuschar = self.status[0]
252 severitychar = self.severity[0]
253 chars = "%c%c" % (statuschar, severitychar)
254 bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
256 if show_comments == True:
257 # take advantage of the string_thread(auto_name_map=True)
258 # SIDE-EFFECT of sorting by comment time.
259 comout = self.comment_root.string_thread(flatten=False,
261 bug_shortname=shortname)
262 output = bugout + '\n' + comout.rstrip('\n')
268 return self.string(shortlist=True)
270 def __cmp__(self, other):
271 return cmp_full(self, other)
273 def get_path(self, name=None):
274 my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
277 assert name in ["values", "comments"]
278 return os.path.join(my_dir, name)
280 def load_settings(self):
281 self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
282 for property in self.settings_properties:
283 if property not in self.settings:
284 self.settings[property] = EMPTY
285 elif self.settings[property] == None:
286 self.settings[property] = EMPTY
287 self._settings_loaded = True
289 def load_comments(self):
290 # Clear _comment_root, so _get_comment_root returns a fresh
291 # version. Turn of syncing temporarily so we don't write our
292 # blank comment tree to disk.
293 self.sync_with_disk = False
294 self._comment_root = None
295 self.sync_with_disk = True
297 def save_settings(self):
298 assert self.summary != None, "Can't save blank bug"
300 for k,v in self.settings.items():
301 if (v != None and v != EMPTY):
303 for k in self.required_saved_properties:
304 map[k] = getattr(self, k)
306 self.rcs.mkdir(self.get_path())
307 path = self.get_path("values")
308 mapfile.map_save(self.rcs, path, map)
313 if len(self.comment_root) > 0:
314 self.rcs.mkdir(self.get_path("comments"))
315 comment.saveComments(self)
318 self.comment_root.remove()
319 path = self.get_path()
320 self.rcs.recursive_remove(path)
323 for comment in self.comment_root.traverse():
326 def new_comment(self, body=None):
327 comm = self.comment_root.new_reply(body=body)
330 def comment_from_shortname(self, shortname, *args, **kwargs):
331 return self.comment_root.comment_from_shortname(shortname,
334 def comment_from_uuid(self, uuid):
335 return self.comment_root.comment_from_uuid(uuid)
337 def comment_shortnames(self, shortname=None):
339 SIDE-EFFECT : Comment.comment_shortnames will sort the comment
342 for id, comment in self.comment_root.comment_shortnames(shortname):
346 # The general rule for bug sorting is that "more important" bugs are
347 # less than "less important" bugs. This way sorting a list of bugs
348 # will put the most important bugs first in the list. When relative
349 # importance is unclear, the sorting follows some arbitrary convention
350 # (i.e. dictionary order).
352 def cmp_severity(bug_1, bug_2):
354 Compare the severity levels of two bugs, with more severe bugs
358 >>> bugA.severity = bugB.severity = "wishlist"
359 >>> cmp_severity(bugA, bugB) == 0
361 >>> bugB.severity = "minor"
362 >>> cmp_severity(bugA, bugB) > 0
364 >>> bugA.severity = "critical"
365 >>> cmp_severity(bugA, bugB) < 0
368 if not hasattr(bug_2, "severity") :
370 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
372 def cmp_status(bug_1, bug_2):
374 Compare the status levels of two bugs, with more 'open' bugs
378 >>> bugA.status = bugB.status = "open"
379 >>> cmp_status(bugA, bugB) == 0
381 >>> bugB.status = "closed"
382 >>> cmp_status(bugA, bugB) < 0
384 >>> bugA.status = "fixed"
385 >>> cmp_status(bugA, bugB) > 0
388 if not hasattr(bug_2, "status") :
390 val_2 = status_index[bug_2.status]
391 return cmp(status_index[bug_1.status], status_index[bug_2.status])
393 def cmp_attr(bug_1, bug_2, attr, invert=False):
395 Compare a general attribute between two bugs using the conventional
396 comparison rule for that attribute type. If invert == True, sort
397 *against* that convention.
401 >>> bugA.severity = "critical"
402 >>> bugB.severity = "wishlist"
403 >>> cmp_attr(bugA, bugB, attr) < 0
405 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
407 >>> bugB.severity = "critical"
408 >>> cmp_attr(bugA, bugB, attr) == 0
411 if not hasattr(bug_2, attr) :
414 return -cmp(getattr(bug_1, attr), getattr(bug_2, attr))
416 return cmp(getattr(bug_1, attr), getattr(bug_2, attr))
418 # alphabetical rankings (a < z)
419 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
420 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
421 # chronological rankings (newer < older)
422 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
424 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
425 cmp_time,cmp_creator)):
426 for comparison in cmp_list :
427 val = comparison(bug_1, bug_2)
432 class InvalidValue(ValueError):
433 def __init__(self, name, value):
434 msg = "Cannot assign value %s to %s" % (value, name)
435 Exception.__init__(self, msg)
439 suite = doctest.DocTestSuite()