-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feature][needs-docs] Plugin dependencies
Implementation of QEP 132: Manage python cross-plugins dependencies A new optional metadata entry will be added to metadata.txt: plugin_dependencies The metadata will contain a comma separated list of plugin names, with a format similar of the one used by pip, with optional version. After a successful plugin installation, if the plugin has any unsatisfied dependency, a dialog will pop-up with the list of unmet dependencies and the user will be able to choose if she wants to install or upgrade the dependencies or ignore them. Example metadata: plugin_dependencies = QuickMapServices==0.19.10.1,QuickWKT Funded by GISCE-TI S.L.
- Loading branch information
Showing
7 changed files
with
383 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
# coding=utf-8 | ||
"""Parse plugin metadata for plugin_dependencies and install/update | ||
required plugins | ||
.. note:: 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. | ||
""" | ||
|
||
__author__ = 'elpaso@itopen.it' | ||
__date__ = '2018-05-29' | ||
__copyright__ = 'Copyright 2018, GISCE-TI S.L.' | ||
|
||
from configparser import NoOptionError, NoSectionError | ||
from .version_compare import compareVersions | ||
from . import installer as plugin_installer | ||
|
||
|
||
def __plugin_name_map(plugin_data_values): | ||
return { | ||
plugin['name']: plugin['id'] | ||
for plugin in plugin_data_values | ||
} | ||
|
||
|
||
def __get_plugin_deps(plugin_id): | ||
|
||
result = {} | ||
from qgis.utils import plugins_metadata_parser | ||
parser = plugins_metadata_parser[plugin_id] | ||
try: | ||
plugin_deps = parser.get('general', 'plugin_dependencies') | ||
except (NoOptionError, NoSectionError): | ||
return result | ||
|
||
for dep in plugin_deps.split(','): | ||
if dep.find('==') > 0: | ||
name, version_required = dep.split('==') | ||
else: | ||
name = dep | ||
version_required = None | ||
result[name] = version_required | ||
return result | ||
|
||
|
||
def find_dependencies(plugin_id, plugin_data=None, plugin_deps=None, installed_plugins=None): | ||
"""Finds the plugin dependencies and checks if they can be installed or upgraded | ||
:param plugin_id: plugin id | ||
:type plugin_id: str | ||
:param plugin_data: for testing only: dictionary of plugin data from the repo, defaults to None | ||
:param plugin_data: dict, optional | ||
:param plugin_deps: for testing only: dict of plugin id -> version_required, parsed from metadata value for "plugin_dependencies", defaults to None | ||
:param plugin_deps: dict, optional | ||
:param installed_plugins: for testing only: dict of plugin id -> version_installed | ||
:param installed_plugins: dict, optional | ||
:return: result dictionaries keyed by plugin name with: to_install, to_upgrade, not_found | ||
:rtype: tuple of dicts | ||
""" | ||
|
||
to_install = {} | ||
to_upgrade = {} | ||
not_found = {} | ||
|
||
if plugin_deps is None: | ||
plugin_deps = __get_plugin_deps(plugin_id) | ||
|
||
if installed_plugins is None: | ||
from qgis.utils import plugins_metadata_parser | ||
installed_plugins = {plugins_metadata_parser[k].get('general', 'name'): plugins_metadata_parser[k].get('general', 'version') for k, v in plugins_metadata_parser.items()} | ||
|
||
if plugin_data is None: | ||
plugin_data = plugin_installer.plugins.all() | ||
|
||
plugins_map = __plugin_name_map(plugin_data.values()) | ||
|
||
# Review all dependencies | ||
for name, version_required in plugin_deps.items(): | ||
try: | ||
p_id = plugins_map[name] | ||
except KeyError: | ||
not_found.update({name: { | ||
'id': None, | ||
'version_installed': None, | ||
'version_required': None, | ||
'version_available': None, | ||
'action': None, | ||
'error': 'missing_id' | ||
}}) | ||
continue | ||
|
||
affected_plugin = dict({ | ||
"id": p_id, | ||
# "version_installed": installed_plugins.get(p_id, {}).get('installed_plugins', None), | ||
"version_installed": installed_plugins.get(name, None), | ||
"version_required": version_required, | ||
"version_available": plugin_data[p_id].get('version_available', None), | ||
"action": None, | ||
}) | ||
|
||
# Install is needed | ||
if name not in installed_plugins: | ||
affected_plugin['action'] = 'install' | ||
destination_list = to_install | ||
# Upgrade is needed | ||
elif version_required is not None and compareVersions(installed_plugins[name], version_required) == 2: | ||
affected_plugin['action'] = 'upgrade' | ||
destination_list = to_upgrade | ||
# TODO @elpaso: review installed but not activated | ||
# No action is needed | ||
else: | ||
continue | ||
|
||
if affected_plugin['version_required'] == affected_plugin['version_available'] or affected_plugin['version_required'] is None: | ||
destination_list.update({name: affected_plugin}) | ||
else: | ||
affected_plugin['error'] = 'unavailable {}'.format(affected_plugin['action']) | ||
not_found.update({name: affected_plugin}) | ||
|
||
return to_install, to_upgrade, not_found |
112 changes: 112 additions & 0 deletions
112
python/pyplugin_installer/qgsplugindependenciesdialog.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
# coding=utf-8 | ||
"""Plugin update/install dialog | ||
.. note:: 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. | ||
""" | ||
|
||
__author__ = 'elpaso@itopen.it' | ||
__date__ = '2018-09-19' | ||
__copyright__ = 'Copyright 2018, GISCE-TI S.L.' | ||
|
||
|
||
import os | ||
|
||
from qgis.PyQt import QtWidgets, QtCore | ||
from .ui_qgsplugindependenciesdialogbase import Ui_QgsPluginDependenciesDialogBase | ||
from qgis.utils import iface | ||
|
||
|
||
class QgsPluginDependenciesDialog(QtWidgets.QDialog, Ui_QgsPluginDependenciesDialogBase): | ||
"""A dialog that shows plugin dependencies and offers a way to install or upgrade the | ||
dependencies. | ||
""" | ||
|
||
def __init__(self, plugin_name, to_install, to_upgrade, not_found, parent=None): | ||
"""Creates the dependencies dialog | ||
:param plugin_name: the name of the parent plugin | ||
:type plugin_name: str | ||
:param to_install: list of plugin IDs that needs to be installed | ||
:type to_install: list | ||
:param to_upgrade: list of plugin IDs that needs to be upgraded | ||
:type to_upgrade: list | ||
:param not_found: list of plugin IDs that are not found (unvailable) | ||
:type not_found: list | ||
:param parent: parent object, defaults to None | ||
:param parent: QWidget, optional | ||
""" | ||
|
||
super().__init__(parent) | ||
self.setupUi(self) | ||
self.setWindowTitle(self.tr("Plugin Dependencies Manager")) | ||
self.mPluginDependenciesLabel.setText(self.tr("Plugin dependencies for <b>%s</b>") % plugin_name) | ||
self.setStyleSheet("QTableView { padding: 20px;}") | ||
# Name, Version Installed, Version Required, Version Available, Action Checkbox | ||
self.pluginList.setColumnCount(5) | ||
self.pluginList.setHorizontalHeaderLabels([self.tr('Name'), self.tr('Installed'), self.tr('Required'), self.tr('Available'), self.tr('Action')]) | ||
self.pluginList.setRowCount(len(not_found) + len(to_install) + len(to_upgrade)) | ||
self.__actions = {} | ||
|
||
def _display(txt): | ||
if txt is None: | ||
return "" | ||
return txt | ||
|
||
def _make_row(data, i, name): | ||
widget = QtWidgets.QLabel("<b>%s</b>" % name) | ||
widget.p_id = data['id'] | ||
widget.action = data['action'] | ||
self.pluginList.setCellWidget(i, 0, widget) | ||
widget = QtWidgets.QTableWidgetItem(_display(data['version_installed'])) | ||
widget.setTextAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) | ||
self.pluginList.setItem(i, 1, widget) | ||
widget = QtWidgets.QTableWidgetItem(_display(data['version_required'])) | ||
widget.setTextAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) | ||
self.pluginList.setItem(i, 2, widget) | ||
widget = QtWidgets.QTableWidgetItem(_display(data['version_available'])) | ||
widget.setTextAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) | ||
self.pluginList.setItem(i, 3, widget) | ||
|
||
i = 0 | ||
for name, data in to_install.items(): | ||
_make_row(data, i, name) | ||
widget = QtWidgets.QCheckBox(self.tr("Install")) | ||
widget.setChecked(True) | ||
self.pluginList.setCellWidget(i, 4, widget) | ||
i += 1 | ||
|
||
for name, data in to_upgrade.items(): | ||
_make_row(data, i, name) | ||
widget = QtWidgets.QCheckBox(self.tr("Upgrade")) | ||
widget.setChecked(True) | ||
self.pluginList.setCellWidget(i, 4, widget) | ||
i += 1 | ||
|
||
for name, data in not_found.items(): | ||
_make_row(data, i, name) | ||
widget = QtWidgets.QLabel(self.tr("Fix manually")) | ||
self.pluginList.setCellWidget(i, 4, widget) | ||
i += 1 | ||
|
||
def actions(self): | ||
"""Returns the list of actions | ||
:return: dict of actions | ||
:rtype: dict | ||
""" | ||
|
||
return self.__actions | ||
|
||
def accept(self): | ||
self.__actions = {} | ||
for i in range(self.pluginList.rowCount()): | ||
try: | ||
if self.pluginList.cellWidget(i, 4).isChecked(): | ||
self.__actions[self.pluginList.cellWidget(i, 0).p_id] = self.pluginList.cellWidget(i, 0).action | ||
except: | ||
pass | ||
super().accept() |
Oops, something went wrong.