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
38 severity_level_def = (
39 ("wishlist","A feature that could improve usefullness, 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
61 severity_values = tuple([val for val,description in severity_level_def])
62 severity_description = dict(severity_level_def)
64 for i in range(len(severity_values)):
65 severity_index[severity_values[i]] = i
67 active_status_values = tuple(val for val,description in active_status_def)
68 inactive_status_values = tuple(val for val,description in inactive_status_def)
69 status_values = active_status_values + inactive_status_values
70 status_description = dict(active_status_def+inactive_status_def)
72 for i in range(len(status_values)):
73 status_index[status_values[i]] = i
76 class Bug(settings_object.SavedSettingsObject):
84 There are two formats for time, int and string. Setting either
85 one will adjust the other appropriately. The string form is the
86 one stored in the bug's settings file on disk.
87 >>> print type(b.time)
89 >>> print type(b.time_string)
92 >>> print b.time_string
93 Thu, 01 Jan 1970 00:00:00 +0000
94 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
97 >>> print b.settings["time"]
98 Thu, 01 Jan 1970 00:01:00 +0000
100 settings_properties = []
101 required_saved_properties = []
102 _prop_save_settings = settings_object.prop_save_settings
103 _prop_load_settings = settings_object.prop_load_settings
104 def _versioned_property(settings_properties=settings_properties,
105 required_saved_properties=required_saved_properties,
107 if "settings_properties" not in kwargs:
108 kwargs["settings_properties"] = settings_properties
109 if "required_saved_properties" not in kwargs:
110 kwargs["required_saved_properties"]=required_saved_properties
111 return settings_object.versioned_property(**kwargs)
113 @_versioned_property(name="severity",
114 doc="A measure of the bug's importance",
116 allowed=severity_values,
118 def severity(): return {}
120 @_versioned_property(name="status",
121 doc="The bug's current status",
123 allowed=status_values,
125 def status(): return {}
129 return self.status in active_status_values
131 @_versioned_property(name="target",
132 doc="The deadline for fixing this bug")
133 def target(): return {}
135 @_versioned_property(name="creator",
136 doc="The user who entered the bug into the system")
137 def creator(): return {}
139 @_versioned_property(name="reporter",
140 doc="The user who reported the bug")
141 def reporter(): return {}
143 @_versioned_property(name="assigned",
144 doc="The developer in charge of the bug")
145 def assigned(): return {}
147 @_versioned_property(name="time",
148 doc="An RFC 2822 timestamp for bug creation")
149 def time_string(): return {}
152 if self.time_string == None or self.time_string == settings_object.EMPTY:
154 return utility.str_to_time(self.time_string)
155 def _set_time(self, value):
156 self.time_string = utility.time_to_str(value)
157 time = property(fget=_get_time,
159 doc="An integer version of .time_string")
161 @_versioned_property(name="summary",
162 doc="A one-line bug description")
163 def summary(): return {}
165 def _get_comment_root(self, load_full=False):
166 if self.sync_with_disk:
167 return comment.loadComments(self, load_full=load_full)
169 return comment.Comment(self, uuid=comment.INVALID_UUID)
172 @cached_property(generator=_get_comment_root)
173 @local_property("comment_root")
174 @doc_property(doc="The trunk of the comment tree")
175 def comment_root(): return {}
178 if hasattr(self.bugdir, "rcs"):
179 return self.bugdir.rcs
182 @cached_property(generator=_get_rcs)
183 @local_property("rcs")
184 @doc_property(doc="A revision control system instance.")
187 def __init__(self, bugdir=None, uuid=None, from_disk=False,
188 load_comments=False, summary=None):
189 settings_object.SavedSettingsObject.__init__(self)
192 if from_disk == True:
193 self.sync_with_disk = True
195 self.sync_with_disk = False
197 self.uuid = uuid_gen()
198 self.time = int(time.time()) # only save to second precision
200 self.creator = self.rcs.get_user_id()
201 self.summary = summary
204 return "Bug(uuid=%r)" % self.uuid
206 def _setting_attr_string(self, setting):
207 value = getattr(self, setting)
208 if value == settings_object.EMPTY:
213 def string(self, shortlist=False, show_comments=False):
214 if self.bugdir == None:
215 shortname = self.uuid
217 shortname = self.bugdir.bug_shortname(self)
218 if shortlist == False:
219 if self.time_string == "":
220 timestring = self.time_string
222 htime = utility.handy_time(self.time)
223 timestring = "%s (%s)" % (htime, self.time_string)
224 info = [("ID", self.uuid),
225 ("Short name", shortname),
226 ("Severity", self.severity),
227 ("Status", self.status),
228 ("Assigned", self._setting_attr_string("assigned")),
229 ("Target", self._setting_attr_string("target")),
230 ("Reporter", self._setting_attr_string("reporter")),
231 ("Creator", self._setting_attr_string("creator")),
232 ("Created", timestring)]
233 longest_key_len = max([len(k) for k,v in info])
234 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
235 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
237 statuschar = self.status[0]
238 severitychar = self.severity[0]
239 chars = "%c%c" % (statuschar, severitychar)
240 bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
242 if show_comments == True:
243 # take advantage of the string_thread(auto_name_map=True)
244 # SIDE-EFFECT of sorting by comment time.
245 comout = self.comment_root.string_thread(flatten=False,
247 bug_shortname=shortname)
248 output = bugout + '\n' + comout.rstrip('\n')
254 return self.string(shortlist=True)
256 def __cmp__(self, other):
257 return cmp_full(self, other)
259 def get_path(self, name=None):
260 my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
263 assert name in ["values", "comments"]
264 return os.path.join(my_dir, name)
266 def load_settings(self):
267 self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
268 self._setup_saved_settings()
270 def load_comments(self, load_full=True):
271 if load_full == True:
272 # Force a complete load of the whole comment tree
273 self.comment_root = self._get_comment_root(load_full=True)
275 # Setup for fresh lazy-loading. Clear _comment_root, so
276 # _get_comment_root returns a fresh version. Turn of
277 # syncing temporarily so we don't write our blank comment
279 self.sync_with_disk = False
280 self.comment_root = None
281 self.sync_with_disk = True
283 def save_settings(self):
284 assert self.summary != None, "Can't save blank bug"
286 self.rcs.mkdir(self.get_path())
287 path = self.get_path("values")
288 mapfile.map_save(self.rcs, path, self._get_saved_settings())
293 if len(self.comment_root) > 0:
294 self.rcs.mkdir(self.get_path("comments"))
295 comment.saveComments(self)
298 self.comment_root.remove()
299 path = self.get_path()
300 self.rcs.recursive_remove(path)
303 for comment in self.comment_root.traverse():
306 def new_comment(self, body=None):
307 comm = self.comment_root.new_reply(body=body)
310 def comment_from_shortname(self, shortname, *args, **kwargs):
311 return self.comment_root.comment_from_shortname(shortname,
314 def comment_from_uuid(self, uuid):
315 return self.comment_root.comment_from_uuid(uuid)
317 def comment_shortnames(self, shortname=None):
319 SIDE-EFFECT : Comment.comment_shortnames will sort the comment
322 for id, comment in self.comment_root.comment_shortnames(shortname):
326 # The general rule for bug sorting is that "more important" bugs are
327 # less than "less important" bugs. This way sorting a list of bugs
328 # will put the most important bugs first in the list. When relative
329 # importance is unclear, the sorting follows some arbitrary convention
330 # (i.e. dictionary order).
332 def cmp_severity(bug_1, bug_2):
334 Compare the severity levels of two bugs, with more severe bugs
338 >>> bugA.severity = bugB.severity = "wishlist"
339 >>> cmp_severity(bugA, bugB) == 0
341 >>> bugB.severity = "minor"
342 >>> cmp_severity(bugA, bugB) > 0
344 >>> bugA.severity = "critical"
345 >>> cmp_severity(bugA, bugB) < 0
348 if not hasattr(bug_2, "severity") :
350 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
352 def cmp_status(bug_1, bug_2):
354 Compare the status levels of two bugs, with more 'open' bugs
358 >>> bugA.status = bugB.status = "open"
359 >>> cmp_status(bugA, bugB) == 0
361 >>> bugB.status = "closed"
362 >>> cmp_status(bugA, bugB) < 0
364 >>> bugA.status = "fixed"
365 >>> cmp_status(bugA, bugB) > 0
368 if not hasattr(bug_2, "status") :
370 val_2 = status_index[bug_2.status]
371 return cmp(status_index[bug_1.status], status_index[bug_2.status])
373 def cmp_attr(bug_1, bug_2, attr, invert=False):
375 Compare a general attribute between two bugs using the conventional
376 comparison rule for that attribute type. If invert == True, sort
377 *against* that convention.
381 >>> bugA.severity = "critical"
382 >>> bugB.severity = "wishlist"
383 >>> cmp_attr(bugA, bugB, attr) < 0
385 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
387 >>> bugB.severity = "critical"
388 >>> cmp_attr(bugA, bugB, attr) == 0
391 if not hasattr(bug_2, attr) :
393 val_1 = getattr(bug_1, attr)
394 val_2 = getattr(bug_2, attr)
395 if val_1 == settings_object.EMPTY: val_1 = None
396 if val_2 == settings_object.EMPTY: val_2 = None
399 return -cmp(val_1, val_2)
401 return cmp(val_1, val_2)
403 # alphabetical rankings (a < z)
404 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
405 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
406 # chronological rankings (newer < older)
407 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
409 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
410 cmp_time,cmp_creator)):
411 for comparison in cmp_list :
412 val = comparison(bug_1, bug_2)
417 class InvalidValue(ValueError):
418 def __init__(self, name, value):
419 msg = "Cannot assign value %s to %s" % (value, name)
420 Exception.__init__(self, msg)
424 suite = doctest.DocTestSuite()