-# Copyright (C) 2005-2010 Aaron Bentley and Panometrics, Inc.
+# Copyright (C) 2005-2012 Aaron Bentley <abentley@panoramicfeedback.com>
# Alexander Belchenko <bialix@ukr.net>
# Chris Ball <cjb@laptop.org>
# Gianluca Montecchi <gian@grys.it>
# Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
-# W. Trevor King <wking@drexel.edu>
+# W. Trevor King <wking@tremily.us>
#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
+# This file is part of Bugs Everywhere.
#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
+# Bugs Everywhere is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 2 of the License, or (at your option) any
+# later version.
#
-# You should have received a copy of the GNU General Public License along
-# with this program; if not, write to the Free Software Foundation, Inc.,
-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# Bugs Everywhere is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
-"""Define the :class:`BugDir` class for storing a collection of bugs.
+"""Define :py:class:`BugDir` for storing a collection of bugs.
"""
import copy
import os
import os.path
import time
+import types
+try: # import core module, Python >= 2.5
+ from xml.etree import ElementTree
+except ImportError: # look for non-core module
+ from elementtree import ElementTree
+import xml.sax.saxutils
import libbe
import libbe.storage as storage
import libbe.storage.base
-class NoBugDir(Exception):
- def __init__(self, path):
- msg = "The directory \"%s\" has no bug directory." % path
- Exception.__init__(self, msg)
- self.path = path
-
-class NoRootEntry(Exception):
- def __init__(self, path):
- self.path = path
- Exception.__init__(self, "Specified root does not exist: %s" % path)
-
-class AlreadyInitialized(Exception):
- def __init__(self, path):
- self.path = path
- Exception.__init__(self,
- "Specified root is already initialized: %s" % path)
-
-class MultipleBugMatches(ValueError):
- def __init__(self, shortname, matches):
- msg = ("More than one bug matches %s. "
- "Please be more specific.\n%s" % (shortname, matches))
- ValueError.__init__(self, msg)
- self.shortname = shortname
- self.matches = matches
-
class NoBugMatches(libbe.util.id.NoIDMatches):
def __init__(self, *args, **kwargs):
libbe.util.id.NoIDMatches.__init__(self, *args, **kwargs)
return 'No bug matches %s' % self.id
return self.msg
-class DiskAccessRequired (Exception):
- def __init__(self, goal):
- msg = "Cannot %s without accessing the disk" % goal
- Exception.__init__(self, msg)
-
class BugDir (list, settings_object.SavedSettingsObject):
- """A BugDir is a container for :class:`libbe.bug.Bug`\s, with some
+ """A BugDir is a container for :py:class:`~libbe.bug.Bug`\s, with some
additional attributes.
- See :class:`SimpleBugDir` for some bugdir manipulation exampes.
+ Parameters
+ ----------
+ storage : :py:class:`~libbe.storage.base.Storage`
+ Storage instance containing the bug directory. If
+ `from_storage` is `False`, `storage` may be `None`.
+ uuid : str, optional
+ Set the bugdir UUID (see :py:mod:`libbe.util.id`).
+ Useful if you are loading one of several bugdirs
+ stored in a single Storage instance.
+ from_storage : bool, optional
+ If `True`, attempt to load from storage. Otherwise,
+ setup in memory, saving to `storage` if it is not `None`.
+
+ See Also
+ --------
+ SimpleBugDir : bugdir manipulation exampes.
"""
settings_properties = []
def load_settings(self, settings_mapfile=None):
if settings_mapfile == None:
settings_mapfile = \
- self.storage.get(self.id.storage('settings'), default='\n')
+ self.storage.get(self.id.storage('settings'), default='{}\n')
try:
settings = mapfile.parse(settings_mapfile)
except mapfile.InvalidMapfileContents, e:
directory=False)
self.save_settings()
for bug in self:
+ bug.bugdir = self
+ bug.storage = self.storage
bug.save()
# methods for managing bugs
def uuids(self, use_cached_disk_uuids=True):
if use_cached_disk_uuids==False or not hasattr(self, '_uuids_cache'):
- self._uuids_cache = []
- # list bugs that are in storage
- if self.storage != None and self.storage.is_readable():
- child_uuids = libbe.util.id.child_uuids(
- self.storage.children(self.id.storage()))
- for id in child_uuids:
- self._uuids_cache.append(id)
- return list(set([bug.uuid for bug in self] + self._uuids_cache))
+ self._refresh_uuid_cache()
+ self._uuids_cache = self._uuids_cache.union([bug.uuid for bug in self])
+ return self._uuids_cache
+
+ def _refresh_uuid_cache(self):
+ self._uuids_cache = set()
+ # list bugs that are in storage
+ if self.storage != None and self.storage.is_readable():
+ child_uuids = libbe.util.id.child_uuids(
+ self.storage.children(self.id.storage()))
+ for id in child_uuids:
+ self._uuids_cache.add(id)
def _clear_bugs(self):
while len(self) > 0:
def new_bug(self, summary=None, _uuid=None):
bg = bug.Bug(bugdir=self, uuid=_uuid, summary=summary,
from_storage=False)
- self.append(bg)
- self._bug_map_gen()
- if hasattr(self, '_uuids_cache') and not bg.uuid in self._uuids_cache:
- self._uuids_cache.append(bg.uuid)
+ self.append(bg, update=True)
return bg
+ def append(self, bug, update=False):
+ super(BugDir, self).append(bug)
+ if update:
+ bug.bugdir = self
+ bug.storage = self.storage
+ self._bug_map_gen()
+ if (hasattr(self, '_uuids_cache') and
+ not bug.uuid in self._uuids_cache):
+ self._uuids_cache.add(bug.uuid)
+
def remove_bug(self, bug):
if hasattr(self, '_uuids_cache') and bug.uuid in self._uuids_cache:
self._uuids_cache.remove(bug.uuid)
return False
return True
+ def xml(self, indent=0, show_bugs=False, show_comments=False):
+ """
+ >>> bug.load_severities(bug.severity_def)
+ >>> bug.load_status(
+ ... active_status_def=bug.active_status_def,
+ ... inactive_status_def=bug.inactive_status_def)
+ >>> bugdirA = SimpleBugDir(memory=True)
+ >>> bugdirA.severities
+ >>> bugdirA.severities = (('minor', 'The standard bug level.'),)
+ >>> bugdirA.inactive_status = (
+ ... ('closed', 'The bug is no longer relevant.'),)
+ >>> bugA = bugdirA.bug_from_uuid('a')
+ >>> commA = bugA.comment_root.new_reply(body='comment A')
+ >>> commA.uuid = 'commA'
+ >>> commA.date = 'Thu, 01 Jan 1970 00:03:00 +0000'
+ >>> print(bugdirA.xml(show_bugs=True, show_comments=True))
+ ... # doctest: +REPORT_UDIFF
+ <bugdir>
+ <uuid>abc123</uuid>
+ <short-name>abc</short-name>
+ <severities>
+ <entry>
+ <key>minor</key>
+ <value>The standard bug level.</value>
+ </entry>
+ </severities>
+ <inactive-status>
+ <entry>
+ <key>closed</key>
+ <value>The bug is no longer relevant.</value>
+ </entry>
+ </inactive-status>
+ <bug>
+ <uuid>a</uuid>
+ <short-name>abc/a</short-name>
+ <severity>minor</severity>
+ <status>open</status>
+ <creator>John Doe <jdoe@example.com></creator>
+ <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+ <summary>Bug A</summary>
+ <comment>
+ <uuid>commA</uuid>
+ <short-name>abc/a/com</short-name>
+ <author></author>
+ <date>Thu, 01 Jan 1970 00:03:00 +0000</date>
+ <content-type>text/plain</content-type>
+ <body>comment A</body>
+ </comment>
+ </bug>
+ <bug>
+ <uuid>b</uuid>
+ <short-name>abc/b</short-name>
+ <severity>minor</severity>
+ <status>closed</status>
+ <creator>Jane Doe <jdoe@example.com></creator>
+ <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+ <summary>Bug B</summary>
+ </bug>
+ </bugdir>
+ >>> bug.load_severities(bug.severity_def)
+ >>> bug.load_status(
+ ... active_status_def=bug.active_status_def,
+ ... inactive_status_def=bug.inactive_status_def)
+ >>> bugdirA.cleanup()
+ """
+ info = [('uuid', self.uuid),
+ ('short-name', self.id.user()),
+ ('target', self.target),
+ ('severities', self.severities),
+ ('active-status', self.active_status),
+ ('inactive-status', self.inactive_status),
+ ]
+ lines = ['<bugdir>']
+ for (k,v) in info:
+ if v is not None:
+ if k in ['severities', 'active-status', 'inactive-status']:
+ lines.append(' <{}>'.format(k))
+ for vk,vv in v:
+ lines.extend([
+ ' <entry>',
+ ' <key>{}</key>'.format(
+ xml.sax.saxutils.escape(vk)),
+ ' <value>{}</value>'.format(
+ xml.sax.saxutils.escape(vv)),
+ ' </entry>',
+ ])
+ lines.append(' </{}>'.format(k))
+ else:
+ v = xml.sax.saxutils.escape(v)
+ lines.append(' <{0}>{1}</{0}>'.format(k, v))
+ for estr in self.extra_strings:
+ lines.append(' <extra-string>{}</extra-string>'.format(estr))
+ if show_bugs:
+ for bug in self:
+ bug_xml = bug.xml(indent=indent+2, show_comments=show_comments)
+ if bug_xml:
+ bug_xml = bug_xml[indent:] # strip leading indent spaces
+ lines.append(bug_xml)
+ lines.append('</bugdir>')
+ istring = ' '*indent
+ sep = '\n' + istring
+ return istring + sep.join(lines).rstrip('\n')
+
+ def from_xml(self, xml_string, preserve_uuids=False):
+ """
+ Note: If a bugdir uuid is given, set .alt_id to it's value.
+ >>> bug.load_severities(bug.severity_def)
+ >>> bug.load_status(
+ ... active_status_def=bug.active_status_def,
+ ... inactive_status_def=bug.inactive_status_def)
+ >>> bugdirA = SimpleBugDir(memory=True)
+ >>> bugdirA.severities = (('minor', 'The standard bug level.'),)
+ >>> bugdirA.inactive_status = (
+ ... ('closed', 'The bug is no longer relevant.'),)
+ >>> bugA = bugdirA.bug_from_uuid('a')
+ >>> commA = bugA.comment_root.new_reply(body='comment A')
+ >>> commA.uuid = 'commA'
+ >>> xml = bugdirA.xml(show_bugs=True, show_comments=True)
+ >>> bugdirB = BugDir(storage=None)
+ >>> bugdirB.from_xml(xml)
+ >>> bugdirB.xml(show_bugs=True, show_comments=True) == xml
+ False
+ >>> bugdirB.uuid = bugdirB.alt_id
+ >>> for bug_ in bugdirB:
+ ... bug_.uuid = bug_.alt_id
+ ... bug_.alt_id = None
+ ... for comm in bug_.comments():
+ ... comm.uuid = comm.alt_id
+ ... comm.alt_id = None
+ >>> bugdirB.xml(show_bugs=True, show_comments=True) == xml
+ True
+ >>> bugdirB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE
+ ['severities', 'inactive_status']
+ >>> bugdirC = BugDir(storage=None)
+ >>> bugdirC.from_xml(xml, preserve_uuids=True)
+ >>> bugdirC.uuid == bugdirA.uuid
+ True
+ >>> bugdirC.xml(show_bugs=True, show_comments=True) == xml
+ True
+ >>> bug.load_severities(bug.severity_def)
+ >>> bug.load_status(
+ ... active_status_def=bug.active_status_def,
+ ... inactive_status_def=bug.inactive_status_def)
+ >>> bugdirA.cleanup()
+ """
+ if type(xml_string) == types.UnicodeType:
+ xml_string = xml_string.strip().encode('unicode_escape')
+ if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
+ bugdir = xml_string
+ else:
+ bugdir = ElementTree.XML(xml_string)
+ if bugdir.tag != 'bugdir':
+ raise utility.InvalidXML(
+ 'bugdir', bugdir, 'root element must be <bugdir>')
+ tags = ['uuid', 'short-name', 'target', 'severities', 'active-status',
+ 'inactive-status', 'extra-string']
+ self.explicit_attrs = []
+ uuid = None
+ estrs = []
+ for child in bugdir.getchildren():
+ if child.tag == 'short-name':
+ pass
+ elif child.tag == 'bug':
+ bg = bug.Bug(bugdir=self)
+ bg.from_xml(child, preserve_uuids=preserve_uuids)
+ self.append(bg, update=True)
+ continue
+ elif child.tag in tags:
+ if child.text == None or len(child.text) == 0:
+ text = settings_object.EMPTY
+ elif child.tag in ['severities', 'active-status',
+ 'inactive-status']:
+ entries = []
+ for entry in child.getchildren():
+ if entry.tag != 'entry':
+ raise utility.InvalidXML(
+ '{} child element {} must be <entry>'.format(
+ child.tag, entry))
+ key = value = None
+ for kv in entry.getchildren():
+ if kv.tag == 'key':
+ if key is not None:
+ raise utility.InvalidXML(
+ ('duplicate keys ({} and {}) in {}'
+ ).format(key, kv.text, child.tag))
+ key = xml.sax.saxutils.unescape(kv.text)
+ elif kv.tag == 'value':
+ if value is not None:
+ raise utility.InvalidXML(
+ ('duplicate values ({} and {}) in {}'
+ ).format(
+ value, kv.text, child.tag))
+ value = xml.sax.saxutils.unescape(kv.text)
+ else:
+ raise utility.InvalidXML(
+ ('{} child element {} must be <key> or '
+ '<value>').format(child.tag, kv))
+ if key is None:
+ raise utility.InvalidXML(
+ 'no key for {}'.format(child.tag))
+ if value is None:
+ raise utility.InvalidXML(
+ 'no key for {}'.format(child.tag))
+ entries.append((key, value))
+ text = entries
+ else:
+ text = xml.sax.saxutils.unescape(child.text)
+ if not isinstance(text, unicode):
+ text = text.decode('unicode_escape')
+ text = text.strip()
+ if child.tag == 'uuid' and not preserve_uuids:
+ uuid = text
+ continue # don't set the bug's uuid tag.
+ elif child.tag == 'extra-string':
+ estrs.append(text)
+ continue # don't set the bug's extra_string yet.
+ attr_name = child.tag.replace('-','_')
+ self.explicit_attrs.append(attr_name)
+ setattr(self, attr_name, text)
+ else:
+ libbe.LOG.warning(
+ 'ignoring unknown tag {0} in {1}'.format(
+ child.tag, bugdir.tag))
+ if uuid != self.uuid:
+ if not hasattr(self, 'alt_id') or self.alt_id == None:
+ self.alt_id = uuid
+ self.extra_strings = estrs
+
+ def merge(self, other, accept_changes=True,
+ accept_extra_strings=True, accept_bugs=True,
+ accept_comments=True, change_exception=False):
+ """Merge info from other into this bugdir.
+
+ Overrides any attributes in self that are listed in
+ other.explicit_attrs.
+
+ >>> bugdirA = SimpleBugDir()
+ >>> bugdirA.extra_strings += ['TAG: favorite']
+ >>> bugdirB = SimpleBugDir()
+ >>> bugdirB.explicit_attrs = ['target']
+ >>> bugdirB.target = '1234'
+ >>> bugdirB.extra_strings += ['TAG: very helpful']
+ >>> bugdirB.extra_strings += ['TAG: useful']
+ >>> bugA = bugdirB.bug_from_uuid('a')
+ >>> commA = bugA.comment_root.new_reply(body='comment A')
+ >>> commA.uuid = 'uuid-commA'
+ >>> commA.date = 'Thu, 01 Jan 1970 00:01:00 +0000'
+ >>> bugC = bugdirB.new_bug(summary='bug C', _uuid='c')
+ >>> bugC.alt_id = 'alt-c'
+ >>> bugC.time_string = 'Thu, 01 Jan 1970 00:02:00 +0000'
+ >>> bugdirA.merge(
+ ... bugdirB, accept_changes=False, accept_extra_strings=False,
+ ... accept_bugs=False, change_exception=False)
+ >>> print(bugdirA.target)
+ None
+ >>> bugdirA.merge(
+ ... bugdirB, accept_changes=False, accept_extra_strings=False,
+ ... accept_bugs=False, change_exception=True)
+ Traceback (most recent call last):
+ ...
+ ValueError: Merge would change target "None"->"1234" for bugdir abc123
+ >>> print(bugdirA.target)
+ None
+ >>> bugdirA.merge(
+ ... bugdirB, accept_changes=True, accept_extra_strings=False,
+ ... accept_bugs=False, change_exception=True)
+ Traceback (most recent call last):
+ ...
+ ValueError: Merge would add extra string "TAG: useful" for bugdir abc123
+ >>> print(bugdirA.target)
+ 1234
+ >>> print(bugdirA.extra_strings)
+ ['TAG: favorite']
+ >>> bugdirA.merge(
+ ... bugdirB, accept_changes=True, accept_extra_strings=True,
+ ... accept_bugs=False, change_exception=True)
+ Traceback (most recent call last):
+ ...
+ ValueError: Merge would add bug c (alt: alt-c) to bugdir abc123
+ >>> print(bugdirA.extra_strings)
+ ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
+ >>> bugdirA.merge(
+ ... bugdirB, accept_changes=True, accept_extra_strings=True,
+ ... accept_bugs=True, change_exception=True)
+ >>> print(bugdirA.xml(show_bugs=True, show_comments=True))
+ ... # doctest: +ELLIPSIS, +REPORT_UDIFF
+ <bugdir>
+ <uuid>abc123</uuid>
+ <short-name>abc</short-name>
+ <target>1234</target>
+ <extra-string>TAG: favorite</extra-string>
+ <extra-string>TAG: useful</extra-string>
+ <extra-string>TAG: very helpful</extra-string>
+ <bug>
+ <uuid>a</uuid>
+ <short-name>abc/a</short-name>
+ <severity>minor</severity>
+ <status>open</status>
+ <creator>John Doe <jdoe@example.com></creator>
+ <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+ <summary>Bug A</summary>
+ <comment>
+ <uuid>uuid-commA</uuid>
+ <short-name>abc/a/uui</short-name>
+ <author></author>
+ <date>Thu, 01 Jan 1970 00:01:00 +0000</date>
+ <content-type>text/plain</content-type>
+ <body>comment A</body>
+ </comment>
+ </bug>
+ <bug>
+ <uuid>b</uuid>
+ <short-name>abc/b</short-name>
+ <severity>minor</severity>
+ <status>closed</status>
+ <creator>Jane Doe <jdoe@example.com></creator>
+ <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
+ <summary>Bug B</summary>
+ </bug>
+ <bug>
+ <uuid>c</uuid>
+ <short-name>abc/c</short-name>
+ <severity>minor</severity>
+ <status>open</status>
+ <created>Thu, 01 Jan 1970 00:02:00 +0000</created>
+ <summary>bug C</summary>
+ </bug>
+ </bugdir>
+ >>> bugdirA.cleanup()
+ >>> bugdirB.cleanup()
+ """
+ if hasattr(other, 'explicit_attrs'):
+ for attr in other.explicit_attrs:
+ old = getattr(self, attr)
+ new = getattr(other, attr)
+ if old != new:
+ if accept_changes:
+ setattr(self, attr, new)
+ elif change_exception:
+ raise ValueError(
+ ('Merge would change {} "{}"->"{}" for bugdir {}'
+ ).format(attr, old, new, self.uuid))
+ for estr in other.extra_strings:
+ if not estr in self.extra_strings:
+ if accept_extra_strings:
+ self.extra_strings += [estr]
+ elif change_exception:
+ raise ValueError(
+ ('Merge would add extra string "{}" for bugdir {}'
+ ).format(estr, self.uuid))
+ for o_bug in other:
+ try:
+ s_bug = self.bug_from_uuid(o_bug.uuid)
+ except KeyError as e:
+ try:
+ s_bug = self.bug_from_uuid(o_bug.alt_id)
+ except KeyError as e:
+ s_bug = None
+ if s_bug is None:
+ if accept_bugs:
+ o_bug_copy = copy.copy(o_bug)
+ o_bug_copy.bugdir = self
+ o_bug_copy.id = libbe.util.id.ID(o_bug_copy, 'bug')
+ self.append(o_bug_copy)
+ elif change_exception:
+ raise ValueError(
+ ('Merge would add bug {} (alt: {}) to bugdir {}'
+ ).format(o_bug.uuid, o_bug.alt_id, self.uuid))
+ else:
+ s_bug.merge(o_bug, accept_changes=accept_changes,
+ accept_extra_strings=accept_extra_strings,
+ change_exception=change_exception)
+
# methods for id generation
def sibling_uuids(self):