1 # Copyright (C) 2008-2011 Gianluca Montecchi <gian@grys.it>
2 # Thomas Habets <thomas@habets.pp.se>
3 # W. Trevor King <wking@drexel.edu>
5 # This file is part of Bugs Everywhere.
7 # Bugs Everywhere is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by the
9 # Free Software Foundation, either version 2 of the License, or (at your
10 # option) any later version.
12 # Bugs Everywhere is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
20 """Define the :class:`Bug` class for representing bugs.
30 try: # import core module, Python >= 2.5
31 from xml.etree import ElementTree
32 except ImportError: # look for non-core module
33 from elementtree import ElementTree
34 import xml.sax.saxutils
38 from libbe.storage.util.properties import Property, doc_property, \
39 local_property, defaulting_property, checked_property, cached_property, \
40 primed_property, change_hook_property, settings_property
41 import libbe.storage.util.settings_object as settings_object
42 import libbe.storage.util.mapfile as mapfile
43 import libbe.comment as comment
44 import libbe.util.utility as utility
46 if libbe.TESTING == True:
50 ### Define and describe valid bug categories
51 # Use a tuple of (category, description) tuples since we don't have
52 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
54 # in order of increasing severity. (name, description) pairs
56 ("target", "The issue is a target or milestone, not a bug."),
57 ("wishlist","A feature that could improve usefulness, but not a bug."),
58 ("minor","The standard bug level."),
59 ("serious","A bug that requires workarounds."),
60 ("critical","A bug that prevents some features from working at all."),
61 ("fatal","A bug that makes the package unusable."))
63 # in order of increasing resolution
64 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
66 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
67 ("open","A working bug that has not been assigned to a developer."),
68 ("assigned","A working bug that has been assigned to a developer."),
69 ("test","The code has been adjusted, but the fix is still being tested."))
70 inactive_status_def = (
71 ("closed", "The bug is no longer relevant."),
72 ("fixed", "The bug should no longer occur."),
73 ("wontfix","It's not a bug, it's a feature."))
76 ### Convert the description tuples to more useful formats
79 severity_description = {}
81 def load_severities(severity_def):
82 global severity_values
83 global severity_description
85 if severity_def == None:
87 severity_values = tuple([val for val,description in severity_def])
88 severity_description = dict(severity_def)
90 for i,severity in enumerate(severity_values):
91 severity_index[severity] = i
92 load_severities(severity_def)
94 active_status_values = []
95 inactive_status_values = []
97 status_description = {}
99 def load_status(active_status_def, inactive_status_def):
100 global active_status_values
101 global inactive_status_values
103 global status_description
105 if active_status_def == None:
106 active_status_def = globals()["active_status_def"]
107 if inactive_status_def == None:
108 inactive_status_def = globals()["inactive_status_def"]
109 active_status_values = tuple([val for val,description in active_status_def])
110 inactive_status_values = tuple([val for val,description in inactive_status_def])
111 status_values = active_status_values + inactive_status_values
112 status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
114 for i,status in enumerate(status_values):
115 status_index[status] = i
116 load_status(active_status_def, inactive_status_def)
119 class Bug (settings_object.SavedSettingsObject):
120 """A bug (or issue) is a place to store attributes and attach
121 :class:`~libbe.comment.Comment`\s. In mailing-list terms, a bug is
122 analogous to a thread. Bugs are normally stored in
123 :class:`~libbe.bugdir.BugDir`\s.
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.
135 >>> print type(b.time)
137 >>> print type(b.time_string)
140 >>> print b.time_string
141 Thu, 01 Jan 1970 00:00:00 +0000
142 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
145 >>> print b.settings["time"]
146 Thu, 01 Jan 1970 00:01:00 +0000
148 settings_properties = []
149 required_saved_properties = []
150 _prop_save_settings = settings_object.prop_save_settings
151 _prop_load_settings = settings_object.prop_load_settings
152 def _versioned_property(settings_properties=settings_properties,
153 required_saved_properties=required_saved_properties,
155 if "settings_properties" not in kwargs:
156 kwargs["settings_properties"] = settings_properties
157 if "required_saved_properties" not in kwargs:
158 kwargs["required_saved_properties"]=required_saved_properties
159 return settings_object.versioned_property(**kwargs)
161 @_versioned_property(name="severity",
162 doc="A measure of the bug's importance",
164 check_fn=lambda s: s in severity_values,
166 def severity(): return {}
168 @_versioned_property(name="status",
169 doc="The bug's current status",
171 check_fn=lambda s: s in status_values,
173 def status(): return {}
177 return self.status in active_status_values
179 @_versioned_property(name="creator",
180 doc="The user who entered the bug into the system")
181 def creator(): return {}
183 @_versioned_property(name="reporter",
184 doc="The user who reported the bug")
185 def reporter(): return {}
187 @_versioned_property(name="assigned",
188 doc="The developer in charge of the bug")
189 def assigned(): return {}
191 @_versioned_property(name="time",
192 doc="An RFC 2822 timestamp for bug creation")
193 def time_string(): return {}
196 if self.time_string == None:
198 return utility.str_to_time(self.time_string)
199 def _set_time(self, value):
200 self.time_string = utility.time_to_str(value)
201 time = property(fget=_get_time,
203 doc="An integer version of .time_string")
205 def _extra_strings_check_fn(value):
206 return utility.iterable_full_of_strings(value, \
207 alternative=settings_object.EMPTY)
208 def _extra_strings_change_hook(self, old, new):
209 self.extra_strings.sort() # to make merging easier
210 self._prop_save_settings(old, new)
211 @_versioned_property(name="extra_strings",
212 doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
214 check_fn=_extra_strings_check_fn,
215 change_hook=_extra_strings_change_hook,
217 def extra_strings(): return {}
219 @_versioned_property(name="summary",
220 doc="A one-line bug description")
221 def summary(): return {}
223 def _get_comment_root(self, load_full=False):
224 if self.storage != None and self.storage.is_readable():
225 return comment.load_comments(self, load_full=load_full)
227 return comment.Comment(self, uuid=comment.INVALID_UUID)
230 @cached_property(generator=_get_comment_root)
231 @local_property("comment_root")
232 @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.")
233 def comment_root(): return {}
235 def __init__(self, bugdir=None, uuid=None, from_storage=False,
236 load_comments=False, summary=None):
237 settings_object.SavedSettingsObject.__init__(self)
241 self.id = libbe.util.id.ID(self, 'bug')
242 if from_storage == False:
244 self.uuid = libbe.util.id.uuid_gen()
245 self.time = int(time.time()) # only save to second precision
246 self.summary = summary
247 dummy = self.comment_root
248 if self.bugdir != None:
249 self.storage = self.bugdir.storage
250 if from_storage == False:
251 if self.storage != None and self.storage.is_writeable():
255 return "Bug(uuid=%r)" % self.uuid
258 return self.string(shortlist=True)
260 def __cmp__(self, other):
261 return cmp_full(self, other)
263 # serializing methods
265 def _setting_attr_string(self, setting):
266 value = getattr(self, setting)
269 if type(value) not in types.StringTypes:
273 def string(self, shortlist=False, show_comments=False):
274 if shortlist == False:
275 if self.time == None:
278 htime = utility.handy_time(self.time)
279 timestring = "%s (%s)" % (htime, self.time_string)
280 info = [("ID", self.uuid),
281 ("Short name", self.id.user()),
282 ("Severity", self.severity),
283 ("Status", self.status),
284 ("Assigned", self._setting_attr_string("assigned")),
285 ("Reporter", self._setting_attr_string("reporter")),
286 ("Creator", self._setting_attr_string("creator")),
287 ("Created", timestring)]
288 for estr in self.extra_strings:
289 info.append(('Extra string', estr))
290 longest_key_len = max([len(k) for k,v in info])
291 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
292 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
294 statuschar = self.status[0]
295 severitychar = self.severity[0]
296 chars = "%c%c" % (statuschar, severitychar)
297 bugout = "%s:%s: %s" % (self.id.user(),chars,self.summary.rstrip('\n'))
299 if show_comments == True:
300 self.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
301 comout = self.comment_root.string_thread(flatten=False)
302 output = bugout + '\n' + comout.rstrip('\n')
307 def xml(self, indent=0, show_comments=False):
308 if self.time == None:
311 timestring = utility.time_to_str(self.time)
313 info = [('uuid', self.uuid),
314 ('short-name', self.id.user()),
315 ('severity', self.severity),
316 ('status', self.status),
317 ('assigned', self.assigned),
318 ('reporter', self.reporter),
319 ('creator', self.creator),
320 ('created', timestring),
321 ('summary', self.summary)]
325 lines.append(' <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
326 for estr in self.extra_strings:
327 lines.append(' <extra-string>%s</extra-string>' % estr)
328 if show_comments == True:
329 comout = self.comment_root.xml_thread(indent=indent+2)
332 lines.append('</bug>')
335 return istring + sep.join(lines).rstrip('\n')
337 def from_xml(self, xml_string, preserve_uuids=False, verbose=True):
339 Note: If a bug uuid is given, set .alt_id to it's value.
340 >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
341 >>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000"
342 >>> bugA.creator = u'Fran\xe7ois'
343 >>> bugA.extra_strings += ['TAG: very helpful']
344 >>> commA = bugA.comment_root.new_reply(body='comment A')
345 >>> commB = bugA.comment_root.new_reply(body='comment B')
346 >>> commC = commA.new_reply(body='comment C')
347 >>> xml = bugA.xml(show_comments=True)
349 >>> bugB.from_xml(xml, verbose=True)
350 >>> bugB.xml(show_comments=True) == xml
352 >>> bugB.uuid = bugB.alt_id
353 >>> for comm in bugB.comments():
354 ... comm.uuid = comm.alt_id
355 ... comm.alt_id = None
356 >>> bugB.xml(show_comments=True) == xml
358 >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE
359 ['severity', 'status', 'creator', 'time', 'summary']
360 >>> len(list(bugB.comments()))
363 >>> bugC.from_xml(xml, preserve_uuids=True)
364 >>> bugC.uuid == bugA.uuid
367 if type(xml_string) == types.UnicodeType:
368 xml_string = xml_string.strip().encode('unicode_escape')
369 if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
372 bug = ElementTree.XML(xml_string)
374 raise utility.InvalidXML( \
375 'bug', bug, 'root element must be <comment>')
376 tags=['uuid','short-name','severity','status','assigned',
377 'reporter', 'creator','created','summary','extra-string']
378 self.explicit_attrs = []
382 for child in bug.getchildren():
383 if child.tag == 'short-name':
385 elif child.tag == 'comment':
386 comm = comment.Comment(bug=self)
388 child, preserve_uuids=preserve_uuids, verbose=verbose)
389 comments.append(comm)
391 elif child.tag in tags:
392 if child.text == None or len(child.text) == 0:
393 text = settings_object.EMPTY
395 text = xml.sax.saxutils.unescape(child.text)
396 text = text.decode('unicode_escape').strip()
397 if child.tag == 'uuid' and not preserve_uuids:
399 continue # don't set the bug's uuid tag.
400 elif child.tag == 'created':
401 self.time = utility.str_to_time(text)
402 self.explicit_attrs.append('time')
404 elif child.tag == 'extra-string':
406 continue # don't set the bug's extra_string yet.
407 attr_name = child.tag.replace('-','_')
408 self.explicit_attrs.append(attr_name)
409 setattr(self, attr_name, text)
410 elif verbose == True:
411 print >> sys.stderr, 'Ignoring unknown tag %s in %s' \
412 % (child.tag, comment.tag)
413 if uuid != self.uuid:
414 if not hasattr(self, 'alt_id') or self.alt_id == None:
416 self.extra_strings = estrs
417 self.add_comments(comments, ignore_missing_references=True)
419 def add_comment(self, comment, *args, **kwargs):
421 Add a comment too the current bug, under the parent specified
422 by comment.in_reply_to.
423 Note: If a bug uuid is given, set .alt_id to it's value.
425 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.add_comment()')
426 >>> bugA.creator = 'Jack'
427 >>> commA = bugA.comment_root.new_reply(body='comment A')
428 >>> commA.uuid = 'commA'
429 >>> commB = comment.Comment(body='comment B')
430 >>> commB.uuid = 'commB'
431 >>> bugA.add_comment(commB)
432 >>> commC = comment.Comment(body='comment C')
433 >>> commC.uuid = 'commC'
434 >>> commC.in_reply_to = commA.uuid
435 >>> bugA.add_comment(commC)
436 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
439 <short-name>/012</short-name>
440 <severity>minor</severity>
441 <status>open</status>
442 <creator>Jack</creator>
443 <created>...</created>
444 <summary>Need to test Bug.add_comment()</summary>
447 <short-name>/012/commA</short-name>
450 <content-type>text/plain</content-type>
451 <body>comment A</body>
455 <short-name>/012/commC</short-name>
456 <in-reply-to>commA</in-reply-to>
459 <content-type>text/plain</content-type>
460 <body>comment C</body>
464 <short-name>/012/commB</short-name>
467 <content-type>text/plain</content-type>
468 <body>comment B</body>
472 self.add_comments([comment], **kwargs)
474 def add_comments(self, comments, default_parent=None,
475 ignore_missing_references=False):
477 Convert a raw list of comments to single root comment. If a
478 comment does not specify a parent with .in_reply_to, the
479 parent defaults to .comment_root, but you can specify another
480 default parent via default_parent.
483 if default_parent == None:
484 default_parent = self.comment_root
485 for c in list(self.comments()) + comments:
486 assert c.uuid != None
487 assert c.uuid not in uuid_map
490 uuid_map[c.alt_id] = c
491 uuid_map[None] = self.comment_root
492 uuid_map[comment.INVALID_UUID] = self.comment_root
493 if default_parent != self.comment_root:
494 assert default_parent.uuid in uuid_map, default_parent.uuid
496 if c.in_reply_to == None \
497 and default_parent.uuid != comment.INVALID_UUID:
498 c.in_reply_to = default_parent.uuid
499 elif c.in_reply_to == comment.INVALID_UUID:
502 parent = uuid_map[c.in_reply_to]
504 if ignore_missing_references == True:
505 print >> sys.stderr, \
506 'Ignoring missing reference to %s' % c.in_reply_to
507 parent = default_parent
508 if parent.uuid != comment.INVALID_UUID:
509 c.in_reply_to = parent.uuid
511 raise comment.MissingReference(c)
515 def merge(self, other, accept_changes=True,
516 accept_extra_strings=True, accept_comments=True,
517 change_exception=False):
519 Merge info from other into this bug. Overrides any attributes
520 in self that are listed in other.explicit_attrs.
522 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()')
523 >>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
524 >>> bugA.creator = 'Frank'
525 >>> bugA.extra_strings += ['TAG: very helpful']
526 >>> bugA.extra_strings += ['TAG: favorite']
527 >>> commA = bugA.comment_root.new_reply(body='comment A')
528 >>> commA.uuid = 'uuid-commA'
529 >>> bugB = Bug(uuid='3210', summary='More tests for Bug.merge()')
530 >>> bugB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
531 >>> bugB.creator = 'John'
532 >>> bugB.explicit_attrs = ['creator', 'summary']
533 >>> bugB.extra_strings += ['TAG: very helpful']
534 >>> bugB.extra_strings += ['TAG: useful']
535 >>> commB = bugB.comment_root.new_reply(body='comment B')
536 >>> commB.uuid = 'uuid-commB'
537 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
538 ... accept_comments=False, change_exception=False)
539 >>> print bugA.creator
541 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
542 ... accept_comments=False, change_exception=True)
543 Traceback (most recent call last):
545 ValueError: Merge would change creator "Frank"->"John" for bug 0123
546 >>> print bugA.creator
548 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=False,
549 ... accept_comments=False, change_exception=True)
550 Traceback (most recent call last):
552 ValueError: Merge would add extra string "TAG: useful" for bug 0123
553 >>> print bugA.creator
555 >>> print bugA.extra_strings
556 ['TAG: favorite', 'TAG: very helpful']
557 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
558 ... accept_comments=False, change_exception=True)
559 Traceback (most recent call last):
561 ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123
562 >>> print bugA.extra_strings
563 ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
564 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
565 ... accept_comments=True, change_exception=True)
566 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
569 <short-name>/012</short-name>
570 <severity>minor</severity>
571 <status>open</status>
572 <creator>John</creator>
573 <created>...</created>
574 <summary>More tests for Bug.merge()</summary>
575 <extra-string>TAG: favorite</extra-string>
576 <extra-string>TAG: useful</extra-string>
577 <extra-string>TAG: very helpful</extra-string>
579 <uuid>uuid-commA</uuid>
580 <short-name>/012/uuid-commA</short-name>
583 <content-type>text/plain</content-type>
584 <body>comment A</body>
587 <uuid>uuid-commB</uuid>
588 <short-name>/012/uuid-commB</short-name>
591 <content-type>text/plain</content-type>
592 <body>comment B</body>
596 for attr in other.explicit_attrs:
597 old = getattr(self, attr)
598 new = getattr(other, attr)
600 if accept_changes == True:
601 setattr(self, attr, new)
602 elif change_exception == True:
604 'Merge would change %s "%s"->"%s" for bug %s' \
605 % (attr, old, new, self.uuid)
606 for estr in other.extra_strings:
607 if not estr in self.extra_strings:
608 if accept_extra_strings == True:
609 self.extra_strings.append(estr)
610 elif change_exception == True:
612 'Merge would add extra string "%s" for bug %s' \
614 for o_comm in other.comments():
616 s_comm = self.comment_root.comment_from_uuid(o_comm.uuid)
619 s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id)
623 if accept_comments == True:
624 o_comm_copy = copy.copy(o_comm)
625 o_comm_copy.bug = self
626 o_comm_copy.id = libbe.util.id.ID(o_comm_copy, 'comment')
627 self.comment_root.add_reply(o_comm_copy)
628 elif change_exception == True:
630 'Merge would add comment %s (alt: %s) to bug %s' \
631 % (o_comm.uuid, o_comm.alt_id, self.uuid)
633 s_comm.merge(o_comm, accept_changes=accept_changes,
634 accept_extra_strings=accept_extra_strings,
635 change_exception=change_exception)
637 # methods for saving/loading/acessing settings and properties.
639 def load_settings(self, settings_mapfile=None):
640 if settings_mapfile == None:
641 settings_mapfile = self.storage.get(
642 self.id.storage('values'), '\n')
644 settings = mapfile.parse(settings_mapfile)
645 except mapfile.InvalidMapfileContents, e:
646 raise Exception('Invalid settings file for bug %s\n'
647 '(BE version missmatch?)' % self.id.user())
648 self._setup_saved_settings(settings)
650 def save_settings(self):
651 mf = mapfile.generate(self._get_saved_settings())
652 self.storage.set(self.id.storage('values'), mf)
656 Save any loaded contents to storage. Because of lazy loading
657 of comments, this is actually not too inefficient.
659 However, if self.storage.is_writeable() == True, then any
660 changes are automatically written to storage as soon as they
661 happen, so calling this method will just waste time (unless
662 something else has been messing with your stored files).
664 assert self.storage != None, "Can't save without storage"
665 if self.bugdir != None:
666 parent = self.bugdir.id.storage()
669 self.storage.add(self.id.storage(), parent=parent, directory=True)
670 self.storage.add(self.id.storage('values'), parent=self.id.storage(),
673 if len(self.comment_root) > 0:
674 comment.save_comments(self)
676 def load_comments(self, load_full=True):
677 if load_full == True:
678 # Force a complete load of the whole comment tree
679 self.comment_root = self._get_comment_root(load_full=True)
681 # Setup for fresh lazy-loading. Clear _comment_root, so
682 # next _get_comment_root returns a fresh version. Turn of
683 # writing temporarily so we don't write our blank comment
685 w = self.storage.writeable
686 self.storage.writeable = False
687 self.comment_root = None
688 self.storage.writeable = w
691 self.storage.recursive_remove(self.id.storage())
693 # methods for managing comments
696 for comment in self.comments():
700 for comment in self.comment_root.traverse():
703 def new_comment(self, body=None):
704 comm = self.comment_root.new_reply(body=body)
707 def comment_from_uuid(self, uuid, *args, **kwargs):
708 return self.comment_root.comment_from_uuid(uuid, *args, **kwargs)
710 # methods for id generation
712 def sibling_uuids(self):
713 if self.bugdir != None:
714 return self.bugdir.uuids()
718 # The general rule for bug sorting is that "more important" bugs are
719 # less than "less important" bugs. This way sorting a list of bugs
720 # will put the most important bugs first in the list. When relative
721 # importance is unclear, the sorting follows some arbitrary convention
722 # (i.e. dictionary order).
724 def cmp_severity(bug_1, bug_2):
726 Compare the severity levels of two bugs, with more severe bugs
731 >>> bugA.severity = bugB.severity = "wishlist"
732 >>> cmp_severity(bugA, bugB) == 0
734 >>> bugB.severity = "minor"
735 >>> cmp_severity(bugA, bugB) > 0
737 >>> bugA.severity = "critical"
738 >>> cmp_severity(bugA, bugB) < 0
741 if not hasattr(bug_2, "severity") :
743 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
745 def cmp_status(bug_1, bug_2):
747 Compare the status levels of two bugs, with more "open" bugs
752 >>> bugA.status = bugB.status = "open"
753 >>> cmp_status(bugA, bugB) == 0
755 >>> bugB.status = "closed"
756 >>> cmp_status(bugA, bugB) < 0
758 >>> bugA.status = "fixed"
759 >>> cmp_status(bugA, bugB) > 0
762 if not hasattr(bug_2, "status") :
764 val_2 = status_index[bug_2.status]
765 return cmp(status_index[bug_1.status], status_index[bug_2.status])
767 def cmp_attr(bug_1, bug_2, attr, invert=False):
769 Compare a general attribute between two bugs using the
770 conventional comparison rule for that attribute type. If
771 ``invert==True``, sort *against* that convention.
776 >>> bugA.severity = "critical"
777 >>> bugB.severity = "wishlist"
778 >>> cmp_attr(bugA, bugB, attr) < 0
780 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
782 >>> bugB.severity = "critical"
783 >>> cmp_attr(bugA, bugB, attr) == 0
786 if not hasattr(bug_2, attr) :
788 val_1 = getattr(bug_1, attr)
789 val_2 = getattr(bug_2, attr)
790 if val_1 == None: val_1 = None
791 if val_2 == None: val_2 = None
794 return -cmp(val_1, val_2)
796 return cmp(val_1, val_2)
798 # alphabetical rankings (a < z)
799 cmp_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid")
800 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
801 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
802 cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter")
803 cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary")
804 cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings")
805 # chronological rankings (newer < older)
806 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
808 def cmp_mine(bug_1, bug_2):
809 user_id = libbe.ui.util.user.get_user_id(bug_1.storage)
810 mine_1 = bug_1.assigned != user_id
811 mine_2 = bug_2.assigned != user_id
812 return cmp(mine_1, mine_2)
814 def cmp_comments(bug_1, bug_2):
816 Compare two bugs' comments lists. Doesn't load any new comments,
817 so you should call each bug's .load_comments() first if you want a
820 comms_1 = sorted(bug_1.comments(), key = lambda comm : comm.uuid)
821 comms_2 = sorted(bug_2.comments(), key = lambda comm : comm.uuid)
822 result = cmp(len(comms_1), len(comms_2))
825 for c_1,c_2 in zip(comms_1, comms_2):
826 result = cmp(c_1, c_2)
831 DEFAULT_CMP_FULL_CMP_LIST = \
832 (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator,
833 cmp_reporter, cmp_comments, cmp_summary, cmp_uuid, cmp_extra_strings)
835 class BugCompoundComparator (object):
836 def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
837 self.cmp_list = cmp_list
838 def __call__(self, bug_1, bug_2):
839 for comparison in self.cmp_list :
840 val = comparison(bug_1, bug_2)
845 cmp_full = BugCompoundComparator()
848 # define some bonus cmp_* functions
849 def cmp_last_modified(bug_1, bug_2):
851 Like cmp_time(), but use most recent comment instead of bug
852 creation for the timestamp.
854 def last_modified(bug):
856 for comment in bug.comment_root.traverse():
857 if comment.time > time:
860 val_1 = last_modified(bug_1)
861 val_2 = last_modified(bug_2)
862 return -cmp(val_1, val_2)
865 if libbe.TESTING == True:
866 suite = doctest.DocTestSuite()