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."),
59 ### Convert the description tuples to more useful formats
62 severity_description = {}
64 def load_severities(severity_def):
65 global severity_values
66 global severity_description
68 severity_values = tuple([val for val,description in severity_def])
69 severity_description = dict(severity_def)
71 for i,severity in enumerate(severity_values):
72 severity_index[severity] = i
73 load_severities(severity_def)
75 active_status_values = tuple(val for val,description in active_status_def)
76 inactive_status_values = tuple(val for val,description in inactive_status_def)
77 status_values = active_status_values + inactive_status_values
78 status_description = dict(active_status_def+inactive_status_def)
80 for i in range(len(status_values)):
81 status_index[status_values[i]] = i
84 class Bug(settings_object.SavedSettingsObject):
92 There are two formats for time, int and string. Setting either
93 one will adjust the other appropriately. The string form is the
94 one stored in the bug's settings file on disk.
95 >>> print type(b.time)
97 >>> print type(b.time_string)
100 >>> print b.time_string
101 Thu, 01 Jan 1970 00:00:00 +0000
102 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
105 >>> print b.settings["time"]
106 Thu, 01 Jan 1970 00:01:00 +0000
108 settings_properties = []
109 required_saved_properties = []
110 _prop_save_settings = settings_object.prop_save_settings
111 _prop_load_settings = settings_object.prop_load_settings
112 def _versioned_property(settings_properties=settings_properties,
113 required_saved_properties=required_saved_properties,
115 if "settings_properties" not in kwargs:
116 kwargs["settings_properties"] = settings_properties
117 if "required_saved_properties" not in kwargs:
118 kwargs["required_saved_properties"]=required_saved_properties
119 return settings_object.versioned_property(**kwargs)
121 @_versioned_property(name="severity",
122 doc="A measure of the bug's importance",
124 check_fn=lambda s: s in severity_values,
126 def severity(): return {}
128 @_versioned_property(name="status",
129 doc="The bug's current status",
131 allowed=status_values,
133 def status(): return {}
137 return self.status in active_status_values
139 @_versioned_property(name="target",
140 doc="The deadline for fixing this bug")
141 def target(): return {}
143 @_versioned_property(name="creator",
144 doc="The user who entered the bug into the system")
145 def creator(): return {}
147 @_versioned_property(name="reporter",
148 doc="The user who reported the bug")
149 def reporter(): return {}
151 @_versioned_property(name="assigned",
152 doc="The developer in charge of the bug")
153 def assigned(): return {}
155 @_versioned_property(name="time",
156 doc="An RFC 2822 timestamp for bug creation")
157 def time_string(): return {}
160 if self.time_string == None or self.time_string == settings_object.EMPTY:
162 return utility.str_to_time(self.time_string)
163 def _set_time(self, value):
164 self.time_string = utility.time_to_str(value)
165 time = property(fget=_get_time,
167 doc="An integer version of .time_string")
169 @_versioned_property(name="summary",
170 doc="A one-line bug description")
171 def summary(): return {}
173 def _get_comment_root(self, load_full=False):
174 if self.sync_with_disk:
175 return comment.loadComments(self, load_full=load_full)
177 return comment.Comment(self, uuid=comment.INVALID_UUID)
180 @cached_property(generator=_get_comment_root)
181 @local_property("comment_root")
182 @doc_property(doc="The trunk of the comment tree")
183 def comment_root(): return {}
186 if hasattr(self.bugdir, "rcs"):
187 return self.bugdir.rcs
190 @cached_property(generator=_get_rcs)
191 @local_property("rcs")
192 @doc_property(doc="A revision control system instance.")
195 def __init__(self, bugdir=None, uuid=None, from_disk=False,
196 load_comments=False, summary=None):
197 settings_object.SavedSettingsObject.__init__(self)
200 if from_disk == True:
201 self.sync_with_disk = True
203 self.sync_with_disk = False
205 self.uuid = uuid_gen()
206 self.time = int(time.time()) # only save to second precision
208 self.creator = self.rcs.get_user_id()
209 self.summary = summary
212 return "Bug(uuid=%r)" % self.uuid
214 def _setting_attr_string(self, setting):
215 value = getattr(self, setting)
216 if value == settings_object.EMPTY:
221 def string(self, shortlist=False, show_comments=False):
222 if self.bugdir == None:
223 shortname = self.uuid
225 shortname = self.bugdir.bug_shortname(self)
226 if shortlist == False:
227 if self.time_string == "":
228 timestring = self.time_string
230 htime = utility.handy_time(self.time)
231 timestring = "%s (%s)" % (htime, self.time_string)
232 info = [("ID", self.uuid),
233 ("Short name", shortname),
234 ("Severity", self.severity),
235 ("Status", self.status),
236 ("Assigned", self._setting_attr_string("assigned")),
237 ("Target", self._setting_attr_string("target")),
238 ("Reporter", self._setting_attr_string("reporter")),
239 ("Creator", self._setting_attr_string("creator")),
240 ("Created", timestring)]
241 longest_key_len = max([len(k) for k,v in info])
242 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
243 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
245 statuschar = self.status[0]
246 severitychar = self.severity[0]
247 chars = "%c%c" % (statuschar, severitychar)
248 bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
250 if show_comments == True:
251 # take advantage of the string_thread(auto_name_map=True)
252 # SIDE-EFFECT of sorting by comment time.
253 comout = self.comment_root.string_thread(flatten=False,
255 bug_shortname=shortname)
256 output = bugout + '\n' + comout.rstrip('\n')
262 return self.string(shortlist=True)
264 def __cmp__(self, other):
265 return cmp_full(self, other)
267 def get_path(self, name=None):
268 my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
271 assert name in ["values", "comments"]
272 return os.path.join(my_dir, name)
274 def load_settings(self):
275 self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
276 self._setup_saved_settings()
278 def load_comments(self, load_full=True):
279 if load_full == True:
280 # Force a complete load of the whole comment tree
281 self.comment_root = self._get_comment_root(load_full=True)
283 # Setup for fresh lazy-loading. Clear _comment_root, so
284 # _get_comment_root returns a fresh version. Turn of
285 # syncing temporarily so we don't write our blank comment
287 self.sync_with_disk = False
288 self.comment_root = None
289 self.sync_with_disk = True
291 def save_settings(self):
292 assert self.summary != None, "Can't save blank bug"
294 self.rcs.mkdir(self.get_path())
295 path = self.get_path("values")
296 mapfile.map_save(self.rcs, path, self._get_saved_settings())
301 if len(self.comment_root) > 0:
302 self.rcs.mkdir(self.get_path("comments"))
303 comment.saveComments(self)
306 self.comment_root.remove()
307 path = self.get_path()
308 self.rcs.recursive_remove(path)
311 for comment in self.comment_root.traverse():
314 def new_comment(self, body=None):
315 comm = self.comment_root.new_reply(body=body)
318 def comment_from_shortname(self, shortname, *args, **kwargs):
319 return self.comment_root.comment_from_shortname(shortname,
322 def comment_from_uuid(self, uuid):
323 return self.comment_root.comment_from_uuid(uuid)
325 def comment_shortnames(self, shortname=None):
327 SIDE-EFFECT : Comment.comment_shortnames will sort the comment
330 for id, comment in self.comment_root.comment_shortnames(shortname):
334 # The general rule for bug sorting is that "more important" bugs are
335 # less than "less important" bugs. This way sorting a list of bugs
336 # will put the most important bugs first in the list. When relative
337 # importance is unclear, the sorting follows some arbitrary convention
338 # (i.e. dictionary order).
340 def cmp_severity(bug_1, bug_2):
342 Compare the severity levels of two bugs, with more severe bugs
346 >>> bugA.severity = bugB.severity = "wishlist"
347 >>> cmp_severity(bugA, bugB) == 0
349 >>> bugB.severity = "minor"
350 >>> cmp_severity(bugA, bugB) > 0
352 >>> bugA.severity = "critical"
353 >>> cmp_severity(bugA, bugB) < 0
356 if not hasattr(bug_2, "severity") :
358 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
360 def cmp_status(bug_1, bug_2):
362 Compare the status levels of two bugs, with more 'open' bugs
366 >>> bugA.status = bugB.status = "open"
367 >>> cmp_status(bugA, bugB) == 0
369 >>> bugB.status = "closed"
370 >>> cmp_status(bugA, bugB) < 0
372 >>> bugA.status = "fixed"
373 >>> cmp_status(bugA, bugB) > 0
376 if not hasattr(bug_2, "status") :
378 val_2 = status_index[bug_2.status]
379 return cmp(status_index[bug_1.status], status_index[bug_2.status])
381 def cmp_attr(bug_1, bug_2, attr, invert=False):
383 Compare a general attribute between two bugs using the conventional
384 comparison rule for that attribute type. If invert == True, sort
385 *against* that convention.
389 >>> bugA.severity = "critical"
390 >>> bugB.severity = "wishlist"
391 >>> cmp_attr(bugA, bugB, attr) < 0
393 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
395 >>> bugB.severity = "critical"
396 >>> cmp_attr(bugA, bugB, attr) == 0
399 if not hasattr(bug_2, attr) :
401 val_1 = getattr(bug_1, attr)
402 val_2 = getattr(bug_2, attr)
403 if val_1 == settings_object.EMPTY: val_1 = None
404 if val_2 == settings_object.EMPTY: val_2 = None
407 return -cmp(val_1, val_2)
409 return cmp(val_1, val_2)
411 # alphabetical rankings (a < z)
412 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
413 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
414 # chronological rankings (newer < older)
415 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
417 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
418 cmp_time,cmp_creator)):
419 for comparison in cmp_list :
420 val = comparison(bug_1, bug_2)
425 class InvalidValue(ValueError):
426 def __init__(self, name, value):
427 msg = "Cannot assign value %s to %s" % (value, name)
428 Exception.__init__(self, msg)
432 suite = doctest.DocTestSuite()