# -*- coding: utf-8 -*-
### Copyright (C) 2008-2012 Antonio Valentino <a_valentino@users.sf.net>
### This file is part of GSDView.
### GSDView 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.
### GSDView 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 GSDView; if not, write to the Free Software
### Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
'''Plugin manager.'''
import os
import sys
import pkgutil
import logging
from distutils.versionpredicate import VersionPredicate
try:
import pkg_resources
except ImportError:
logging.getLogger(__name__).debug('"pkg_resources" not found.')
__author__ = 'Antonio Valentino <a_valentino@users.sf.net>'
__date__ = '$Date: 2009-05-31 10:28:43 +0200 (dom, 31 mag 2009) $'
__revision__ = '$Revision: 430 $'
[docs]class PluginManager(object):
def __init__(self, app, syspath=None):
super(PluginManager, self).__init__()
self.paths = []
self.syspath = syspath
self.plugins = {}
self.autoload = []
self._app = app
def _get_allplugins(self):
plugins = set(self.plugins.keys())
plugins.update(self._scanpaths())
return sorted(plugins)
allplugins = property(_get_allplugins, doc='List of all availabe plugins.')
def _scanpaths(self):
if not self.paths:
return []
plugins = []
for loader, name, ispkg in pkgutil.iter_modules(self.paths):
if name.startswith('_'):
continue
plugins.append(name)
for path in self.paths:
try:
for egg in pkg_resources.find_distributions(path):
if egg.egg_name().startswith('_'):
continue
plugins.append(egg.key)
except NameError:
pass
return plugins
def _check_dependency(self, depstring):
# @TODO: use pkg_resources' parse_requirements and Requirement
# if available:
# for r in parse_requirements('gsdview >= 0.5'):
# return r.key in self.plugins and avail_ver in r
depstring = depstring.strip()
if not depstring:
return True
modules = dict(self.plugins)
# @TODO: use a cleaner way to provide extra modules for check
#import gsdview
#modules['gsdview'] = gsdview
try:
vp = VersionPredicate(depstring)
except ValueError as e:
# @TODO: remove dependency from self._app
self._app.logger.error('invalid version preficate "%s": %s' % (
depstring, e))
return False
if vp.name in modules:
try:
return vp.satisfied_by(modules[vp.name].version)
except ValueError as e:
logging.warning(str(e)) # , exc_info=True)
return False
else:
return False
def _check_deps(self, module):
try:
for depstring in module.__requires__:
if not self._check_dependency(depstring):
return False
except Exception:
logger = logging.getLogger('gsdview')
logger.error('error checking dependencies for module: %s' % module)
raise
return True
[docs] def load_module(self, module, name=None):
# @TODO: make the module independent from gsdview
logger = logging.getLogger('gsdview')
if not name:
name = module.__name__
try:
# @TODO: find a more general form to pass arguments to plugins
module.init(self._app)
self.plugins[name] = module
logger.info('"%s" plugin loaded.' % name)
except Exception as e: # AttributeError:
logger.warning('error loading "%s" plugin: %s' % (name, e))
# @WARNING: (pychecker) Parameter (type_) not used
[docs] def load(self, names, paths=None, info_only=False, type_='plugins'):
if paths is None:
paths = self.paths
elif isinstance(paths, basestring):
paths = [paths]
if not paths:
paths = []
if self.syspath:
paths.append(self.syspath)
if names is None:
names = []
elif isinstance(names, basestring):
names = [names]
# @TODO: make the module independent from gsdview
logger = logging.getLogger('gsdview')
delayed = {}
for path in paths:
importer = pkgutil.get_importer(path)
try:
distributions = pkg_resources.find_distributions(path)
distributions = dict((egg.key, egg) for egg in distributions)
except NameError:
distributions = {}
for name in names:
if name in self.plugins:
continue
if name in sys.modules:
module = sys.modules[name]
else:
try:
loader = importer.find_module(name)
if loader:
module = loader.load_module(name)
elif name in distributions:
egg = distributions[name]
egg.activate()
module = __import__(name)
else:
logger.warning('unable to find "%s" plugin' % name)
continue
except ImportError as e:
logger.warning('unable to import "%s" plugin: %s' %
(name, e))
continue
if not info_only:
if not self._check_deps(module):
delayed[name] = module
logging.info('loading of "%s" plugin delayed' %
module.name)
else:
self.load_module(module, name)
# @TODO: delayed loading
if delayed and not info_only:
MAXATTEMPTS = 10
delayed_again = delayed
iter_count = 0
loaded_count = None
while delayed_again and loaded_count != 0:
# check for max number of iterations
if iter_count > MAXATTEMPTS:
logger.warning('max number of attempts reached for '
'delayed plugins loading')
break
else:
iter_count += 1
loaded_count = 0
delayed = delayed_again
delayed_again = {}
for name, module in delayed.iteritems():
if not self._check_deps(module):
delayed_again[name] = module
logging.debug('loading of "%s" plugin delayed '
'again' % name)
else:
self.load_module(module, name)
loaded_count += 1
if len(delayed_again):
logger.info('%d modules not loaded because of unmet '
'dependency' % len(delayed_again))
# @TODO: log more verbose info: per module dependency failure
# @WARNING: (pychecker) Parameter (type_) not used
[docs] def unload(self, names, type_='plugin'):
if isinstance(names, basestring):
names = [names]
for name in names:
module = self.plugins.pop(name)
# @TODO: find a more general form to pass arguments to plugins
module.close(self._app)
[docs] def reset(self):
# the dictionary is modified during the iteration so the iteration
# have to be performed on a concrete list
for name in self.plugins.keys():
plugin = self.plugins.pop(name)
# @TODO: find a more general form to pass arguments to plugins
plugin.close(self._app)
self.paths = []
# @NOTE: this method is Qt specific
# @TODO: move to specialized classes implementations that rely on a
# specific external library
[docs] def save_settings(self, settings):
settings.beginGroup('pluginmanager')
try:
paths = list(self.paths) # @NOTE: copy
if self.syspath in paths:
paths.remove(self.syspath)
settings.setValue('pluginspaths', paths)
autoload = set(self.autoload)
autoload = list(autoload.intersection(self.allplugins))
settings.setValue('autoload_plugins', autoload)
settings.setValue('available_plugins', self.allplugins)
finally:
settings.endGroup()
# @NOTE: this method is Qt specific
# @TODO: move to specialized classes implementations that rely on a
# specific external library
[docs] def load_settings(self, settings):
settings.beginGroup('pluginmanager')
try:
paths = settings.value('pluginspaths')
if paths is not None:
self.paths = paths
if self.syspath and not self.syspath in self.paths:
self.paths.append(self.syspath)
self.autoload = settings.value('autoload_plugins', [])
if self.autoload is None:
self.autoload = []
self.load(self.autoload)
# @TODO: check
# @NOTE: by default loads new plugins
available_plugins = set(self.allplugins)
old_plugins = settings.value('available_plugins', [])
new_plugins = available_plugins.difference(old_plugins)
self.load(list(new_plugins))
# only marks actually loaded plugins
new_plugins.intersection_update(self.plugins)
self.autoload.extend(new_plugins)
finally:
settings.endGroup()
### GUI #######################################################################
# @TODO: move Qt specific implementation elsewhere
import functools
from qt import QtCore, QtGui
# @TODO: check dependency - getuiform, geticon, setViewContextActions
from . import qt4support
PluginManagerGuiBase = qt4support.getuiform('pluginmanager', __name__)
[docs]class PluginManagerGui(QtGui.QWidget, PluginManagerGuiBase):
# @TODO: emit signal for ???
def __init__(self, pluginmanager, parent=None,
flags=QtCore.Qt.WindowFlags(0), **kwargs):
super(PluginManagerGui, self).__init__(parent, flags, **kwargs)
self.setupUi(self)
# Set icons
geticon = qt4support.geticon
self.addButton.setIcon(geticon('add.svg', __name__))
self.removeButton.setIcon(geticon('remove.svg', __name__))
self.editButton.setIcon(geticon('edit.svg', __name__))
self.upButton.setIcon(geticon('go-up.svg', __name__))
self.downButton.setIcon(geticon('go-down.svg', __name__))
# Set plugin manager attribute
self.pluginmanager = pluginmanager
# Context menu
qt4support.setViewContextActions(self.pathListWidget)
qt4support.setViewContextActions(self.pluginsTableWidget)
# @TODO: check edit triggers
#int(self.pathListWidget.editTriggers() &
# self.pathListWidget.DoubleClicked)
self.pathListWidget.itemSelectionChanged.connect(
self.pathSelectionChanged)
self.addButton.clicked.connect(self.addPathItem)
self.removeButton.clicked.connect(self.removeSelectedPathItem)
self.upButton.clicked.connect(self.moveSelectedPathItemsUp)
self.downButton.clicked.connect(self.moveSelectedPathItemsDown)
self.editButton.clicked.connect(self.editSelectedPathItem)
@QtCore.Slot()
[docs] def pathSelectionChanged(self):
enabled = bool(self.pathListWidget.selectedItems())
self.editButton.setEnabled(enabled)
self.removeButton.setEnabled(enabled)
self.upButton.setEnabled(enabled)
self.downButton.setEnabled(enabled)
@QtCore.Slot()
[docs] def addPathItem(self):
# @TODO: don't directly use _app attribute
filedialog = self.pluginmanager._app.filedialog
filedialog.setFileMode(filedialog.Directory)
if(filedialog.exec_()):
dirs = filedialog.selectedFiles()
existingdirs = [str(self.pathListWidget.item(row).text())
for row in range(self.pathListWidget.count())]
for dir_ in dirs:
if dir_ not in existingdirs:
self.pathListWidget.addItem(dir_)
@QtCore.Slot()
[docs] def removeSelectedPathItem(self):
model = self.pathListWidget.model()
for item in self.pathListWidget.selectedItems():
model.removeRow(self.pathListWidget.row(item))
@QtCore.Slot()
[docs] def editSelectedPathItem(self):
items = self.pathListWidget.selectedItems()
if items:
item = items[0]
# @TODO: don't directly use _app attribute
filedialog = self.pluginmanager._app.filedialog
filedialog.setFileMode(filedialog.Directory)
filedialog.selectFile(item.text())
if(filedialog.exec_()):
dirs = filedialog.selectedFiles()
if dirs:
dir_ = dirs[0]
item.setText(dir_)
def _movePathItem(self, item, offset):
if offset == 0:
return
listwidget = self.pathListWidget
row = listwidget.row(item)
if (row + offset) < 0:
offset = -row
elif (row + offset) >= listwidget.count():
offset = listwidget.count() - 1 - row
if offset == 0:
return
selected = item.isSelected()
item = listwidget.takeItem(row)
listwidget.insertItem(row + offset, item)
item.setSelected(selected)
@QtCore.Slot()
[docs] def moveSelectedPathItemsUp(self):
selected = sorted(self.pathListWidget.selectedItems(),
key=self.pathListWidget.row)
if self.pathListWidget.row(selected[0]) == 0:
return
for item in selected:
self._movePathItem(item, -1)
@QtCore.Slot()
[docs] def moveSelectedPathItemsDown(self):
selected = sorted(self.pathListWidget.selectedItems(),
key=self.pathListWidget.row, reverse=True)
if (self.pathListWidget.row(selected[0]) ==
self.pathListWidget.count() - 1):
return
for item in selected:
self._movePathItem(item, 1)
[docs] def update_view(self):
self.pathListWidget.clear()
for item in self.pluginmanager.paths:
self.pathListWidget.addItem(item)
self.pluginmanager.load(self.pluginmanager.allplugins, info_only=True)
tablewidget = self.pluginsTableWidget
tablewidget.clear()
tablewidget.setRowCount(0)
tablewidget.setHorizontalHeaderLabels([self.tr('Name'),
self.tr('Description'),
self.tr('Info'),
self.tr('Active'),
self.tr('Load on startup')])
for plugin in self.pluginmanager.allplugins:
name = plugin
short_description = tablewidget.tr('NOT AVAILABLE')
disabled = False
for dict_ in (self.pluginmanager.plugins, sys.modules):
try:
module = dict_[name]
name = module.name
short_description = module.short_description
break
except AttributeError as e:
msg = str(e)
if (not "'name'" in msg
and not "'short_description'" in msg):
raise
disabled = True
except KeyError:
disabled = True
index = tablewidget.rowCount()
tablewidget.insertRow(index)
# name/description
tablewidget.setItem(index, 0, QtGui.QTableWidgetItem(name))
tablewidget.setItem(index, 1,
QtGui.QTableWidgetItem(short_description))
# info
icon = qt4support.geticon('info.svg', __name__)
w = QtGui.QPushButton(icon, '', tablewidget,
toolTip=self.tr('Show plugin info.'),
clicked=functools.partial(
self.showPluginInfo, index))
#clicked=lambda index=index:
# self.showPluginInfo(index))
tablewidget.setCellWidget(index, 2, w)
# active
checked = bool(plugin in self.pluginmanager.plugins)
w = QtGui.QCheckBox(tablewidget, checked=checked)
tablewidget.setCellWidget(index, 3, w)
# TODO: remove this block when plugins unloading will be
# available
if w.isChecked():
w.setEnabled(False)
# autoload
checked = bool(plugin in self.pluginmanager.autoload)
w = QtGui.QCheckBox(tablewidget, checked=checked,
toolTip=self.tr('Load on startup'))
tablewidget.setCellWidget(index, 4, w)
if disabled:
for col in range(tablewidget.columnCount() - 1):
item = tablewidget.item(index, col)
if item:
item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEnabled)
msg = tablewidget.tr("Plugin don't seems to be "
"compatible with GSDView.")
item.setToolTip(msg)
else:
w = tablewidget.cellWidget(index, col)
w.setEnabled(False)
tablewidget.resizeColumnsToContents()
[docs] def load(self, settings):
self.pluginmanager.load_settings(settings)
self.update_view()
[docs] def update_pluginmanager(self):
paths = []
for row in range(self.pathListWidget.count()):
paths.append(str(self.pathListWidget.item(row).text()))
self.pluginmanager.paths = paths
tablewidget = self.pluginsTableWidget
active = set()
autoload = []
for row in range(tablewidget.rowCount()):
name = str(tablewidget.item(row, 0).text())
if tablewidget.cellWidget(row, 3).isChecked():
active.add(name)
if tablewidget.cellWidget(row, 4).isChecked():
autoload.append(name)
toload = active.difference(self.pluginmanager.plugins)
tounload = set(self.pluginmanager.plugins).difference(active)
assert not set(toload).intersection(tounload)
self.pluginmanager.load(toload)
# TODO: do not allow backends unloading
self.pluginmanager.unload(tounload)
self.pluginmanager.autoload = autoload
[docs] def save(self, settings):
self.update_pluginmanager()
self.pluginmanager.save_settings(settings)
[docs] def showPluginInfo(self, index):
item = self.pluginsTableWidget.item(index, 0)
name = str(item.text())
try:
plugin = self.pluginmanager.plugins[name]
active = True
except KeyError:
active = False
try:
plugin = sys.modules[name]
except KeyError:
return
d = PluginInfoDialog(plugin, active)
d.exec_()
PluginInfoFormBase = qt4support.getuiform('plugininfo', __name__)
[docs]class PluginInfoDialog(QtGui.QDialog):
def __init__(self, plugin, active, parent=None,
flags=QtCore.Qt.WindowFlags(0), **kwargs):
super(PluginInfoDialog, self).__init__(parent, flags, **kwargs)
self.setModal(True)
bbox = QtGui.QDialogButtonBox()
bbox.addButton(bbox.Close)
b = bbox.button(bbox.Close)
b.clicked.connect(self.accept)
layout = QtGui.QVBoxLayout()
layout.addWidget(PluginInfoForm(plugin, active))
layout.addWidget(bbox)
self.setLayout(layout)