1 # Copyright (C) 2008-2010 Gianluca Montecchi <gian@grys.it>
2 # Thomas Habets <thomas@habets.pp.se>
3 # W. Trevor King <wking@drexel.edu>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19 """Define the :class:`Bug` class for representing bugs.
29 try: # import core module, Python >= 2.5
30 from xml.etree import ElementTree
31 except ImportError: # look for non-core module
32 from elementtree import ElementTree
33 import xml.sax.saxutils
37 from libbe.storage.util.properties import Property, doc_property, \
38 local_property, defaulting_property, checked_property, cached_property, \
39 primed_property, change_hook_property, settings_property
40 import libbe.storage.util.settings_object as settings_object
41 import libbe.storage.util.mapfile as mapfile
42 import libbe.comment as comment
43 import libbe.util.utility as utility
45 if libbe.TESTING == True:
49 class DiskAccessRequired (Exception):
50 def __init__(self, goal):
51 msg = "Cannot %s without accessing the disk" % goal
52 Exception.__init__(self, msg)
54 ### Define and describe valid bug categories
55 # Use a tuple of (category, description) tuples since we don't have
56 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
58 # in order of increasing severity. (name, description) pairs
60 ("target", "The issue is a target or milestone, not a bug."),
61 ("wishlist","A feature that could improve usefulness, but not a bug."),
62 ("minor","The standard bug level."),
63 ("serious","A bug that requires workarounds."),
64 ("critical","A bug that prevents some features from working at all."),
65 ("fatal","A bug that makes the package unusable."))
67 # in order of increasing resolution
68 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
70 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
71 ("open","A working bug that has not been assigned to a developer."),
72 ("assigned","A working bug that has been assigned to a developer."),
73 ("test","The code has been adjusted, but the fix is still being tested."))
74 inactive_status_def = (
75 ("closed", "The bug is no longer relevant."),
76 ("fixed", "The bug should no longer occur."),
77 ("wontfix","It's not a bug, it's a feature."))
80 ### Convert the description tuples to more useful formats
83 severity_description = {}
85 def load_severities(severity_def):
86 global severity_values
87 global severity_description
89 if severity_def == None:
91 severity_values = tuple([val for val,description in severity_def])
92 severity_description = dict(severity_def)
94 for i,severity in enumerate(severity_values):
95 severity_index[severity] = i
96 load_severities(severity_def)
98 active_status_values = []
99 inactive_status_values = []
101 status_description = {}
103 def load_status(active_status_def, inactive_status_def):
104 global active_status_values
105 global inactive_status_values
107 global status_description
109 if active_status_def == None:
110 active_status_def = globals()["active_status_def"]
111 if inactive_status_def == None:
112 inactive_status_def = globals()["inactive_status_def"]
113 active_status_values = tuple([val for val,description in active_status_def])
114 inactive_status_values = tuple([val for val,description in inactive_status_def])
115 status_values = active_status_values + inactive_status_values
116 status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
118 for i,status in enumerate(status_values):
119 status_index[status] = i
120 load_status(active_status_def, inactive_status_def)
123 class Bug (settings_object.SavedSettingsObject):
124 """A bug (or issue) is a place to store attributes and attach
125 :class:`~libbe.comment.Comment`\s. In mailing-list terms, a bug is
126 analogous to a thread. Bugs are normally stored in
127 :class:`~libbe.bugdir.BugDir`\s.
135 There are two formats for time, int and string. Setting either
136 one will adjust the other appropriately. The string form is the
137 one stored in the bug's settings file on disk.
139 >>> print type(b.time)
141 >>> print type(b.time_string)
144 >>> print b.time_string
145 Thu, 01 Jan 1970 00:00:00 +0000
146 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
149 >>> print b.settings["time"]
150 Thu, 01 Jan 1970 00:01:00 +0000
152 settings_properties = []
153 required_saved_properties = []
154 _prop_save_settings = settings_object.prop_save_settings
155 _prop_load_settings = settings_object.prop_load_settings
156 def _versioned_property(settings_properties=settings_properties,
157 required_saved_properties=required_saved_properties,
159 if "settings_properties" not in kwargs:
160 kwargs["settings_properties"] = settings_properties
161 if "required_saved_properties" not in kwargs:
162 kwargs["required_saved_properties"]=required_saved_properties
163 return settings_object.versioned_property(**kwargs)
165 @_versioned_property(name="severity",
166 doc="A measure of the bug's importance",
168 check_fn=lambda s: s in severity_values,
170 def severity(): return {}
172 @_versioned_property(name="status",
173 doc="The bug's current status",
175 check_fn=lambda s: s in status_values,
177 def status(): return {}
181 return self.status in active_status_values
183 @_versioned_property(name="creator",
184 doc="The user who entered the bug into the system")
185 def creator(): return {}
187 @_versioned_property(name="reporter",
188 doc="The user who reported the bug")
189 def reporter(): return {}
191 @_versioned_property(name="assigned",
192 doc="The developer in charge of the bug")
193 def assigned(): return {}
195 @_versioned_property(name="time",
196 doc="An RFC 2822 timestamp for bug creation")
197 def time_string(): return {}
200 if self.time_string == None:
202 return utility.str_to_time(self.time_string)
203 def _set_time(self, value):
204 self.time_string = utility.time_to_str(value)
205 time = property(fget=_get_time,
207 doc="An integer version of .time_string")
209 def _extra_strings_check_fn(value):
210 return utility.iterable_full_of_strings(value, \
211 alternative=settings_object.EMPTY)
212 def _extra_strings_change_hook(self, old, new):
213 self.extra_strings.sort() # to make merging easier
214 self._prop_save_settings(old, new)
215 @_versioned_property(name="extra_strings",
216 doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
218 check_fn=_extra_strings_check_fn,
219 change_hook=_extra_strings_change_hook,
221 def extra_strings(): return {}
223 @_versioned_property(name="summary",
224 doc="A one-line bug description")
225 def summary(): return {}
227 def _get_comment_root(self, load_full=False):
228 if self.storage != None and self.storage.is_readable():
229 return comment.load_comments(self, load_full=load_full)
231 return comment.Comment(self, uuid=comment.INVALID_UUID)
234 @cached_property(generator=_get_comment_root)
235 @local_property("comment_root")
236 @doc_property(doc="The trunk of the comment tree. We use a dummy root comment by default, because there can be several comment threads rooted on the same parent bug. To simplify comment interaction, we condense these threads into a single thread with a Comment dummy root.")
237 def comment_root(): return {}
239 def __init__(self, bugdir=None, uuid=None, from_storage=False,
240 load_comments=False, summary=None):
241 settings_object.SavedSettingsObject.__init__(self)
245 self.id = libbe.util.id.ID(self, 'bug')
246 if from_storage == False:
248 self.uuid = libbe.util.id.uuid_gen()
249 self.time = int(time.time()) # only save to second precision
250 self.summary = summary
251 dummy = self.comment_root
252 if self.bugdir != None:
253 self.storage = self.bugdir.storage
254 if from_storage == False:
255 if self.storage != None and self.storage.is_writeable():
259 return "Bug(uuid=%r)" % self.uuid
262 return self.string(shortlist=True)
264 def __cmp__(self, other):
265 return cmp_full(self, other)
267 # serializing methods
269 def _setting_attr_string(self, setting):
270 value = getattr(self, setting)
273 if type(value) not in types.StringTypes:
277 def string(self, shortlist=False, show_comments=False):
278 if shortlist == False:
279 if self.time == None:
282 htime = utility.handy_time(self.time)
283 timestring = "%s (%s)" % (htime, self.time_string)
284 info = [("ID", self.uuid),
285 ("Short name", self.id.user()),
286 ("Severity", self.severity),
287 ("Status", self.status),
288 ("Assigned", self._setting_attr_string("assigned")),
289 ("Reporter", self._setting_attr_string("reporter")),
290 ("Creator", self._setting_attr_string("creator")),
291 ("Created", timestring)]
292 longest_key_len = max([len(k) for k,v in info])
293 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
294 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
296 statuschar = self.status[0]
297 severitychar = self.severity[0]
298 chars = "%c%c" % (statuschar, severitychar)
299 bugout = "%s:%s: %s" % (self.id.user(),chars,self.summary.rstrip('\n'))
301 if show_comments == True:
302 self.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
303 comout = self.comment_root.string_thread(flatten=False)
304 output = bugout + '\n' + comout.rstrip('\n')
309 def xml(self, indent=0, show_comments=False):
310 if self.time == None:
313 timestring = utility.time_to_str(self.time)
315 info = [('uuid', self.uuid),
316 ('short-name', self.id.user()),
317 ('severity', self.severity),
318 ('status', self.status),
319 ('assigned', self.assigned),
320 ('reporter', self.reporter),
321 ('creator', self.creator),
322 ('created', timestring),
323 ('summary', self.summary)]
327 lines.append(' <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
328 for estr in self.extra_strings:
329 lines.append(' <extra-string>%s</extra-string>' % estr)
330 if show_comments == True:
331 comout = self.comment_root.xml_thread(indent=indent+2)
334 lines.append('</bug>')
337 return istring + sep.join(lines).rstrip('\n')
339 def from_xml(self, xml_string, verbose=True):
341 Note: If a bug uuid is given, set .alt_id to it's value.
342 >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
343 >>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000"
344 >>> bugA.creator = u'Fran\xe7ois'
345 >>> bugA.extra_strings += ['TAG: very helpful']
346 >>> commA = bugA.comment_root.new_reply(body='comment A')
347 >>> commB = bugA.comment_root.new_reply(body='comment B')
348 >>> commC = commA.new_reply(body='comment C')
349 >>> xml = bugA.xml(show_comments=True)
351 >>> bugB.from_xml(xml, verbose=True)
352 >>> bugB.xml(show_comments=True) == xml
354 >>> bugB.uuid = bugB.alt_id
355 >>> for comm in bugB.comments():
356 ... comm.uuid = comm.alt_id
357 ... comm.alt_id = None
358 >>> bugB.xml(show_comments=True) == xml
360 >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE
361 ['severity', 'status', 'creator', 'created', 'summary']
362 >>> len(list(bugB.comments()))
365 if type(xml_string) == types.UnicodeType:
366 xml_string = xml_string.strip().encode('unicode_escape')
367 if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
370 bug = ElementTree.XML(xml_string)
372 raise utility.InvalidXML( \
373 'bug', bug, 'root element must be <comment>')
374 tags=['uuid','short-name','severity','status','assigned',
375 'reporter', 'creator','created','summary','extra-string']
376 self.explicit_attrs = []
380 for child in bug.getchildren():
381 if child.tag == 'short-name':
383 elif child.tag == 'comment':
384 comm = comment.Comment(bug=self)
386 comments.append(comm)
388 elif child.tag in tags:
389 if child.text == None or len(child.text) == 0:
390 text = settings_object.EMPTY
392 text = xml.sax.saxutils.unescape(child.text)
393 text = text.decode('unicode_escape').strip()
394 if child.tag == 'uuid':
396 continue # don't set the bug's uuid tag.
397 elif child.tag == 'extra-string':
399 continue # don't set the bug's extra_string yet.
400 attr_name = child.tag.replace('-','_')
401 self.explicit_attrs.append(attr_name)
402 setattr(self, attr_name, text)
403 elif verbose == True:
404 print >> sys.stderr, 'Ignoring unknown tag %s in %s' \
405 % (child.tag, comment.tag)
406 if uuid != self.uuid:
407 if not hasattr(self, 'alt_id') or self.alt_id == None:
409 self.extra_strings = estrs
410 self.add_comments(comments, ignore_missing_references=True)
412 def add_comment(self, comment, *args, **kwargs):
414 Add a comment too the current bug, under the parent specified
415 by comment.in_reply_to.
416 Note: If a bug uuid is given, set .alt_id to it's value.
418 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.add_comment()')
419 >>> bugA.creator = 'Jack'
420 >>> commA = bugA.comment_root.new_reply(body='comment A')
421 >>> commA.uuid = 'commA'
422 >>> commB = comment.Comment(body='comment B')
423 >>> commB.uuid = 'commB'
424 >>> bugA.add_comment(commB)
425 >>> commC = comment.Comment(body='comment C')
426 >>> commC.uuid = 'commC'
427 >>> commC.in_reply_to = commA.uuid
428 >>> bugA.add_comment(commC)
429 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
432 <short-name>/012</short-name>
433 <severity>minor</severity>
434 <status>open</status>
435 <creator>Jack</creator>
436 <created>...</created>
437 <summary>Need to test Bug.add_comment()</summary>
440 <short-name>/012/commA</short-name>
443 <content-type>text/plain</content-type>
444 <body>comment A</body>
448 <short-name>/012/commC</short-name>
449 <in-reply-to>commA</in-reply-to>
452 <content-type>text/plain</content-type>
453 <body>comment C</body>
457 <short-name>/012/commB</short-name>
460 <content-type>text/plain</content-type>
461 <body>comment B</body>
465 self.add_comments([comment], **kwargs)
467 def add_comments(self, comments, default_parent=None,
468 ignore_missing_references=False):
470 Convert a raw list of comments to single root comment. If a
471 comment does not specify a parent with .in_reply_to, the
472 parent defaults to .comment_root, but you can specify another
473 default parent via default_parent.
476 if default_parent == None:
477 default_parent = self.comment_root
478 for c in list(self.comments()) + comments:
479 assert c.uuid != None
480 assert c.uuid not in uuid_map
483 uuid_map[c.alt_id] = c
484 uuid_map[None] = self.comment_root
485 uuid_map[comment.INVALID_UUID] = self.comment_root
486 if default_parent != self.comment_root:
487 assert default_parent.uuid in uuid_map, default_parent.uuid
489 if c.in_reply_to == None \
490 and default_parent.uuid != comment.INVALID_UUID:
491 c.in_reply_to = default_parent.uuid
492 elif c.in_reply_to == comment.INVALID_UUID:
495 parent = uuid_map[c.in_reply_to]
497 if ignore_missing_references == True:
498 print >> sys.stderr, \
499 'Ignoring missing reference to %s' % c.in_reply_to
500 parent = default_parent
501 if parent.uuid != comment.INVALID_UUID:
502 c.in_reply_to = parent.uuid
504 raise comment.MissingReference(c)
508 def merge(self, other, accept_changes=True,
509 accept_extra_strings=True, accept_comments=True,
510 change_exception=False):
512 Merge info from other into this bug. Overrides any attributes
513 in self that are listed in other.explicit_attrs.
515 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()')
516 >>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
517 >>> bugA.creator = 'Frank'
518 >>> bugA.extra_strings += ['TAG: very helpful']
519 >>> bugA.extra_strings += ['TAG: favorite']
520 >>> commA = bugA.comment_root.new_reply(body='comment A')
521 >>> commA.uuid = 'uuid-commA'
522 >>> bugB = Bug(uuid='3210', summary='More tests for Bug.merge()')
523 >>> bugB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
524 >>> bugB.creator = 'John'
525 >>> bugB.explicit_attrs = ['creator', 'summary']
526 >>> bugB.extra_strings += ['TAG: very helpful']
527 >>> bugB.extra_strings += ['TAG: useful']
528 >>> commB = bugB.comment_root.new_reply(body='comment B')
529 >>> commB.uuid = 'uuid-commB'
530 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
531 ... accept_comments=False, change_exception=False)
532 >>> print bugA.creator
534 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
535 ... accept_comments=False, change_exception=True)
536 Traceback (most recent call last):
538 ValueError: Merge would change creator "Frank"->"John" for bug 0123
539 >>> print bugA.creator
541 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=False,
542 ... accept_comments=False, change_exception=True)
543 Traceback (most recent call last):
545 ValueError: Merge would add extra string "TAG: useful" for bug 0123
546 >>> print bugA.creator
548 >>> print bugA.extra_strings
549 ['TAG: favorite', 'TAG: very helpful']
550 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
551 ... accept_comments=False, change_exception=True)
552 Traceback (most recent call last):
554 ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123
555 >>> print bugA.extra_strings
556 ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
557 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
558 ... accept_comments=True, change_exception=True)
559 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
562 <short-name>/012</short-name>
563 <severity>minor</severity>
564 <status>open</status>
565 <creator>John</creator>
566 <created>...</created>
567 <summary>More tests for Bug.merge()</summary>
568 <extra-string>TAG: favorite</extra-string>
569 <extra-string>TAG: useful</extra-string>
570 <extra-string>TAG: very helpful</extra-string>
572 <uuid>uuid-commA</uuid>
573 <short-name>/012/uuid-commA</short-name>
576 <content-type>text/plain</content-type>
577 <body>comment A</body>
580 <uuid>uuid-commB</uuid>
581 <short-name>/012/uuid-commB</short-name>
584 <content-type>text/plain</content-type>
585 <body>comment B</body>
589 for attr in other.explicit_attrs:
590 old = getattr(self, attr)
591 new = getattr(other, attr)
593 if accept_changes == True:
594 setattr(self, attr, new)
595 elif change_exception == True:
597 'Merge would change %s "%s"->"%s" for bug %s' \
598 % (attr, old, new, self.uuid)
599 for estr in other.extra_strings:
600 if not estr in self.extra_strings:
601 if accept_extra_strings == True:
602 self.extra_strings.append(estr)
603 elif change_exception == True:
605 'Merge would add extra string "%s" for bug %s' \
607 for o_comm in other.comments():
609 s_comm = self.comment_root.comment_from_uuid(o_comm.uuid)
612 s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id)
616 if accept_comments == True:
617 o_comm_copy = copy.copy(o_comm)
618 o_comm_copy.bug = self
619 o_comm_copy.id = libbe.util.id.ID(o_comm_copy, 'comment')
620 self.comment_root.add_reply(o_comm_copy)
621 elif change_exception == True:
623 'Merge would add comment %s (alt: %s) to bug %s' \
624 % (o_comm.uuid, o_comm.alt_id, self.uuid)
626 s_comm.merge(o_comm, accept_changes=accept_changes,
627 accept_extra_strings=accept_extra_strings,
628 change_exception=change_exception)
630 # methods for saving/loading/acessing settings and properties.
632 def load_settings(self, settings_mapfile=None):
633 if settings_mapfile == None:
635 self.storage.get(self.id.storage('values'), default='\n')
637 settings = mapfile.parse(settings_mapfile)
638 except mapfile.InvalidMapfileContents, e:
639 raise Exception('Invalid settings file for bug %s\n'
640 '(BE version missmatch?)' % self.id.user())
641 self._setup_saved_settings(settings)
643 def save_settings(self):
644 mf = mapfile.generate(self._get_saved_settings())
645 self.storage.set(self.id.storage('values'), mf)
649 Save any loaded contents to storage. Because of lazy loading
650 of comments, this is actually not too inefficient.
652 However, if self.storage.is_writeable() == True, then any
653 changes are automatically written to storage as soon as they
654 happen, so calling this method will just waste time (unless
655 something else has been messing with your stored files).
657 assert self.storage != None, "Can't save without storage"
658 if self.bugdir != None:
659 parent = self.bugdir.id.storage()
662 self.storage.add(self.id.storage(), parent=parent, directory=True)
663 self.storage.add(self.id.storage('values'), parent=self.id.storage(),
666 if len(self.comment_root) > 0:
667 comment.save_comments(self)
669 def load_comments(self, load_full=True):
670 if load_full == True:
671 # Force a complete load of the whole comment tree
672 self.comment_root = self._get_comment_root(load_full=True)
674 # Setup for fresh lazy-loading. Clear _comment_root, so
675 # next _get_comment_root returns a fresh version. Turn of
676 # writing temporarily so we don't write our blank comment
678 w = self.storage.writeable
679 self.storage.writeable = False
680 self.comment_root = None
681 self.storage.writeable = w
684 self.storage.recursive_remove(self.id.storage())
686 # methods for managing comments
689 for comment in self.comments():
693 for comment in self.comment_root.traverse():
696 def new_comment(self, body=None):
697 comm = self.comment_root.new_reply(body=body)
700 def comment_from_uuid(self, uuid, *args, **kwargs):
701 return self.comment_root.comment_from_uuid(uuid, *args, **kwargs)
703 # methods for id generation
705 def sibling_uuids(self):
706 if self.bugdir != None:
707 return self.bugdir.uuids()
711 # The general rule for bug sorting is that "more important" bugs are
712 # less than "less important" bugs. This way sorting a list of bugs
713 # will put the most important bugs first in the list. When relative
714 # importance is unclear, the sorting follows some arbitrary convention
715 # (i.e. dictionary order).
717 def cmp_severity(bug_1, bug_2):
719 Compare the severity levels of two bugs, with more severe bugs
724 >>> bugA.severity = bugB.severity = "wishlist"
725 >>> cmp_severity(bugA, bugB) == 0
727 >>> bugB.severity = "minor"
728 >>> cmp_severity(bugA, bugB) > 0
730 >>> bugA.severity = "critical"
731 >>> cmp_severity(bugA, bugB) < 0
734 if not hasattr(bug_2, "severity") :
736 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
738 def cmp_status(bug_1, bug_2):
740 Compare the status levels of two bugs, with more "open" bugs
745 >>> bugA.status = bugB.status = "open"
746 >>> cmp_status(bugA, bugB) == 0
748 >>> bugB.status = "closed"
749 >>> cmp_status(bugA, bugB) < 0
751 >>> bugA.status = "fixed"
752 >>> cmp_status(bugA, bugB) > 0
755 if not hasattr(bug_2, "status") :
757 val_2 = status_index[bug_2.status]
758 return cmp(status_index[bug_1.status], status_index[bug_2.status])
760 def cmp_attr(bug_1, bug_2, attr, invert=False):
762 Compare a general attribute between two bugs using the
763 conventional comparison rule for that attribute type. If
764 ``invert==True``, sort *against* that convention.
769 >>> bugA.severity = "critical"
770 >>> bugB.severity = "wishlist"
771 >>> cmp_attr(bugA, bugB, attr) < 0
773 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
775 >>> bugB.severity = "critical"
776 >>> cmp_attr(bugA, bugB, attr) == 0
779 if not hasattr(bug_2, attr) :
781 val_1 = getattr(bug_1, attr)
782 val_2 = getattr(bug_2, attr)
783 if val_1 == None: val_1 = None
784 if val_2 == None: val_2 = None
787 return -cmp(val_1, val_2)
789 return cmp(val_1, val_2)
791 # alphabetical rankings (a < z)
792 cmp_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid")
793 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
794 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
795 cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter")
796 cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary")
797 cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings")
798 # chronological rankings (newer < older)
799 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
801 def cmp_comments(bug_1, bug_2):
803 Compare two bugs' comments lists. Doesn't load any new comments,
804 so you should call each bug's .load_comments() first if you want a
807 comms_1 = sorted(bug_1.comments(), key = lambda comm : comm.uuid)
808 comms_2 = sorted(bug_2.comments(), key = lambda comm : comm.uuid)
809 result = cmp(len(comms_1), len(comms_2))
812 for c_1,c_2 in zip(comms_1, comms_2):
813 result = cmp(c_1, c_2)
818 DEFAULT_CMP_FULL_CMP_LIST = \
819 (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator,
820 cmp_reporter, cmp_comments, cmp_summary, cmp_uuid, cmp_extra_strings)
822 class BugCompoundComparator (object):
823 def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
824 self.cmp_list = cmp_list
825 def __call__(self, bug_1, bug_2):
826 for comparison in self.cmp_list :
827 val = comparison(bug_1, bug_2)
832 cmp_full = BugCompoundComparator()
835 # define some bonus cmp_* functions
836 def cmp_last_modified(bug_1, bug_2):
838 Like cmp_time(), but use most recent comment instead of bug
839 creation for the timestamp.
841 def last_modified(bug):
843 for comment in bug.comment_root.traverse():
844 if comment.time > time:
847 val_1 = last_modified(bug_1)
848 val_2 = last_modified(bug_2)
849 return -cmp(val_1, val_2)
852 if libbe.TESTING == True:
853 suite = doctest.DocTestSuite()