1 # Copyright (C) 2008-2009 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.
20 Define the 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):
131 There are two formats for time, int and string. Setting either
132 one will adjust the other appropriately. The string form is the
133 one stored in the bug's settings file on disk.
134 >>> print type(b.time)
136 >>> print type(b.time_string)
139 >>> print b.time_string
140 Thu, 01 Jan 1970 00:00:00 +0000
141 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
144 >>> print b.settings["time"]
145 Thu, 01 Jan 1970 00:01:00 +0000
147 settings_properties = []
148 required_saved_properties = []
149 _prop_save_settings = settings_object.prop_save_settings
150 _prop_load_settings = settings_object.prop_load_settings
151 def _versioned_property(settings_properties=settings_properties,
152 required_saved_properties=required_saved_properties,
154 if "settings_properties" not in kwargs:
155 kwargs["settings_properties"] = settings_properties
156 if "required_saved_properties" not in kwargs:
157 kwargs["required_saved_properties"]=required_saved_properties
158 return settings_object.versioned_property(**kwargs)
160 @_versioned_property(name="severity",
161 doc="A measure of the bug's importance",
163 check_fn=lambda s: s in severity_values,
165 def severity(): return {}
167 @_versioned_property(name="status",
168 doc="The bug's current status",
170 check_fn=lambda s: s in status_values,
172 def status(): return {}
176 return self.status in active_status_values
178 def _get_user_id(self):
179 if self.bugdir != None:
180 return self.bugdir._get_user_id()
183 @_versioned_property(name="creator",
184 doc="The user who entered the bug into the system",
185 generator=_get_user_id)
186 def creator(): return {}
188 @_versioned_property(name="reporter",
189 doc="The user who reported the bug")
190 def reporter(): return {}
192 @_versioned_property(name="assigned",
193 doc="The developer in charge of the bug")
194 def assigned(): return {}
196 @_versioned_property(name="time",
197 doc="An RFC 2822 timestamp for bug creation")
198 def time_string(): return {}
201 if self.time_string == None:
203 return utility.str_to_time(self.time_string)
204 def _set_time(self, value):
205 self.time_string = utility.time_to_str(value)
206 time = property(fget=_get_time,
208 doc="An integer version of .time_string")
210 def _extra_strings_check_fn(value):
211 return utility.iterable_full_of_strings(value, \
212 alternative=settings_object.EMPTY)
213 def _extra_strings_change_hook(self, old, new):
214 self.extra_strings.sort() # to make merging easier
215 self._prop_save_settings(old, new)
216 @_versioned_property(name="extra_strings",
217 doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
219 check_fn=_extra_strings_check_fn,
220 change_hook=_extra_strings_change_hook,
222 def extra_strings(): return {}
224 @_versioned_property(name="summary",
225 doc="A one-line bug description")
226 def summary(): return {}
228 def _get_comment_root(self, load_full=False):
229 if self.storage != None and self.storage.is_readable():
230 return comment.load_comments(self, load_full=load_full)
232 return comment.Comment(self, uuid=comment.INVALID_UUID)
235 @cached_property(generator=_get_comment_root)
236 @local_property("comment_root")
237 @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.")
238 def comment_root(): return {}
240 def __init__(self, bugdir=None, uuid=None, from_storage=False,
241 load_comments=False, summary=None):
242 settings_object.SavedSettingsObject.__init__(self)
246 self.id = libbe.util.id.ID(self, 'bug')
247 if from_storage == False:
249 self.uuid = libbe.util.id.uuid_gen()
251 self._setup_saved_settings()
252 self.time = int(time.time()) # only save to second precision
253 self.summary = summary
254 dummy = self.comment_root
255 if self.bugdir != None:
256 self.storage = self.bugdir.storage
257 if from_storage == False:
258 if self.storage != None and self.storage.is_writeable():
262 return "Bug(uuid=%r)" % self.uuid
265 return self.string(shortlist=True)
267 def __cmp__(self, other):
268 return cmp_full(self, other)
270 # serializing methods
272 def _setting_attr_string(self, setting):
273 value = getattr(self, setting)
276 if type(value) not in types.StringTypes:
280 def string(self, shortlist=False, show_comments=False):
281 if shortlist == False:
282 if self.time == None:
285 htime = utility.handy_time(self.time)
286 timestring = "%s (%s)" % (htime, self.time_string)
287 info = [("ID", self.uuid),
288 ("Short name", self.id.user()),
289 ("Severity", self.severity),
290 ("Status", self.status),
291 ("Assigned", self._setting_attr_string("assigned")),
292 ("Reporter", self._setting_attr_string("reporter")),
293 ("Creator", self._setting_attr_string("creator")),
294 ("Created", timestring)]
295 longest_key_len = max([len(k) for k,v in info])
296 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
297 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
299 statuschar = self.status[0]
300 severitychar = self.severity[0]
301 chars = "%c%c" % (statuschar, severitychar)
302 bugout = "%s:%s: %s" % (self.id.user(),chars,self.summary.rstrip('\n'))
304 if show_comments == True:
305 # take advantage of the string_thread(auto_name_map=True)
306 # SIDE-EFFECT of sorting by comment time.
307 comout = self.comment_root.string_thread(flatten=False)
308 output = bugout + '\n' + comout.rstrip('\n')
313 def xml(self, indent=0, show_comments=False):
314 if self.time == None:
317 timestring = utility.time_to_str(self.time)
319 info = [('uuid', self.uuid),
320 ('short-name', self.id.user()),
321 ('severity', self.severity),
322 ('status', self.status),
323 ('assigned', self.assigned),
324 ('reporter', self.reporter),
325 ('creator', self.creator),
326 ('created', timestring),
327 ('summary', self.summary)]
331 lines.append(' <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
332 for estr in self.extra_strings:
333 lines.append(' <extra-string>%s</extra-string>' % estr)
334 if show_comments == True:
335 comout = self.comment_root.xml_thread(indent=indent+2)
338 lines.append('</bug>')
341 return istring + sep.join(lines).rstrip('\n')
343 def from_xml(self, xml_string, verbose=True):
345 Note: If a bug uuid is given, set .alt_id to it's value.
346 >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
347 >>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000"
348 >>> bugA.creator = u'Fran\xe7ois'
349 >>> bugA.extra_strings += ['TAG: very helpful']
350 >>> commA = bugA.comment_root.new_reply(body='comment A')
351 >>> commB = bugA.comment_root.new_reply(body='comment B')
352 >>> commC = commA.new_reply(body='comment C')
353 >>> xml = bugA.xml(show_comments=True)
355 >>> bugB.from_xml(xml, verbose=True)
356 >>> bugB.xml(show_comments=True) == xml
358 >>> bugB.uuid = bugB.alt_id
359 >>> for comm in bugB.comments():
360 ... comm.uuid = comm.alt_id
361 ... comm.alt_id = None
362 >>> bugB.xml(show_comments=True) == xml
364 >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE
365 ['severity', 'status', 'creator', 'created', 'summary']
366 >>> len(list(bugB.comments()))
369 if type(xml_string) == types.UnicodeType:
370 xml_string = xml_string.strip().encode('unicode_escape')
371 if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
374 bug = ElementTree.XML(xml_string)
376 raise utility.InvalidXML( \
377 'bug', bug, 'root element must be <comment>')
378 tags=['uuid','short-name','severity','status','assigned',
379 'reporter', 'creator','created','summary','extra-string']
380 self.explicit_attrs = []
384 for child in bug.getchildren():
385 if child.tag == 'short-name':
387 elif child.tag == 'comment':
388 comm = comment.Comment(bug=self)
390 comments.append(comm)
392 elif child.tag in tags:
393 if child.text == None or len(child.text) == 0:
394 text = settings_object.EMPTY
396 text = xml.sax.saxutils.unescape(child.text)
397 text = text.decode('unicode_escape').strip()
398 if child.tag == 'uuid':
400 continue # don't set the bug's uuid tag.
401 elif child.tag == 'extra-string':
403 continue # don't set the bug's extra_string yet.
404 attr_name = child.tag.replace('-','_')
405 self.explicit_attrs.append(attr_name)
406 setattr(self, attr_name, text)
407 elif verbose == True:
408 print >> sys.stderr, "Ignoring unknown tag %s in %s" \
409 % (child.tag, comment.tag)
410 if uuid != self.uuid:
411 if not hasattr(self, 'alt_id') or self.alt_id == None:
413 self.extra_strings = estrs
414 self.add_comments(comments)
416 def add_comment(self, comment, *args, **kwargs):
418 Add a comment too the current bug, under the parent specified
419 by comment.in_reply_to.
420 Note: If a bug uuid is given, set .alt_id to it's value.
421 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.add_comment()')
422 >>> bugA.creator = 'Jack'
423 >>> commA = bugA.comment_root.new_reply(body='comment A')
424 >>> commA.uuid = 'commA'
425 >>> commB = comment.Comment(body='comment B')
426 >>> commB.uuid = 'commB'
427 >>> bugA.add_comment(commB)
428 >>> commC = comment.Comment(body='comment C')
429 >>> commC.uuid = 'commC'
430 >>> commC.in_reply_to = commA.uuid
431 >>> bugA.add_comment(commC)
432 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
435 <short-name>/012</short-name>
436 <severity>minor</severity>
437 <status>open</status>
438 <creator>Jack</creator>
439 <created>...</created>
440 <summary>Need to test Bug.add_comment()</summary>
443 <short-name>/012/commA</short-name>
446 <content-type>text/plain</content-type>
447 <body>comment A</body>
451 <short-name>/012/commC</short-name>
452 <in-reply-to>commA</in-reply-to>
455 <content-type>text/plain</content-type>
456 <body>comment C</body>
460 <short-name>/012/commB</short-name>
463 <content-type>text/plain</content-type>
464 <body>comment B</body>
468 self.add_comments([comment], **kwargs)
470 def add_comments(self, comments, default_parent=None,
471 ignore_missing_references=False):
473 Convert a raw list of comments to single root comment. If a
474 comment does not specify a parent with .in_reply_to, the
475 parent defaults to .comment_root, but you can specify another
476 default parent via default_parent.
479 if default_parent == None:
480 default_parent = self.comment_root
481 for c in list(self.comments()) + comments:
482 assert c.uuid != None
483 assert c.uuid not in uuid_map
486 uuid_map[c.alt_id] = c
487 uuid_map[None] = self.comment_root
488 uuid_map[comment.INVALID_UUID] = self.comment_root
489 if default_parent != self.comment_root:
490 assert default_parent.uuid in uuid_map, default_parent.uuid
492 if c.in_reply_to == None \
493 and default_parent.uuid != comment.INVALID_UUID:
494 c.in_reply_to = default_parent.uuid
495 elif c.in_reply_to == comment.INVALID_UUID:
498 parent = uuid_map[c.in_reply_to]
500 if ignore_missing_references == True:
501 print >> sys.stderr, \
502 "Ignoring missing reference to %s" % c.in_reply_to
503 parent = default_parent
504 if parent.uuid != comment.INVALID_UUID:
505 c.in_reply_to = parent.uuid
507 raise comment.MissingReference(c)
511 def merge(self, other, accept_changes=True,
512 accept_extra_strings=True, accept_comments=True,
513 change_exception=False):
515 Merge info from other into this bug. Overrides any attributes
516 in self that are listed in other.explicit_attrs.
517 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()')
518 >>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
519 >>> bugA.creator = 'Frank'
520 >>> bugA.extra_strings += ['TAG: very helpful']
521 >>> bugA.extra_strings += ['TAG: favorite']
522 >>> commA = bugA.comment_root.new_reply(body='comment A')
523 >>> commA.uuid = 'uuid-commA'
524 >>> bugB = Bug(uuid='3210', summary='More tests for Bug.merge()')
525 >>> bugB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
526 >>> bugB.creator = 'John'
527 >>> bugB.explicit_attrs = ['creator', 'summary']
528 >>> bugB.extra_strings += ['TAG: very helpful']
529 >>> bugB.extra_strings += ['TAG: useful']
530 >>> commB = bugB.comment_root.new_reply(body='comment B')
531 >>> commB.uuid = 'uuid-commB'
532 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
533 ... accept_comments=False, change_exception=False)
534 >>> print bugA.creator
536 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
537 ... accept_comments=False, change_exception=True)
538 Traceback (most recent call last):
540 ValueError: Merge would change creator "Frank"->"John" for bug 0123
541 >>> print bugA.creator
543 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=False,
544 ... accept_comments=False, change_exception=True)
545 Traceback (most recent call last):
547 ValueError: Merge would add extra string "TAG: useful" for bug 0123
548 >>> print bugA.creator
550 >>> print bugA.extra_strings
551 ['TAG: favorite', 'TAG: very helpful']
552 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
553 ... accept_comments=False, change_exception=True)
554 Traceback (most recent call last):
556 ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123
557 >>> print bugA.extra_strings
558 ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
559 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
560 ... accept_comments=True, change_exception=True)
561 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
564 <short-name>/012</short-name>
565 <severity>minor</severity>
566 <status>open</status>
567 <creator>John</creator>
568 <created>...</created>
569 <summary>More tests for Bug.merge()</summary>
570 <extra-string>TAG: favorite</extra-string>
571 <extra-string>TAG: useful</extra-string>
572 <extra-string>TAG: very helpful</extra-string>
574 <uuid>uuid-commA</uuid>
575 <short-name>/012/uuid-commA</short-name>
578 <content-type>text/plain</content-type>
579 <body>comment A</body>
582 <uuid>uuid-commB</uuid>
583 <short-name>/012/uuid-commB</short-name>
586 <content-type>text/plain</content-type>
587 <body>comment B</body>
591 for attr in other.explicit_attrs:
592 old = getattr(self, attr)
593 new = getattr(other, attr)
595 if accept_changes == True:
596 setattr(self, attr, new)
597 elif change_exception == True:
599 'Merge would change %s "%s"->"%s" for bug %s' \
600 % (attr, old, new, self.uuid)
601 for estr in other.extra_strings:
602 if not estr in self.extra_strings:
603 if accept_extra_strings == True:
604 self.extra_strings.append(estr)
605 elif change_exception == True:
607 'Merge would add extra string "%s" for bug %s' \
609 for o_comm in other.comments():
611 s_comm = self.comment_root.comment_from_uuid(o_comm.uuid)
614 s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id)
618 if accept_comments == True:
619 o_comm_copy = copy.copy(o_comm)
620 o_comm_copy.bug = self
621 o_comm_copy.id = libbe.util.id.ID(o_comm_copy, 'comment')
622 self.comment_root.add_reply(o_comm_copy)
623 elif change_exception == True:
625 'Merge would add comment %s (alt: %s) to bug %s' \
626 % (o_comm.uuid, o_comm.alt_id, self.uuid)
628 s_comm.merge(o_comm, accept_changes=accept_changes,
629 accept_extra_strings=accept_extra_strings,
630 change_exception=change_exception)
632 # methods for saving/loading/acessing settings and properties.
634 def load_settings(self, settings_mapfile=None):
635 if settings_mapfile == None:
637 self.storage.get(self.id.storage("values"), default="\n")
638 self.settings = mapfile.parse(settings_mapfile)
639 self._setup_saved_settings()
641 def save_settings(self):
642 mf = mapfile.generate(self._get_saved_settings())
643 self.storage.set(self.id.storage("values"), mf)
647 Save any loaded contents to storage. Because of lazy loading
648 of comments, this is actually not too inefficient.
650 However, if self.storage.is_writeable() == True, then any
651 changes are automatically written to storage as soon as they
652 happen, so calling this method will just waste time (unless
653 something else has been messing with your stored files).
655 assert self.storage != None, "Can't save without storage"
656 if self.bugdir != None:
657 parent = self.bugdir.id.storage()
660 self.storage.add(self.id.storage(), parent=parent, directory=True)
661 self.storage.add(self.id.storage('values'), parent=self.id.storage(),
664 if len(self.comment_root) > 0:
665 comment.save_comments(self)
667 def load_comments(self, load_full=True):
668 if load_full == True:
669 # Force a complete load of the whole comment tree
670 self.comment_root = self._get_comment_root(load_full=True)
672 # Setup for fresh lazy-loading. Clear _comment_root, so
673 # next _get_comment_root returns a fresh version. Turn of
674 # writing temporarily so we don't write our blank comment
676 w = self.storage.writeable
677 self.storage.writeable = False
678 self.comment_root = None
679 self.storage.writeable = w
682 self.storage.recursive_remove(self.id.storage())
684 # methods for managing comments
687 for comment in self.comments():
691 for comment in self.comment_root.traverse():
694 def new_comment(self, body=None):
695 comm = self.comment_root.new_reply(body=body)
698 def comment_from_uuid(self, uuid, *args, **kwargs):
699 return self.comment_root.comment_from_uuid(uuid, *args, **kwargs)
701 # methods for id generation
703 def sibling_uuids(self):
704 if self.bugdir != None:
705 return self.bugdir.uuids()
709 # The general rule for bug sorting is that "more important" bugs are
710 # less than "less important" bugs. This way sorting a list of bugs
711 # will put the most important bugs first in the list. When relative
712 # importance is unclear, the sorting follows some arbitrary convention
713 # (i.e. dictionary order).
715 def cmp_severity(bug_1, bug_2):
717 Compare the severity levels of two bugs, with more severe bugs
721 >>> bugA.severity = bugB.severity = "wishlist"
722 >>> cmp_severity(bugA, bugB) == 0
724 >>> bugB.severity = "minor"
725 >>> cmp_severity(bugA, bugB) > 0
727 >>> bugA.severity = "critical"
728 >>> cmp_severity(bugA, bugB) < 0
731 if not hasattr(bug_2, "severity") :
733 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
735 def cmp_status(bug_1, bug_2):
737 Compare the status levels of two bugs, with more 'open' bugs
741 >>> bugA.status = bugB.status = "open"
742 >>> cmp_status(bugA, bugB) == 0
744 >>> bugB.status = "closed"
745 >>> cmp_status(bugA, bugB) < 0
747 >>> bugA.status = "fixed"
748 >>> cmp_status(bugA, bugB) > 0
751 if not hasattr(bug_2, "status") :
753 val_2 = status_index[bug_2.status]
754 return cmp(status_index[bug_1.status], status_index[bug_2.status])
756 def cmp_attr(bug_1, bug_2, attr, invert=False):
758 Compare a general attribute between two bugs using the conventional
759 comparison rule for that attribute type. If invert == True, sort
760 *against* that convention.
764 >>> bugA.severity = "critical"
765 >>> bugB.severity = "wishlist"
766 >>> cmp_attr(bugA, bugB, attr) < 0
768 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
770 >>> bugB.severity = "critical"
771 >>> cmp_attr(bugA, bugB, attr) == 0
774 if not hasattr(bug_2, attr) :
776 val_1 = getattr(bug_1, attr)
777 val_2 = getattr(bug_2, attr)
778 if val_1 == None: val_1 = None
779 if val_2 == None: val_2 = None
782 return -cmp(val_1, val_2)
784 return cmp(val_1, val_2)
786 # alphabetical rankings (a < z)
787 cmp_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid")
788 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
789 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
790 cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter")
791 cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary")
792 cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings")
793 # chronological rankings (newer < older)
794 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
796 def cmp_comments(bug_1, bug_2):
798 Compare two bugs' comments lists. Doesn't load any new comments,
799 so you should call each bug's .load_comments() first if you want a
802 comms_1 = sorted(bug_1.comments(), key = lambda comm : comm.uuid)
803 comms_2 = sorted(bug_2.comments(), key = lambda comm : comm.uuid)
804 result = cmp(len(comms_1), len(comms_2))
807 for c_1,c_2 in zip(comms_1, comms_2):
808 result = cmp(c_1, c_2)
813 DEFAULT_CMP_FULL_CMP_LIST = \
814 (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator,
815 cmp_reporter, cmp_comments, cmp_summary, cmp_uuid, cmp_extra_strings)
817 class BugCompoundComparator (object):
818 def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
819 self.cmp_list = cmp_list
820 def __call__(self, bug_1, bug_2):
821 for comparison in self.cmp_list :
822 val = comparison(bug_1, bug_2)
827 cmp_full = BugCompoundComparator()
830 # define some bonus cmp_* functions
831 def cmp_last_modified(bug_1, bug_2):
833 Like cmp_time(), but use most recent comment instead of bug
834 creation for the timestamp.
836 def last_modified(bug):
838 for comment in bug.comment_root.traverse():
839 if comment.time > time:
842 val_1 = last_modified(bug_1)
843 val_2 = last_modified(bug_2)
844 return -cmp(val_1, val_2)
847 if libbe.TESTING == True:
848 suite = doctest.DocTestSuite()