Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/src/sugar/bundle
diff options
context:
space:
mode:
authorMarco Pesenti Gritti <marco@localhost.localdomain>2008-04-29 12:58:34 (GMT)
committer Marco Pesenti Gritti <marco@localhost.localdomain>2008-04-29 12:58:34 (GMT)
commit72c2a1d770aac5af9df899cbfd25a15cbc2956ea (patch)
tree8909999bda47a3dfe9abc7befcd38e5278d6716f /src/sugar/bundle
parent6efdf259b62d7af47b1e06601eaee17b74dbeb58 (diff)
Use a src directory consistently with base and shell.
Diffstat (limited to 'src/sugar/bundle')
-rw-r--r--src/sugar/bundle/Makefile.am6
-rw-r--r--src/sugar/bundle/__init__.py16
-rw-r--r--src/sugar/bundle/activitybundle.py328
-rw-r--r--src/sugar/bundle/bundle.py157
-rw-r--r--src/sugar/bundle/contentbundle.py199
5 files changed, 706 insertions, 0 deletions
diff --git a/src/sugar/bundle/Makefile.am b/src/sugar/bundle/Makefile.am
new file mode 100644
index 0000000..f1af791
--- /dev/null
+++ b/src/sugar/bundle/Makefile.am
@@ -0,0 +1,6 @@
+sugardir = $(pythondir)/sugar/bundle
+sugar_PYTHON = \
+ __init__.py \
+ bundle.py \
+ activitybundle.py \
+ contentbundle.py
diff --git a/src/sugar/bundle/__init__.py b/src/sugar/bundle/__init__.py
new file mode 100644
index 0000000..85ebced
--- /dev/null
+++ b/src/sugar/bundle/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (C) 2006-2007, Red Hat, Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
diff --git a/src/sugar/bundle/activitybundle.py b/src/sugar/bundle/activitybundle.py
new file mode 100644
index 0000000..5f1fb7b
--- /dev/null
+++ b/src/sugar/bundle/activitybundle.py
@@ -0,0 +1,328 @@
+# Copyright (C) 2007, Red Hat, Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""Sugar activity bundles"""
+
+from ConfigParser import ConfigParser
+import locale
+import os
+import tempfile
+
+from sugar.bundle.bundle import Bundle, MalformedBundleException, \
+ AlreadyInstalledException, RegistrationException, \
+ NotInstalledException
+
+from sugar import activity
+from sugar import env
+
+import logging
+
+class ActivityBundle(Bundle):
+ """A Sugar activity bundle
+
+ See http://wiki.laptop.org/go/Activity_bundles for details
+ """
+
+ MIME_TYPE = 'application/vnd.olpc-sugar'
+ DEPRECATED_MIME_TYPE = 'application/vnd.olpc-x-sugar'
+
+ _zipped_extension = '.xo'
+ _unzipped_extension = '.activity'
+ _infodir = 'activity'
+
+ def __init__(self, path):
+ Bundle.__init__(self, path)
+ self.activity_class = None
+ self.bundle_exec = None
+
+ self._name = None
+ self._icon = None
+ self._bundle_id = None
+ self._mime_types = None
+ self._show_launcher = True
+ self._activity_version = 0
+
+ info_file = self._get_file('activity/activity.info')
+ if info_file is None:
+ raise MalformedBundleException('No activity.info file')
+ self._parse_info(info_file)
+
+ linfo_file = self._get_linfo_file()
+ if linfo_file:
+ self._parse_linfo(linfo_file)
+
+ def _parse_info(self, info_file):
+ cp = ConfigParser()
+ cp.readfp(info_file)
+
+ section = 'Activity'
+
+ if cp.has_option(section, 'bundle_id'):
+ self._bundle_id = cp.get(section, 'bundle_id')
+ # FIXME deprecated
+ elif cp.has_option(section, 'service_name'):
+ self._bundle_id = cp.get(section, 'service_name')
+ else:
+ raise MalformedBundleException(
+ 'Activity bundle %s does not specify a bundle id' %
+ self._path)
+
+ if cp.has_option(section, 'name'):
+ self._name = cp.get(section, 'name')
+ else:
+ raise MalformedBundleException(
+ 'Activity bundle %s does not specify a name' % self._path)
+
+ # FIXME class is deprecated
+ if cp.has_option(section, 'class'):
+ self.activity_class = cp.get(section, 'class')
+ elif cp.has_option(section, 'exec'):
+ self.bundle_exec = cp.get(section, 'exec')
+ else:
+ raise MalformedBundleException(
+ 'Activity bundle %s must specify either class or exec' %
+ self._path)
+
+ if cp.has_option(section, 'mime_types'):
+ mime_list = cp.get(section, 'mime_types').strip(';')
+ self._mime_types = [ mime.strip() for mime in mime_list.split(';') ]
+
+ if cp.has_option(section, 'show_launcher'):
+ if cp.get(section, 'show_launcher') == 'no':
+ self._show_launcher = False
+
+ if cp.has_option(section, 'icon'):
+ self._icon = cp.get(section, 'icon')
+
+ if cp.has_option(section, 'activity_version'):
+ version = cp.get(section, 'activity_version')
+ try:
+ self._activity_version = int(version)
+ except ValueError:
+ raise MalformedBundleException(
+ 'Activity bundle %s has invalid version number %s' %
+ (self._path, version))
+
+ def _get_linfo_file(self):
+ lang = locale.getdefaultlocale()[0]
+ if not lang:
+ return None
+
+ linfo_path = os.path.join('locale', lang, 'activity.linfo')
+ linfo_file = self._get_file(linfo_path)
+ if linfo_file is not None:
+ return linfo_file
+
+ linfo_path = os.path.join('locale', lang[:2], 'activity.linfo')
+ linfo_file = self._get_file(linfo_path)
+ if linfo_file is not None:
+ return linfo_file
+
+ return None
+
+ def _parse_linfo(self, linfo_file):
+ cp = ConfigParser()
+ cp.readfp(linfo_file)
+
+ section = 'Activity'
+
+ if cp.has_option(section, 'name'):
+ self._name = cp.get(section, 'name')
+
+ def get_locale_path(self):
+ """Get the locale path inside the (installed) activity bundle."""
+ if not self._unpacked:
+ raise NotInstalledException
+ return os.path.join(self._path, 'locale')
+
+ def get_icons_path(self):
+ """Get the icons path inside the (installed) activity bundle."""
+ if not self._unpacked:
+ raise NotInstalledException
+ return os.path.join(self._path, 'icons')
+
+ def get_path(self):
+ """Get the activity bundle path."""
+ return self._path
+
+ def get_name(self):
+ """Get the activity user visible name."""
+ return self._name
+
+ def get_bundle_id(self):
+ """Get the activity bundle id"""
+ return self._bundle_id
+
+ # FIXME: this should return the icon data, not a filename, so that
+ # we don't need to create a temp file in the zip case
+ def get_icon(self):
+ """Get the activity icon name"""
+ icon_path = os.path.join('activity', self._icon + '.svg')
+ if self._unpacked:
+ return os.path.join(self._path, icon_path)
+ else:
+ icon_data = self._get_file(icon_path).read()
+ temp_file, temp_file_path = tempfile.mkstemp(self._icon)
+ os.write(temp_file, icon_data)
+ os.close(temp_file)
+ return temp_file_path
+
+ def get_activity_version(self):
+ """Get the activity version"""
+ return self._activity_version
+
+ def get_command(self):
+ """Get the command to execute to launch the activity factory"""
+ if self.bundle_exec:
+ command = os.path.expandvars(self.bundle_exec)
+ else:
+ command = 'sugar-activity ' + self.activity_class
+
+ return command
+
+
+ def get_mime_types(self):
+ """Get the MIME types supported by the activity"""
+ return self._mime_types
+
+ def get_show_launcher(self):
+ """Get whether there should be a visible launcher for the activity"""
+ return self._show_launcher
+
+ def is_installed(self):
+ if activity.get_registry().get_activity(self._bundle_id):
+ return True
+ else:
+ return False
+
+ def need_upgrade(self):
+ act = activity.get_registry().get_activity(self._bundle_id)
+ if act is None or act.version != self._activity_version:
+ return True
+ else:
+ return False
+
+ def install(self):
+ activities_path = env.get_user_activities_path()
+ act = activity.get_registry().get_activity(self._bundle_id)
+ if act is not None and act.path.startswith(activities_path):
+ raise AlreadyInstalledException
+
+ install_dir = env.get_user_activities_path()
+ self._unzip(install_dir)
+
+ install_path = os.path.join(install_dir, self._zip_root_dir)
+
+ xdg_data_home = os.getenv('XDG_DATA_HOME',
+ os.path.expanduser('~/.local/share'))
+
+ mime_path = os.path.join(install_path, 'activity', 'mimetypes.xml')
+ if os.path.isfile(mime_path):
+ mime_dir = os.path.join(xdg_data_home, 'mime')
+ mime_pkg_dir = os.path.join(mime_dir, 'packages')
+ if not os.path.isdir(mime_pkg_dir):
+ os.makedirs(mime_pkg_dir)
+ installed_mime_path = os.path.join(mime_pkg_dir,
+ '%s.xml' % self._bundle_id)
+ os.symlink(mime_path, installed_mime_path)
+ os.spawnlp(os.P_WAIT, 'update-mime-database',
+ 'update-mime-database', mime_dir)
+
+ mime_types = self.get_mime_types()
+ if mime_types is not None:
+ installed_icons_dir = os.path.join(xdg_data_home,
+ 'icons/sugar/scalable/mimetypes')
+ if not os.path.isdir(installed_icons_dir):
+ os.makedirs(installed_icons_dir)
+
+ for mime_type in mime_types:
+ mime_icon_base = os.path.join(install_path, 'activity',
+ mime_type.replace('/', '-'))
+ svg_file = mime_icon_base + '.svg'
+ info_file = mime_icon_base + '.icon'
+ if os.path.isfile(svg_file):
+ os.symlink(svg_file,
+ os.path.join(installed_icons_dir,
+ os.path.basename(svg_file)))
+ if os.path.isfile(info_file):
+ os.symlink(info_file,
+ os.path.join(installed_icons_dir,
+ os.path.basename(info_file)))
+
+ if not activity.get_registry().add_bundle(install_path):
+ raise RegistrationException
+
+ def uninstall(self, force=False):
+ if self._unpacked:
+ install_path = self._path
+ else:
+ if not self.is_installed():
+ raise NotInstalledException
+
+ act = activity.get_registry().get_activity(self._bundle_id)
+ if not force and act.version != self._activity_version:
+ logging.warning('Not uninstalling, different bundle present')
+ return
+ elif not act.path.startswith(env.get_user_activities_path()):
+ logging.warning('Not uninstalling system activity')
+ return
+
+ install_path = os.path.join(env.get_user_activities_path(),
+ self._zip_root_dir)
+
+ xdg_data_home = os.getenv('XDG_DATA_HOME',
+ os.path.expanduser('~/.local/share'))
+
+ mime_dir = os.path.join(xdg_data_home, 'mime')
+ installed_mime_path = os.path.join(mime_dir, 'packages',
+ '%s.xml' % self._bundle_id)
+ if os.path.exists(installed_mime_path):
+ os.remove(installed_mime_path)
+ os.spawnlp(os.P_WAIT, 'update-mime-database',
+ 'update-mime-database', mime_dir)
+
+ mime_types = self.get_mime_types()
+ if mime_types is not None:
+ installed_icons_dir = os.path.join(xdg_data_home,
+ 'icons/sugar/scalable/mimetypes')
+ for f in os.listdir(installed_icons_dir):
+ path = os.path.join(installed_icons_dir, f)
+ if os.path.islink(path) and \
+ os.readlink(path).startswith(install_path):
+ os.remove(path)
+
+ self._uninstall(install_path)
+
+ if not activity.get_registry().remove_bundle(install_path):
+ raise RegistrationException
+
+ def upgrade(self):
+ act = activity.get_registry().get_activity(self._bundle_id)
+ if act is None:
+ logging.warning('Activity not installed')
+ elif act.path.startswith(env.get_user_activities_path()):
+ try:
+ self.uninstall(force=True)
+ except Exception, e:
+ logging.warning('Uninstall failed (%s), still trying ' \
+ 'to install newer bundle', e)
+ else:
+ logging.warning('Unable to uninstall system activity, ' \
+ 'installing upgraded version in user activities')
+
+ self.install()
+
diff --git a/src/sugar/bundle/bundle.py b/src/sugar/bundle/bundle.py
new file mode 100644
index 0000000..47d08b2
--- /dev/null
+++ b/src/sugar/bundle/bundle.py
@@ -0,0 +1,157 @@
+# Copyright (C) 2007, Red Hat, Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""Sugar bundle file handler"""
+
+import os
+import logging
+import StringIO
+import zipfile
+
+class AlreadyInstalledException(Exception):
+ pass
+
+class NotInstalledException(Exception):
+ pass
+
+class InvalidPathException(Exception):
+ pass
+
+class ZipExtractException(Exception):
+ pass
+
+class RegistrationException(Exception):
+ pass
+
+class MalformedBundleException(Exception):
+ pass
+
+class Bundle:
+ """A Sugar activity, content module, etc.
+
+ The bundle itself may be either a zip file or a directory
+ hierarchy, with metadata about the bundle stored various files
+ inside it.
+
+ This is an abstract base class. See ActivityBundle and
+ ContentBundle for more details on those bundle types.
+ """
+
+ _zipped_extension = None
+ _unzipped_extension = None
+
+ def __init__(self, path):
+ self._path = path
+ self._zip_root_dir = None
+
+ if os.path.isdir(self._path):
+ self._unpacked = True
+ else:
+ self._unpacked = False
+ self._check_zip_bundle()
+
+ # manifest = self._get_file(self._infodir + '/contents')
+ # if manifest is None:
+ # raise MalformedBundleException('No manifest file')
+ #
+ # signature = self._get_file(self._infodir + '/contents.sig')
+ # if signature is None:
+ # raise MalformedBundleException('No signature file')
+
+ def _check_zip_bundle(self):
+ zip_file = zipfile.ZipFile(self._path)
+ file_names = zip_file.namelist()
+ if len(file_names) == 0:
+ raise MalformedBundleException('Empty zip file')
+
+ if file_names[0] == 'mimetype':
+ del file_names[0]
+
+ self._zip_root_dir = file_names[0].split('/')[0]
+ if self._unzipped_extension is not None:
+ (name_, ext) = os.path.splitext(self._zip_root_dir)
+ if ext != self._unzipped_extension:
+ raise MalformedBundleException(
+ 'All files in the bundle must be inside a single ' +
+ 'directory whose name ends with "%s"' %
+ self._unzipped_extension)
+
+ for file_name in file_names:
+ if not file_name.startswith(self._zip_root_dir):
+ raise MalformedBundleException(
+ 'All files in the bundle must be inside a single ' +
+ 'top-level directory')
+
+ def _get_file(self, filename):
+ f = None
+
+ if self._unpacked:
+ path = os.path.join(self._path, filename)
+ if os.path.isfile(path):
+ f = open(path)
+ else:
+ zip_file = zipfile.ZipFile(self._path)
+ path = os.path.join(self._zip_root_dir, filename)
+ try:
+ data = zip_file.read(path)
+ f = StringIO.StringIO(data)
+ except KeyError:
+ logging.debug('%s not found.' % filename)
+ zip_file.close()
+
+ return f
+
+ def get_path(self):
+ """Get the bundle path."""
+ return self._path
+
+ def _unzip(self, install_dir):
+ if self._unpacked:
+ raise AlreadyInstalledException
+
+ if not os.path.isdir(install_dir):
+ os.mkdir(install_dir, 0775)
+
+ # zipfile provides API that in theory would let us do this
+ # correctly by hand, but handling all the oddities of
+ # Windows/UNIX mappings, extension attributes, deprecated
+ # features, etc makes it impractical.
+ # FIXME: use manifest
+ if os.spawnlp(os.P_WAIT, 'unzip', 'unzip', '-o', self._path,
+ '-x', 'mimetype', '-d', install_dir):
+ raise ZipExtractException
+
+ def _zip(self, bundle_path):
+ if not self._unpacked:
+ raise NotInstalledException
+
+ raise NotImplementedError
+
+ def _uninstall(self, install_path):
+ if not os.path.isdir(install_path):
+ raise InvalidPathException
+ if self._unzipped_extension is not None:
+ (name_, ext) = os.path.splitext(install_path)
+ if ext != self._unzipped_extension:
+ raise InvalidPathException
+
+ for root, dirs, files in os.walk(install_path, topdown=False):
+ for name in files:
+ os.remove(os.path.join(root, name))
+ for name in dirs:
+ os.rmdir(os.path.join(root, name))
+ os.rmdir(install_path)
diff --git a/src/sugar/bundle/contentbundle.py b/src/sugar/bundle/contentbundle.py
new file mode 100644
index 0000000..effab23
--- /dev/null
+++ b/src/sugar/bundle/contentbundle.py
@@ -0,0 +1,199 @@
+# Copyright (C) 2007, Red Hat, Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+# Boston, MA 02111-1307, USA.
+
+"""Sugar content bundles"""
+
+from ConfigParser import ConfigParser
+import os
+
+from sugar import env
+from sugar.bundle.bundle import Bundle, NotInstalledException, \
+ MalformedBundleException
+
+class ContentBundle(Bundle):
+ """A Sugar content bundle
+
+ See http://wiki.laptop.org/go/Content_bundles for details
+ """
+
+ MIME_TYPE = 'application/vnd.olpc-content'
+
+ _zipped_extension = '.xol'
+ _unzipped_extension = None
+ _infodir = 'library'
+
+ def __init__(self, path):
+ Bundle.__init__(self, path)
+
+ self._locale = None
+ self._l10n = None
+ self._category = None
+ self._name = None
+ self._subcategory = None
+ self._category_class = None
+ self._category_icon = None
+ self._library_version = None
+ self._bundle_class = None
+
+ info_file = self._get_file('library/library.info')
+ if info_file is None:
+ raise MalformedBundleException('No library.info file')
+ self._parse_info(info_file)
+
+ if (self._get_file('index.html') is None and
+ self._get_file('library/library.xml') is None):
+ raise MalformedBundleException(
+ 'Content bundle %s has neither index.html nor library.xml' %
+ self._path)
+
+ def _parse_info(self, info_file):
+ cp = ConfigParser()
+ cp.readfp(info_file)
+
+ section = 'Library'
+
+ if cp.has_option(section, 'host_version'):
+ version = cp.get(section, 'host_version')
+ try:
+ if int(version) != 1:
+ raise MalformedBundleException(
+ 'Content bundle %s has unknown host_version number %s' %
+ (self._path, version))
+ except ValueError:
+ raise MalformedBundleException(
+ 'Content bundle %s has invalid host_version number %s' %
+ (self._path, version))
+
+ if cp.has_option(section, 'name'):
+ self._name = cp.get(section, 'name')
+ else:
+ raise MalformedBundleException(
+ 'Content bundle %s does not specify a name' % self._path)
+
+ if cp.has_option(section, 'library_version'):
+ version = cp.get(section, 'library_version')
+ try:
+ self._library_version = int(version)
+ except ValueError:
+ raise MalformedBundleException(
+ 'Content bundle %s has invalid version number %s' %
+ (self._path, version))
+
+ if cp.has_option(section, 'l10n'):
+ l10n = cp.get(section, 'l10n')
+ if l10n == 'true':
+ self._l10n = True
+ elif l10n == 'false':
+ self._l10n = False
+ else:
+ raise MalformedBundleException(
+ 'Content bundle %s has invalid l10n key "%s"' %
+ (self._path, l10n))
+ else:
+ raise MalformedBundleException(
+ 'Content bundle %s does not specify if it is localized' %
+ self._path)
+
+ if cp.has_option(section, 'locale'):
+ self._locale = cp.get(section, 'locale')
+ else:
+ raise MalformedBundleException(
+ 'Content bundle %s does not specify a locale' % self._path)
+
+ if cp.has_option(section, 'category'):
+ self._category = cp.get(section, 'category')
+ else:
+ raise MalformedBundleException(
+ 'Content bundle %s does not specify a category' % self._path)
+
+ if cp.has_option(section, 'category_icon'):
+ self._category_icon = cp.get(section, 'category_icon')
+ else:
+ self._category_icon = None
+
+ if cp.has_option(section, 'category_class'):
+ self._category_class = cp.get(section, 'category_class')
+ else:
+ self._category_class = None
+
+ if cp.has_option(section, 'subcategory'):
+ self._subcategory = cp.get(section, 'subcategory')
+ else:
+ self._subcategory = None
+
+ if cp.has_option(section, 'bundle_class'):
+ self._bundle_class = cp.get(section, 'bundle_class')
+ else:
+ self._bundle_class = None
+
+ def get_name(self):
+ return self._name
+
+ def get_library_version(self):
+ return self._library_version
+
+ def get_l10n(self):
+ return self._l10n
+
+ def get_locale(self):
+ return self._locale
+
+ def get_category(self):
+ return self._category
+
+ def get_category_icon(self):
+ return self._category_icon
+
+ def get_category_class(self):
+ return self._category_class
+
+ def get_subcategory(self):
+ return self._subcategory
+
+ def get_bundle_class(self):
+ return self._bundle_class
+
+ def _run_indexer(self):
+ if os.environ.has_key('XDG_DATA_DIRS'):
+ for path in os.environ['XDG_DATA_DIRS'].split(':'):
+ indexer = os.path.join(path, 'library-common', 'make_index.py')
+ if os.path.exists(indexer):
+ os.spawnlp(os.P_WAIT, 'python', 'python', indexer)
+
+ def is_installed(self):
+ if self._unpacked:
+ return True
+ elif os.path.isdir(os.path.join(env.get_user_library_path(),
+ self._zip_root_dir)):
+ return True
+ else:
+ return False
+
+ def install(self):
+ self._unzip(env.get_user_library_path())
+ self._run_indexer()
+
+ def uninstall(self):
+ if self._unpacked:
+ if not self.is_installed():
+ raise NotInstalledException
+ install_dir = self._path
+ else:
+ install_dir = os.path.join(env.get_user_library_path(),
+ self._zip_root_dir)
+ self._uninstall(install_dir)
+ self._run_indexer()