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 ("Creator", self._setting_attr_string("creator")),
231 ("Created", timestring)]
232 longest_key_len = max([len(k) for k,v in info])
233 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
234 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
236 statuschar = self.status[0]
237 severitychar = self.severity[0]
238 chars = "%c%c" % (statuschar, severitychar)
239 bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n'))
241 if show_comments == True:
242 # take advantage of the string_thread(auto_name_map=True)
243 # SIDE-EFFECT of sorting by comment time.
244 comout = self.comment_root.string_thread(flatten=False,
246 bug_shortname=shortname)
247 output = bugout + '\n' + comout.rstrip('\n')
253 return self.string(shortlist=True)
255 def __cmp__(self, other):
256 return cmp_full(self, other)
258 def get_path(self, name=None):
259 my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
262 assert name in ["values", "comments"]
263 return os.path.join(my_dir, name)
265 def load_settings(self):
266 self.settings = mapfile.map_load(self.rcs, self.get_path("values"))
267 self._setup_saved_settings()
269 def load_comments(self, load_full=True):
270 if load_full == True:
271 # Force a complete load of the whole comment tree
272 self.comment_root = self._get_comment_root(load_full=True)
274 # Setup for fresh lazy-loading. Clear _comment_root, so
275 # _get_comment_root returns a fresh version. Turn of
276 # syncing temporarily so we don't write our blank comment
278 self.sync_with_disk = False
279 self.comment_root = None
280 self.sync_with_disk = True
282 def save_settings(self):
283 assert self.summary != None, "Can't save blank bug"
285 self.rcs.mkdir(self.get_path())
286 path = self.get_path("values")
287 mapfile.map_save(self.rcs, path, self._get_saved_settings())
292 if len(self.comment_root) > 0:
293 self.rcs.mkdir(self.get_path("comments"))
294 comment.saveComments(self)
297 self.comment_root.remove()
298 path = self.get_path()
299 self.rcs.recursive_remove(path)
302 for comment in self.comment_root.traverse():
305 def new_comment(self, body=None):
306 comm = self.comment_root.new_reply(body=body)
309 def comment_from_shortname(self, shortname, *args, **kwargs):
310 return self.comment_root.comment_from_shortname(shortname,
313 def comment_from_uuid(self, uuid):
314 return self.comment_root.comment_from_uuid(uuid)
316 def comment_shortnames(self, shortname=None):
318 SIDE-EFFECT : Comment.comment_shortnames will sort the comment
321 for id, comment in self.comment_root.comment_shortnames(shortname):
325 # The general rule for bug sorting is that "more important" bugs are
326 # less than "less important" bugs. This way sorting a list of bugs
327 # will put the most important bugs first in the list. When relative
328 # importance is unclear, the sorting follows some arbitrary convention
329 # (i.e. dictionary order).
331 def cmp_severity(bug_1, bug_2):
333 Compare the severity levels of two bugs, with more severe bugs
337 >>> bugA.severity = bugB.severity = "wishlist"
338 >>> cmp_severity(bugA, bugB) == 0
340 >>> bugB.severity = "minor"
341 >>> cmp_severity(bugA, bugB) > 0
343 >>> bugA.severity = "critical"
344 >>> cmp_severity(bugA, bugB) < 0
347 if not hasattr(bug_2, "severity") :
349 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
351 def cmp_status(bug_1, bug_2):
353 Compare the status levels of two bugs, with more 'open' bugs
357 >>> bugA.status = bugB.status = "open"
358 >>> cmp_status(bugA, bugB) == 0
360 >>> bugB.status = "closed"
361 >>> cmp_status(bugA, bugB) < 0
363 >>> bugA.status = "fixed"
364 >>> cmp_status(bugA, bugB) > 0
367 if not hasattr(bug_2, "status") :
369 val_2 = status_index[bug_2.status]
370 return cmp(status_index[bug_1.status], status_index[bug_2.status])
372 def cmp_attr(bug_1, bug_2, attr, invert=False):
374 Compare a general attribute between two bugs using the conventional
375 comparison rule for that attribute type. If invert == True, sort
376 *against* that convention.
380 >>> bugA.severity = "critical"
381 >>> bugB.severity = "wishlist"
382 >>> cmp_attr(bugA, bugB, attr) < 0
384 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
386 >>> bugB.severity = "critical"
387 >>> cmp_attr(bugA, bugB, attr) == 0
390 if not hasattr(bug_2, attr) :
392 val_1 = getattr(bug_1, attr)
393 val_2 = getattr(bug_2, attr)
394 if val_1 == settings_object.EMPTY: val_1 = None
395 if val_2 == settings_object.EMPTY: val_2 = None
398 return -cmp(val_1, val_2)
400 return cmp(val_1, val_2)
402 # alphabetical rankings (a < z)
403 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
404 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
405 # chronological rankings (newer < older)
406 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
408 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
409 cmp_time,cmp_creator)):
410 for comparison in cmp_list :
411 val = comparison(bug_1, bug_2)
416 class InvalidValue(ValueError):
417 def __init__(self, name, value):
418 msg = "Cannot assign value %s to %s" % (value, name)
419 Exception.__init__(self, msg)
423 suite = doctest.DocTestSuite()