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 class DiskAccessRequired (Exception):
51 def __init__(self, goal):
52 msg = "Cannot %s without accessing the disk" % goal
53 Exception.__init__(self, msg)
55 ### Define and describe valid bug categories
56 # Use a tuple of (category, description) tuples since we don't have
57 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
59 # in order of increasing severity. (name, description) pairs
61 ("target", "The issue is a target or milestone, not a bug."),
62 ("wishlist","A feature that could improve usefulness, but not a bug."),
63 ("minor","The standard bug level."),
64 ("serious","A bug that requires workarounds."),
65 ("critical","A bug that prevents some features from working at all."),
66 ("fatal","A bug that makes the package unusable."))
68 # in order of increasing resolution
69 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
71 ("unconfirmed","A possible bug which lacks independent existance confirmation."),
72 ("open","A working bug that has not been assigned to a developer."),
73 ("assigned","A working bug that has been assigned to a developer."),
74 ("test","The code has been adjusted, but the fix is still being tested."))
75 inactive_status_def = (
76 ("closed", "The bug is no longer relevant."),
77 ("fixed", "The bug should no longer occur."),
78 ("wontfix","It's not a bug, it's a feature."))
81 ### Convert the description tuples to more useful formats
84 severity_description = {}
86 def load_severities(severity_def):
87 global severity_values
88 global severity_description
90 if severity_def == None:
92 severity_values = tuple([val for val,description in severity_def])
93 severity_description = dict(severity_def)
95 for i,severity in enumerate(severity_values):
96 severity_index[severity] = i
97 load_severities(severity_def)
99 active_status_values = []
100 inactive_status_values = []
102 status_description = {}
104 def load_status(active_status_def, inactive_status_def):
105 global active_status_values
106 global inactive_status_values
108 global status_description
110 if active_status_def == None:
111 active_status_def = globals()["active_status_def"]
112 if inactive_status_def == None:
113 inactive_status_def = globals()["inactive_status_def"]
114 active_status_values = tuple([val for val,description in active_status_def])
115 inactive_status_values = tuple([val for val,description in inactive_status_def])
116 status_values = active_status_values + inactive_status_values
117 status_description = dict(tuple(active_status_def) + tuple(inactive_status_def))
119 for i,status in enumerate(status_values):
120 status_index[status] = i
121 load_status(active_status_def, inactive_status_def)
124 class Bug (settings_object.SavedSettingsObject):
125 """A bug (or issue) is a place to store attributes and attach
126 :class:`~libbe.comment.Comment`\s. In mailing-list terms, a bug is
127 analogous to a thread. Bugs are normally stored in
128 :class:`~libbe.bugdir.BugDir`\s.
136 There are two formats for time, int and string. Setting either
137 one will adjust the other appropriately. The string form is the
138 one stored in the bug's settings file on disk.
140 >>> print type(b.time)
142 >>> print type(b.time_string)
145 >>> print b.time_string
146 Thu, 01 Jan 1970 00:00:00 +0000
147 >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000"
150 >>> print b.settings["time"]
151 Thu, 01 Jan 1970 00:01:00 +0000
153 settings_properties = []
154 required_saved_properties = []
155 _prop_save_settings = settings_object.prop_save_settings
156 _prop_load_settings = settings_object.prop_load_settings
157 def _versioned_property(settings_properties=settings_properties,
158 required_saved_properties=required_saved_properties,
160 if "settings_properties" not in kwargs:
161 kwargs["settings_properties"] = settings_properties
162 if "required_saved_properties" not in kwargs:
163 kwargs["required_saved_properties"]=required_saved_properties
164 return settings_object.versioned_property(**kwargs)
166 @_versioned_property(name="severity",
167 doc="A measure of the bug's importance",
169 check_fn=lambda s: s in severity_values,
171 def severity(): return {}
173 @_versioned_property(name="status",
174 doc="The bug's current status",
176 check_fn=lambda s: s in status_values,
178 def status(): return {}
182 return self.status in active_status_values
184 @_versioned_property(name="creator",
185 doc="The user who entered the bug into the system")
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()
250 self.time = int(time.time()) # only save to second precision
251 self.summary = summary
252 dummy = self.comment_root
253 if self.bugdir != None:
254 self.storage = self.bugdir.storage
255 if from_storage == False:
256 if self.storage != None and self.storage.is_writeable():
260 return "Bug(uuid=%r)" % self.uuid
263 return self.string(shortlist=True)
265 def __cmp__(self, other):
266 return cmp_full(self, other)
268 # serializing methods
270 def _setting_attr_string(self, setting):
271 value = getattr(self, setting)
274 if type(value) not in types.StringTypes:
278 def string(self, shortlist=False, show_comments=False):
279 if shortlist == False:
280 if self.time == None:
283 htime = utility.handy_time(self.time)
284 timestring = "%s (%s)" % (htime, self.time_string)
285 info = [("ID", self.uuid),
286 ("Short name", self.id.user()),
287 ("Severity", self.severity),
288 ("Status", self.status),
289 ("Assigned", self._setting_attr_string("assigned")),
290 ("Reporter", self._setting_attr_string("reporter")),
291 ("Creator", self._setting_attr_string("creator")),
292 ("Created", timestring)]
293 longest_key_len = max([len(k) for k,v in info])
294 infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
295 bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
297 statuschar = self.status[0]
298 severitychar = self.severity[0]
299 chars = "%c%c" % (statuschar, severitychar)
300 bugout = "%s:%s: %s" % (self.id.user(),chars,self.summary.rstrip('\n'))
302 if show_comments == True:
303 self.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
304 comout = self.comment_root.string_thread(flatten=False)
305 output = bugout + '\n' + comout.rstrip('\n')
310 def xml(self, indent=0, show_comments=False):
311 if self.time == None:
314 timestring = utility.time_to_str(self.time)
316 info = [('uuid', self.uuid),
317 ('short-name', self.id.user()),
318 ('severity', self.severity),
319 ('status', self.status),
320 ('assigned', self.assigned),
321 ('reporter', self.reporter),
322 ('creator', self.creator),
323 ('created', timestring),
324 ('summary', self.summary)]
328 lines.append(' <%s>%s</%s>' % (k,xml.sax.saxutils.escape(v),k))
329 for estr in self.extra_strings:
330 lines.append(' <extra-string>%s</extra-string>' % estr)
331 if show_comments == True:
332 comout = self.comment_root.xml_thread(indent=indent+2)
335 lines.append('</bug>')
338 return istring + sep.join(lines).rstrip('\n')
340 def from_xml(self, xml_string, verbose=True):
342 Note: If a bug uuid is given, set .alt_id to it's value.
343 >>> bugA = Bug(uuid="0123", summary="Need to test Bug.from_xml()")
344 >>> bugA.date = "Thu, 01 Jan 1970 00:00:00 +0000"
345 >>> bugA.creator = u'Fran\xe7ois'
346 >>> bugA.extra_strings += ['TAG: very helpful']
347 >>> commA = bugA.comment_root.new_reply(body='comment A')
348 >>> commB = bugA.comment_root.new_reply(body='comment B')
349 >>> commC = commA.new_reply(body='comment C')
350 >>> xml = bugA.xml(show_comments=True)
352 >>> bugB.from_xml(xml, verbose=True)
353 >>> bugB.xml(show_comments=True) == xml
355 >>> bugB.uuid = bugB.alt_id
356 >>> for comm in bugB.comments():
357 ... comm.uuid = comm.alt_id
358 ... comm.alt_id = None
359 >>> bugB.xml(show_comments=True) == xml
361 >>> bugB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE
362 ['severity', 'status', 'creator', 'time', 'summary']
363 >>> len(list(bugB.comments()))
366 if type(xml_string) == types.UnicodeType:
367 xml_string = xml_string.strip().encode('unicode_escape')
368 if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
371 bug = ElementTree.XML(xml_string)
373 raise utility.InvalidXML( \
374 'bug', bug, 'root element must be <comment>')
375 tags=['uuid','short-name','severity','status','assigned',
376 'reporter', 'creator','created','summary','extra-string']
377 self.explicit_attrs = []
381 for child in bug.getchildren():
382 if child.tag == 'short-name':
384 elif child.tag == 'comment':
385 comm = comment.Comment(bug=self)
387 comments.append(comm)
389 elif child.tag in tags:
390 if child.text == None or len(child.text) == 0:
391 text = settings_object.EMPTY
393 text = xml.sax.saxutils.unescape(child.text)
394 text = text.decode('unicode_escape').strip()
395 if child.tag == 'uuid':
397 continue # don't set the bug's uuid tag.
398 elif child.tag == 'created':
399 self.time = utility.str_to_time(text)
400 self.explicit_attrs.append('time')
402 elif child.tag == 'extra-string':
404 continue # don't set the bug's extra_string yet.
405 attr_name = child.tag.replace('-','_')
406 self.explicit_attrs.append(attr_name)
407 setattr(self, attr_name, text)
408 elif verbose == True:
409 print >> sys.stderr, 'Ignoring unknown tag %s in %s' \
410 % (child.tag, comment.tag)
411 if uuid != self.uuid:
412 if not hasattr(self, 'alt_id') or self.alt_id == None:
414 self.extra_strings = estrs
415 self.add_comments(comments, ignore_missing_references=True)
417 def add_comment(self, comment, *args, **kwargs):
419 Add a comment too the current bug, under the parent specified
420 by comment.in_reply_to.
421 Note: If a bug uuid is given, set .alt_id to it's value.
423 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.add_comment()')
424 >>> bugA.creator = 'Jack'
425 >>> commA = bugA.comment_root.new_reply(body='comment A')
426 >>> commA.uuid = 'commA'
427 >>> commB = comment.Comment(body='comment B')
428 >>> commB.uuid = 'commB'
429 >>> bugA.add_comment(commB)
430 >>> commC = comment.Comment(body='comment C')
431 >>> commC.uuid = 'commC'
432 >>> commC.in_reply_to = commA.uuid
433 >>> bugA.add_comment(commC)
434 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
437 <short-name>/012</short-name>
438 <severity>minor</severity>
439 <status>open</status>
440 <creator>Jack</creator>
441 <created>...</created>
442 <summary>Need to test Bug.add_comment()</summary>
445 <short-name>/012/commA</short-name>
448 <content-type>text/plain</content-type>
449 <body>comment A</body>
453 <short-name>/012/commC</short-name>
454 <in-reply-to>commA</in-reply-to>
457 <content-type>text/plain</content-type>
458 <body>comment C</body>
462 <short-name>/012/commB</short-name>
465 <content-type>text/plain</content-type>
466 <body>comment B</body>
470 self.add_comments([comment], **kwargs)
472 def add_comments(self, comments, default_parent=None,
473 ignore_missing_references=False):
475 Convert a raw list of comments to single root comment. If a
476 comment does not specify a parent with .in_reply_to, the
477 parent defaults to .comment_root, but you can specify another
478 default parent via default_parent.
481 if default_parent == None:
482 default_parent = self.comment_root
483 for c in list(self.comments()) + comments:
484 assert c.uuid != None
485 assert c.uuid not in uuid_map
488 uuid_map[c.alt_id] = c
489 uuid_map[None] = self.comment_root
490 uuid_map[comment.INVALID_UUID] = self.comment_root
491 if default_parent != self.comment_root:
492 assert default_parent.uuid in uuid_map, default_parent.uuid
494 if c.in_reply_to == None \
495 and default_parent.uuid != comment.INVALID_UUID:
496 c.in_reply_to = default_parent.uuid
497 elif c.in_reply_to == comment.INVALID_UUID:
500 parent = uuid_map[c.in_reply_to]
502 if ignore_missing_references == True:
503 print >> sys.stderr, \
504 'Ignoring missing reference to %s' % c.in_reply_to
505 parent = default_parent
506 if parent.uuid != comment.INVALID_UUID:
507 c.in_reply_to = parent.uuid
509 raise comment.MissingReference(c)
513 def merge(self, other, accept_changes=True,
514 accept_extra_strings=True, accept_comments=True,
515 change_exception=False):
517 Merge info from other into this bug. Overrides any attributes
518 in self that are listed in other.explicit_attrs.
520 >>> bugA = Bug(uuid='0123', summary='Need to test Bug.merge()')
521 >>> bugA.date = 'Thu, 01 Jan 1970 00:00:00 +0000'
522 >>> bugA.creator = 'Frank'
523 >>> bugA.extra_strings += ['TAG: very helpful']
524 >>> bugA.extra_strings += ['TAG: favorite']
525 >>> commA = bugA.comment_root.new_reply(body='comment A')
526 >>> commA.uuid = 'uuid-commA'
527 >>> bugB = Bug(uuid='3210', summary='More tests for Bug.merge()')
528 >>> bugB.date = 'Fri, 02 Jan 1970 00:00:00 +0000'
529 >>> bugB.creator = 'John'
530 >>> bugB.explicit_attrs = ['creator', 'summary']
531 >>> bugB.extra_strings += ['TAG: very helpful']
532 >>> bugB.extra_strings += ['TAG: useful']
533 >>> commB = bugB.comment_root.new_reply(body='comment B')
534 >>> commB.uuid = 'uuid-commB'
535 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
536 ... accept_comments=False, change_exception=False)
537 >>> print bugA.creator
539 >>> bugA.merge(bugB, accept_changes=False, accept_extra_strings=False,
540 ... accept_comments=False, change_exception=True)
541 Traceback (most recent call last):
543 ValueError: Merge would change creator "Frank"->"John" for bug 0123
544 >>> print bugA.creator
546 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=False,
547 ... accept_comments=False, change_exception=True)
548 Traceback (most recent call last):
550 ValueError: Merge would add extra string "TAG: useful" for bug 0123
551 >>> print bugA.creator
553 >>> print bugA.extra_strings
554 ['TAG: favorite', 'TAG: very helpful']
555 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
556 ... accept_comments=False, change_exception=True)
557 Traceback (most recent call last):
559 ValueError: Merge would add comment uuid-commB (alt: None) to bug 0123
560 >>> print bugA.extra_strings
561 ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
562 >>> bugA.merge(bugB, accept_changes=True, accept_extra_strings=True,
563 ... accept_comments=True, change_exception=True)
564 >>> print bugA.xml(show_comments=True) # doctest: +ELLIPSIS
567 <short-name>/012</short-name>
568 <severity>minor</severity>
569 <status>open</status>
570 <creator>John</creator>
571 <created>...</created>
572 <summary>More tests for Bug.merge()</summary>
573 <extra-string>TAG: favorite</extra-string>
574 <extra-string>TAG: useful</extra-string>
575 <extra-string>TAG: very helpful</extra-string>
577 <uuid>uuid-commA</uuid>
578 <short-name>/012/uuid-commA</short-name>
581 <content-type>text/plain</content-type>
582 <body>comment A</body>
585 <uuid>uuid-commB</uuid>
586 <short-name>/012/uuid-commB</short-name>
589 <content-type>text/plain</content-type>
590 <body>comment B</body>
594 for attr in other.explicit_attrs:
595 old = getattr(self, attr)
596 new = getattr(other, attr)
598 if accept_changes == True:
599 setattr(self, attr, new)
600 elif change_exception == True:
602 'Merge would change %s "%s"->"%s" for bug %s' \
603 % (attr, old, new, self.uuid)
604 for estr in other.extra_strings:
605 if not estr in self.extra_strings:
606 if accept_extra_strings == True:
607 self.extra_strings.append(estr)
608 elif change_exception == True:
610 'Merge would add extra string "%s" for bug %s' \
612 for o_comm in other.comments():
614 s_comm = self.comment_root.comment_from_uuid(o_comm.uuid)
617 s_comm = self.comment_root.comment_from_uuid(o_comm.alt_id)
621 if accept_comments == True:
622 o_comm_copy = copy.copy(o_comm)
623 o_comm_copy.bug = self
624 o_comm_copy.id = libbe.util.id.ID(o_comm_copy, 'comment')
625 self.comment_root.add_reply(o_comm_copy)
626 elif change_exception == True:
628 'Merge would add comment %s (alt: %s) to bug %s' \
629 % (o_comm.uuid, o_comm.alt_id, self.uuid)
631 s_comm.merge(o_comm, accept_changes=accept_changes,
632 accept_extra_strings=accept_extra_strings,
633 change_exception=change_exception)
635 # methods for saving/loading/acessing settings and properties.
637 def load_settings(self, settings_mapfile=None):
638 if settings_mapfile == None:
640 self.storage.get(self.id.storage('values'), default='\n')
642 settings = mapfile.parse(settings_mapfile)
643 except mapfile.InvalidMapfileContents, e:
644 raise Exception('Invalid settings file for bug %s\n'
645 '(BE version missmatch?)' % self.id.user())
646 self._setup_saved_settings(settings)
648 def save_settings(self):
649 mf = mapfile.generate(self._get_saved_settings())
650 self.storage.set(self.id.storage('values'), mf)
654 Save any loaded contents to storage. Because of lazy loading
655 of comments, this is actually not too inefficient.
657 However, if self.storage.is_writeable() == True, then any
658 changes are automatically written to storage as soon as they
659 happen, so calling this method will just waste time (unless
660 something else has been messing with your stored files).
662 assert self.storage != None, "Can't save without storage"
663 if self.bugdir != None:
664 parent = self.bugdir.id.storage()
667 self.storage.add(self.id.storage(), parent=parent, directory=True)
668 self.storage.add(self.id.storage('values'), parent=self.id.storage(),
671 if len(self.comment_root) > 0:
672 comment.save_comments(self)
674 def load_comments(self, load_full=True):
675 if load_full == True:
676 # Force a complete load of the whole comment tree
677 self.comment_root = self._get_comment_root(load_full=True)
679 # Setup for fresh lazy-loading. Clear _comment_root, so
680 # next _get_comment_root returns a fresh version. Turn of
681 # writing temporarily so we don't write our blank comment
683 w = self.storage.writeable
684 self.storage.writeable = False
685 self.comment_root = None
686 self.storage.writeable = w
689 self.storage.recursive_remove(self.id.storage())
691 # methods for managing comments
694 for comment in self.comments():
698 for comment in self.comment_root.traverse():
701 def new_comment(self, body=None):
702 comm = self.comment_root.new_reply(body=body)
705 def comment_from_uuid(self, uuid, *args, **kwargs):
706 return self.comment_root.comment_from_uuid(uuid, *args, **kwargs)
708 # methods for id generation
710 def sibling_uuids(self):
711 if self.bugdir != None:
712 return self.bugdir.uuids()
716 # The general rule for bug sorting is that "more important" bugs are
717 # less than "less important" bugs. This way sorting a list of bugs
718 # will put the most important bugs first in the list. When relative
719 # importance is unclear, the sorting follows some arbitrary convention
720 # (i.e. dictionary order).
722 def cmp_severity(bug_1, bug_2):
724 Compare the severity levels of two bugs, with more severe bugs
729 >>> bugA.severity = bugB.severity = "wishlist"
730 >>> cmp_severity(bugA, bugB) == 0
732 >>> bugB.severity = "minor"
733 >>> cmp_severity(bugA, bugB) > 0
735 >>> bugA.severity = "critical"
736 >>> cmp_severity(bugA, bugB) < 0
739 if not hasattr(bug_2, "severity") :
741 return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
743 def cmp_status(bug_1, bug_2):
745 Compare the status levels of two bugs, with more "open" bugs
750 >>> bugA.status = bugB.status = "open"
751 >>> cmp_status(bugA, bugB) == 0
753 >>> bugB.status = "closed"
754 >>> cmp_status(bugA, bugB) < 0
756 >>> bugA.status = "fixed"
757 >>> cmp_status(bugA, bugB) > 0
760 if not hasattr(bug_2, "status") :
762 val_2 = status_index[bug_2.status]
763 return cmp(status_index[bug_1.status], status_index[bug_2.status])
765 def cmp_attr(bug_1, bug_2, attr, invert=False):
767 Compare a general attribute between two bugs using the
768 conventional comparison rule for that attribute type. If
769 ``invert==True``, sort *against* that convention.
774 >>> bugA.severity = "critical"
775 >>> bugB.severity = "wishlist"
776 >>> cmp_attr(bugA, bugB, attr) < 0
778 >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
780 >>> bugB.severity = "critical"
781 >>> cmp_attr(bugA, bugB, attr) == 0
784 if not hasattr(bug_2, attr) :
786 val_1 = getattr(bug_1, attr)
787 val_2 = getattr(bug_2, attr)
788 if val_1 == None: val_1 = None
789 if val_2 == None: val_2 = None
792 return -cmp(val_1, val_2)
794 return cmp(val_1, val_2)
796 # alphabetical rankings (a < z)
797 cmp_uuid = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "uuid")
798 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
799 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
800 cmp_reporter = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "reporter")
801 cmp_summary = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "summary")
802 cmp_extra_strings = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "extra_strings")
803 # chronological rankings (newer < older)
804 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
806 def cmp_mine(bug_1, bug_2):
807 user_id = libbe.ui.util.user.get_user_id(bug_1.storage)
808 mine_1 = bug_1.assigned != user_id
809 mine_2 = bug_2.assigned != user_id
810 return cmp(mine_1, mine_2)
812 def cmp_comments(bug_1, bug_2):
814 Compare two bugs' comments lists. Doesn't load any new comments,
815 so you should call each bug's .load_comments() first if you want a
818 comms_1 = sorted(bug_1.comments(), key = lambda comm : comm.uuid)
819 comms_2 = sorted(bug_2.comments(), key = lambda comm : comm.uuid)
820 result = cmp(len(comms_1), len(comms_2))
823 for c_1,c_2 in zip(comms_1, comms_2):
824 result = cmp(c_1, c_2)
829 DEFAULT_CMP_FULL_CMP_LIST = \
830 (cmp_status, cmp_severity, cmp_assigned, cmp_time, cmp_creator,
831 cmp_reporter, cmp_comments, cmp_summary, cmp_uuid, cmp_extra_strings)
833 class BugCompoundComparator (object):
834 def __init__(self, cmp_list=DEFAULT_CMP_FULL_CMP_LIST):
835 self.cmp_list = cmp_list
836 def __call__(self, bug_1, bug_2):
837 for comparison in self.cmp_list :
838 val = comparison(bug_1, bug_2)
843 cmp_full = BugCompoundComparator()
846 # define some bonus cmp_* functions
847 def cmp_last_modified(bug_1, bug_2):
849 Like cmp_time(), but use most recent comment instead of bug
850 creation for the timestamp.
852 def last_modified(bug):
854 for comment in bug.comment_root.traverse():
855 if comment.time > time:
858 val_1 = last_modified(bug_1)
859 val_2 = last_modified(bug_2)
860 return -cmp(val_1, val_2)
863 if libbe.TESTING == True:
864 suite = doctest.DocTestSuite()