Skip to content

Commit 62895d1

Browse files
authoredApr 1, 2019
Merge pull request #9619 from elpaso/plugin-dependencies
Plugin dependencies optional support
2 parents 2174577 + 2a10c3f commit 62895d1

File tree

12 files changed

+514
-6
lines changed

12 files changed

+514
-6
lines changed
 

‎python/pyplugin_installer/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ SET(PY_PLUGININSTALLER_FILES
99
qgsplugininstallerpluginerrordialog.py
1010
qgsplugininstallerfetchingdialog.py
1111
qgsplugininstallerrepositorydialog.py
12+
qgsplugindependenciesdialog.py
13+
plugindependencies.py
1214
unzip.py
1315
version_compare.py
1416
)
@@ -21,6 +23,7 @@ PYQT_WRAP_UI(PYUI_FILES
2123
qgsplugininstallerinstallingbase.ui
2224
qgsplugininstallerpluginerrorbase.ui
2325
qgsplugininstallerrepositorybase.ui
26+
qgsplugindependenciesdialogbase.ui
2427
)
2528

2629
ADD_CUSTOM_TARGET(pyplugin-installer ALL DEPENDS ${PYUI_FILES})

‎python/pyplugin_installer/installer.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,16 @@
3535
from qgis.core import Qgis, QgsApplication, QgsNetworkAccessManager, QgsSettings, QgsNetworkRequestParameters
3636
from qgis.gui import QgsMessageBar, QgsPasswordLineEdit
3737
from qgis.utils import (iface, startPlugin, unloadPlugin, loadPlugin,
38-
reloadPlugin, updateAvailablePlugins)
38+
reloadPlugin, updateAvailablePlugins, plugins_metadata_parser)
3939
from .installer_data import (repositories, plugins, officialRepo,
4040
settingsGroup, reposGroup, removeDir)
4141
from .qgsplugininstallerinstallingdialog import QgsPluginInstallerInstallingDialog
4242
from .qgsplugininstallerpluginerrordialog import QgsPluginInstallerPluginErrorDialog
4343
from .qgsplugininstallerfetchingdialog import QgsPluginInstallerFetchingDialog
4444
from .qgsplugininstallerrepositorydialog import QgsPluginInstallerRepositoryDialog
4545
from .unzip import unzip
46+
from .plugindependencies import find_dependencies
47+
from .qgsplugindependenciesdialog import QgsPluginDependenciesDialog
4648

4749

4850
# public instances:
@@ -128,7 +130,7 @@ def fetchAvailablePlugins(self, reloadMode):
128130

129131
QApplication.restoreOverrideCursor()
130132

131-
# display error messages for every unavailable reposioty, unless Shift pressed nor all repositories are unavailable
133+
# display error messages for every unavailable repository, unless Shift pressed nor all repositories are unavailable
132134
keepQuiet = QgsApplication.keyboardModifiers() == Qt.KeyboardModifiers(Qt.ShiftModifier)
133135
if repositories.allUnavailable() and repositories.allUnavailable() != repositories.allEnabled():
134136
for key in repositories.allUnavailable():
@@ -230,6 +232,7 @@ def exportPluginsToManager(self):
230232
"downloads": plugin["downloads"],
231233
"average_vote": plugin["average_vote"],
232234
"rating_votes": plugin["rating_votes"],
235+
"plugin_dependencies": plugin.get("plugin_dependencies", None),
233236
"pythonic": "true"
234237
})
235238
iface.pluginManagerInterface().reloadModel()
@@ -309,6 +312,7 @@ def installPlugin(self, key, quiet=False):
309312
QApplication.setOverrideCursor(Qt.WaitCursor)
310313
# update the list of plugins in plugin handling routines
311314
updateAvailablePlugins()
315+
self.processDependencies(plugin["id"])
312316
# try to load the plugin
313317
loadPlugin(plugin["id"])
314318
plugins.getAllInstalled()
@@ -417,6 +421,11 @@ def uninstallPlugin(self, key, quiet=False):
417421
exec("del sys.modules[%s]" % plugin["id"])
418422
except:
419423
pass
424+
try:
425+
exec("del plugins_metadata_parser[%s]" % plugin["id"])
426+
except:
427+
pass
428+
420429
plugins.getAllInstalled()
421430
plugins.rebuild()
422431
self.exportPluginsToManager()
@@ -599,6 +608,7 @@ def installFromZipFile(self, filePath):
599608

600609
if success:
601610
updateAvailablePlugins()
611+
self.processDependencies(pluginName)
602612
loadPlugin(pluginName)
603613
plugins.getAllInstalled()
604614
plugins.rebuild()
@@ -621,3 +631,32 @@ def installFromZipFile(self, filePath):
621631

622632
level = Qgis.Info if success else Qgis.Critical
623633
iface.pluginManagerInterface().pushMessage(msg, level)
634+
635+
def processDependencies(self, plugin_id):
636+
"""Processes plugin dependencies
637+
638+
:param plugin_id: plugin id
639+
:type plugin_id: str
640+
"""
641+
642+
to_install, to_upgrade, not_found = find_dependencies(plugin_id)
643+
if to_install or to_upgrade or not_found:
644+
dlg = QgsPluginDependenciesDialog(plugin_id, to_install, to_upgrade, not_found)
645+
if dlg.exec_() == QgsPluginDependenciesDialog.Accepted:
646+
actions = dlg.actions()
647+
for dependency_plugin_id, action in actions.items():
648+
try:
649+
self.installPlugin(dependency_plugin_id)
650+
if action == 'install':
651+
iface.pluginManagerInterface().pushMessage(self.tr("Plugin dependency <b>%s</b> successfully installed") %
652+
dependency_plugin_id, Qgis.Info)
653+
else:
654+
iface.pluginManagerInterface().pushMessage(self.tr("Plugin dependency <b>%s</b> successfully upgraded") %
655+
dependency_plugin_id, Qgis.Info)
656+
except Exception as ex:
657+
if action == 'install':
658+
iface.pluginManagerInterface().pushMessage(self.tr("Error installing plugin dependency <b>%s</b>: %s") %
659+
(dependency_plugin_id, ex), Qgis.Warning)
660+
else:
661+
iface.pluginManagerInterface().pushMessage(self.tr("Error upgrading plugin dependency <b>%s</b>: %s") %
662+
(dependency_plugin_id, ex), Qgis.Warning)

‎python/pyplugin_installer/installer_data.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"downloads" unicode, # number of downloads
8686
"average_vote" unicode, # average vote
8787
"rating_votes" unicode, # number of votes
88+
"plugin_dependencies" unicode, # PIP-style comma separated list of plugin dependencies
8889
}}
8990
"""
9091

@@ -446,7 +447,8 @@ def xmlDownloaded(self):
446447
"version_installed": "",
447448
"zip_repository": reposName,
448449
"library": "",
449-
"readonly": False
450+
"readonly": False,
451+
"plugin_dependencies": pluginNodes.item(i).firstChildElement("plugin_dependencies").text().strip(),
450452
}
451453
qgisMinimumVersion = pluginNodes.item(i).firstChildElement("qgis_minimum_version").text().strip()
452454
if not qgisMinimumVersion:
@@ -674,7 +676,9 @@ def pluginMetadata(fct):
674676
"status": "orphan", # Will be overwritten, if any available version found.
675677
"error": error,
676678
"error_details": errorDetails,
677-
"readonly": readOnly}
679+
"readonly": readOnly,
680+
"plugin_dependencies": pluginMetadata("plugin_dependencies"),
681+
}
678682
return plugin
679683

680684
# ----------------------------------------- #
@@ -746,9 +750,9 @@ def rebuild(self):
746750
# other remote metadata is preferred:
747751
for attrib in ["name", "plugin_id", "description", "about", "category", "tags", "changelog", "author_name", "author_email", "homepage",
748752
"tracker", "code_repository", "experimental", "deprecated", "version_available", "zip_repository",
749-
"download_url", "filename", "downloads", "average_vote", "rating_votes", "trusted"]:
753+
"download_url", "filename", "downloads", "average_vote", "rating_votes", "trusted", "plugin_dependencies"]:
750754
if attrib not in translatableAttributes or attrib == "name": # include name!
751-
if plugin[attrib]:
755+
if plugin.get(attrib, False):
752756
self.mPlugins[key][attrib] = plugin[attrib]
753757
# set status
754758
#
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# coding=utf-8
2+
"""Parse plugin metadata for plugin_dependencies
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
9+
"""
10+
11+
__author__ = 'elpaso@itopen.it'
12+
__date__ = '2018-05-29'
13+
__copyright__ = 'Copyright 2018, GISCE-TI S.L.'
14+
15+
from configparser import NoOptionError, NoSectionError
16+
from .version_compare import compareVersions
17+
from . import installer as plugin_installer
18+
from qgis.utils import updateAvailablePlugins
19+
20+
21+
def __plugin_name_map(plugin_data_values):
22+
return {
23+
plugin['name']: plugin['id']
24+
for plugin in plugin_data_values
25+
}
26+
27+
28+
def __get_plugin_deps(plugin_id):
29+
30+
result = {}
31+
parser = updateAvailablePlugins()[plugin_id]
32+
try:
33+
plugin_deps = parser.get('general', 'plugin_dependencies')
34+
except (NoOptionError, NoSectionError):
35+
return result
36+
37+
for dep in plugin_deps.split(','):
38+
if dep.find('==') > 0:
39+
name, version_required = dep.split('==')
40+
else:
41+
name = dep
42+
version_required = None
43+
result[name] = version_required
44+
return result
45+
46+
47+
def find_dependencies(plugin_id, plugin_data=None, plugin_deps=None, installed_plugins=None):
48+
"""Finds the plugin dependencies and checks if they can be installed or upgraded
49+
50+
:param plugin_id: plugin id
51+
:type plugin_id: str
52+
:param plugin_data: for testing only: dictionary of plugin data from the repo, defaults to None
53+
:param plugin_data: dict, optional
54+
:param plugin_deps: for testing only: dict of plugin id -> version_required, parsed from metadata value for "plugin_dependencies", defaults to None
55+
:param plugin_deps: dict, optional
56+
:param installed_plugins: for testing only: dict of plugin id -> version_installed
57+
:param installed_plugins: dict, optional
58+
:return: result dictionaries keyed by plugin name with: to_install, to_upgrade, not_found
59+
:rtype: tuple of dicts
60+
"""
61+
62+
to_install = {}
63+
to_upgrade = {}
64+
not_found = {}
65+
66+
if plugin_deps is None:
67+
plugin_deps = __get_plugin_deps(plugin_id)
68+
69+
if installed_plugins is None:
70+
metadata_parser = updateAvailablePlugins()
71+
installed_plugins = {metadata_parser[k].get('general', 'name'): metadata_parser[k].get('general', 'version') for k, v in metadata_parser.items()}
72+
73+
if plugin_data is None:
74+
plugin_data = plugin_installer.plugins.all()
75+
76+
plugins_map = __plugin_name_map(plugin_data.values())
77+
78+
# Review all dependencies
79+
for name, version_required in plugin_deps.items():
80+
try:
81+
p_id = plugins_map[name]
82+
except KeyError:
83+
not_found.update({name: {
84+
'id': None,
85+
'version_installed': None,
86+
'version_required': None,
87+
'version_available': None,
88+
'action': None,
89+
'error': 'missing_id'
90+
}})
91+
continue
92+
93+
affected_plugin = dict({
94+
"id": p_id,
95+
# "version_installed": installed_plugins.get(p_id, {}).get('installed_plugins', None),
96+
"version_installed": installed_plugins.get(name, None),
97+
"version_required": version_required,
98+
"version_available": plugin_data[p_id].get('version_available', None),
99+
"action": None,
100+
})
101+
102+
# Install is needed
103+
if name not in installed_plugins:
104+
affected_plugin['action'] = 'install'
105+
destination_list = to_install
106+
# Upgrade is needed
107+
elif version_required is not None and compareVersions(installed_plugins[name], version_required) == 2:
108+
affected_plugin['action'] = 'upgrade'
109+
destination_list = to_upgrade
110+
# TODO @elpaso: review installed but not activated
111+
# No action is needed
112+
else:
113+
continue
114+
115+
if affected_plugin['version_required'] == affected_plugin['version_available'] or affected_plugin['version_required'] is None:
116+
destination_list.update({name: affected_plugin})
117+
else:
118+
affected_plugin['error'] = 'unavailable {}'.format(affected_plugin['action'])
119+
not_found.update({name: affected_plugin})
120+
121+
return to_install, to_upgrade, not_found
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# coding=utf-8
2+
"""Plugin dependencies selection dialog
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
9+
"""
10+
11+
__author__ = 'elpaso@itopen.it'
12+
__date__ = '2018-09-19'
13+
__copyright__ = 'Copyright 2018, GISCE-TI S.L.'
14+
15+
16+
import os
17+
18+
from qgis.PyQt import QtWidgets, QtCore
19+
from .ui_qgsplugindependenciesdialogbase import Ui_QgsPluginDependenciesDialogBase
20+
from qgis.utils import iface
21+
22+
23+
class QgsPluginDependenciesDialog(QtWidgets.QDialog, Ui_QgsPluginDependenciesDialogBase):
24+
"""A dialog that shows plugin dependencies and offers a way to install or upgrade the
25+
dependencies.
26+
"""
27+
28+
def __init__(self, plugin_name, to_install, to_upgrade, not_found, parent=None):
29+
"""Creates the dependencies dialog
30+
31+
:param plugin_name: the name of the parent plugin
32+
:type plugin_name: str
33+
:param to_install: list of plugin IDs that needs to be installed
34+
:type to_install: list
35+
:param to_upgrade: list of plugin IDs that needs to be upgraded
36+
:type to_upgrade: list
37+
:param not_found: list of plugin IDs that are not found (unavailable)
38+
:type not_found: list
39+
:param parent: parent object, defaults to None
40+
:param parent: QWidget, optional
41+
"""
42+
43+
super().__init__(parent)
44+
self.setupUi(self)
45+
self.setWindowTitle(self.tr("Plugin Dependencies Manager"))
46+
self.mPluginDependenciesLabel.setText(self.tr("Plugin dependencies for <b>%s</b>") % plugin_name)
47+
self.setStyleSheet("QTableView { padding: 20px;}")
48+
# Name, Version Installed, Version Required, Version Available, Action Checkbox
49+
self.pluginList.setColumnCount(5)
50+
self.pluginList.setHorizontalHeaderLabels([self.tr('Name'), self.tr('Installed'), self.tr('Required'), self.tr('Available'), self.tr('Action')])
51+
self.pluginList.setRowCount(len(not_found) + len(to_install) + len(to_upgrade))
52+
self.__actions = {}
53+
54+
def _display(txt):
55+
if txt is None:
56+
return ""
57+
return txt
58+
59+
def _make_row(data, i, name):
60+
widget = QtWidgets.QLabel("<b>%s</b>" % name)
61+
widget.p_id = data['id']
62+
widget.action = data['action']
63+
self.pluginList.setCellWidget(i, 0, widget)
64+
widget = QtWidgets.QTableWidgetItem(_display(data['version_installed']))
65+
widget.setTextAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
66+
self.pluginList.setItem(i, 1, widget)
67+
widget = QtWidgets.QTableWidgetItem(_display(data['version_required']))
68+
widget.setTextAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
69+
self.pluginList.setItem(i, 2, widget)
70+
widget = QtWidgets.QTableWidgetItem(_display(data['version_available']))
71+
widget.setTextAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
72+
self.pluginList.setItem(i, 3, widget)
73+
74+
i = 0
75+
for name, data in to_install.items():
76+
_make_row(data, i, name)
77+
widget = QtWidgets.QCheckBox(self.tr("Install"))
78+
widget.setChecked(True)
79+
self.pluginList.setCellWidget(i, 4, widget)
80+
i += 1
81+
82+
for name, data in to_upgrade.items():
83+
_make_row(data, i, name)
84+
widget = QtWidgets.QCheckBox(self.tr("Upgrade"))
85+
widget.setChecked(True)
86+
self.pluginList.setCellWidget(i, 4, widget)
87+
i += 1
88+
89+
for name, data in not_found.items():
90+
_make_row(data, i, name)
91+
widget = QtWidgets.QLabel(self.tr("Fix manually"))
92+
self.pluginList.setCellWidget(i, 4, widget)
93+
i += 1
94+
95+
def actions(self):
96+
"""Returns the list of actions
97+
98+
:return: dict of actions
99+
:rtype: dict
100+
"""
101+
102+
return self.__actions
103+
104+
def accept(self):
105+
self.__actions = {}
106+
for i in range(self.pluginList.rowCount()):
107+
try:
108+
if self.pluginList.cellWidget(i, 4).isChecked():
109+
self.__actions[self.pluginList.cellWidget(i, 0).p_id] = self.pluginList.cellWidget(i, 0).action
110+
except:
111+
pass
112+
super().accept()
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>QgsPluginDependenciesDialogBase</class>
4+
<widget class="QDialog" name="QgsPluginDependenciesDialogBase">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>1211</width>
10+
<height>437</height>
11+
</rect>
12+
</property>
13+
<property name="windowTitle">
14+
<string>Dialog</string>
15+
</property>
16+
<layout class="QVBoxLayout" name="verticalLayout">
17+
<item>
18+
<widget class="QLabel" name="mPluginDependenciesLabel">
19+
<property name="text">
20+
<string>Plugin dependencies</string>
21+
</property>
22+
</widget>
23+
</item>
24+
<item>
25+
<widget class="QTableWidget" name="pluginList">
26+
<property name="editTriggers">
27+
<set>QAbstractItemView::NoEditTriggers</set>
28+
</property>
29+
<property name="selectionMode">
30+
<enum>QAbstractItemView::NoSelection</enum>
31+
</property>
32+
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
33+
<bool>false</bool>
34+
</attribute>
35+
<attribute name="horizontalHeaderStretchLastSection">
36+
<bool>true</bool>
37+
</attribute>
38+
<attribute name="verticalHeaderVisible">
39+
<bool>false</bool>
40+
</attribute>
41+
<attribute name="verticalHeaderHighlightSections">
42+
<bool>false</bool>
43+
</attribute>
44+
<attribute name="verticalHeaderStretchLastSection">
45+
<bool>false</bool>
46+
</attribute>
47+
</widget>
48+
</item>
49+
<item>
50+
<widget class="QDialogButtonBox" name="buttonBox">
51+
<property name="orientation">
52+
<enum>Qt::Horizontal</enum>
53+
</property>
54+
<property name="standardButtons">
55+
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
56+
</property>
57+
</widget>
58+
</item>
59+
</layout>
60+
</widget>
61+
<resources/>
62+
<connections>
63+
<connection>
64+
<sender>buttonBox</sender>
65+
<signal>accepted()</signal>
66+
<receiver>QgsPluginDependenciesDialogBase</receiver>
67+
<slot>accept()</slot>
68+
<hints>
69+
<hint type="sourcelabel">
70+
<x>248</x>
71+
<y>254</y>
72+
</hint>
73+
<hint type="destinationlabel">
74+
<x>157</x>
75+
<y>274</y>
76+
</hint>
77+
</hints>
78+
</connection>
79+
<connection>
80+
<sender>buttonBox</sender>
81+
<signal>rejected()</signal>
82+
<receiver>QgsPluginDependenciesDialogBase</receiver>
83+
<slot>reject()</slot>
84+
<hints>
85+
<hint type="sourcelabel">
86+
<x>316</x>
87+
<y>260</y>
88+
</hint>
89+
<hint type="destinationlabel">
90+
<x>286</x>
91+
<y>274</y>
92+
</hint>
93+
</hints>
94+
</connection>
95+
</connections>
96+
</ui>

‎python/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ def updateAvailablePlugins():
282282
available_plugins = plugins
283283
global plugins_metadata_parser
284284
plugins_metadata_parser = metadata_parser
285+
return metadata_parser
285286

286287

287288
def pluginMetadata(packageName, fct):

‎scripts/spell_check/.agignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@ tests/testdata/qgis_server_accesscontrol/project.qgs
6464
tests/testdata/qgis_server/ets-wms13/project.qgs
6565
tests/testdata/layouts/sample_project.qgs
6666
tests/testdata/layouts/2x_template_attributetable.qpt
67+
tests/testdata/plugindependencies_data.json
6768

‎src/app/pluginmanager/qgspluginmanager.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,13 @@ void QgsPluginManager::showPluginDetails( QStandardItem *item )
995995
html += QStringLiteral( "<tr><td class='key'>%1 </td><td>%2</td></tr>" ).arg( tr( "Changelog" ), changelog );
996996
}
997997

998+
if ( ! metadata->value( QStringLiteral( "plugin_dependencies" ) ).isEmpty() )
999+
{
1000+
QString pluginDependencies = metadata->value( QStringLiteral( "plugin_dependencies" ) );
1001+
pluginDependencies = pluginDependencies.trimmed();
1002+
html += QStringLiteral( "<tr><td class='key'>%1 </td><td>%2</td></tr>" ).arg( tr( "Plugin dependencies" ), pluginDependencies );
1003+
}
1004+
9981005
html += QLatin1String( "</table>" );
9991006

10001007
html += QLatin1String( "</body>" );

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ ADD_PYTHON_TEST(PyQgsAuthSettingsWidget test_authsettingswidget.py)
244244
ADD_PYTHON_TEST(PyQgsAuxiliaryStorage test_qgsauxiliarystorage.py)
245245
ADD_PYTHON_TEST(PyQgsAuthManagerOgr test_authmanager_ogr.py)
246246
ADD_PYTHON_TEST(PyQgsFieldValidator test_qgsfieldvalidator.py)
247+
ADD_PYTHON_TEST(PyQgsPluginDependencies test_plugindependencies.py)
247248

248249
IF (NOT WIN32)
249250
ADD_PYTHON_TEST(PyQgsLogger test_qgslogger.py)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# coding=utf-8
2+
"""QGIS Plugin dependencies test
3+
4+
.. note:: This program is free software; you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation; either version 2 of the License, or
7+
(at your option) any later version.
8+
9+
"""
10+
11+
__author__ = 'elpaso@itopen.it'
12+
__date__ = '2018-09-19'
13+
__copyright__ = 'Copyright 2018, GISCE-TI S.L.'
14+
15+
import uuid
16+
import os
17+
import re
18+
import json
19+
import unittest
20+
from qgis.PyQt.QtCore import QCoreApplication
21+
from pyplugin_installer.plugindependencies import find_dependencies
22+
23+
24+
from qgis.testing import (
25+
start_app,
26+
unittest,
27+
)
28+
29+
from utilities import unitTestDataPath
30+
31+
TESTDATA_PATH = unitTestDataPath()
32+
33+
34+
class PluginDependenciesTest(unittest.TestCase):
35+
"""Test plugin dependencies"""
36+
37+
@classmethod
38+
def setUpClass(cls):
39+
"""Runs at start."""
40+
41+
QCoreApplication.setOrganizationName("QGIS")
42+
QCoreApplication.setOrganizationDomain("qgis.org")
43+
QCoreApplication.setApplicationName("QGIS-TEST-%s" % uuid.uuid1())
44+
qgis_app = start_app()
45+
46+
# Installed plugins
47+
cls.installed_plugins = {
48+
'MetaSearch': '0.3.5',
49+
'QuickWKT': '3.1',
50+
'db_manager': '0.1.20',
51+
'firstaid': '2.1.1',
52+
'InaSAFE': '5.0.0',
53+
'ipyconsole': '1.8',
54+
'plugin_reloader': '0.7.4',
55+
'processing': '2.12.99',
56+
'qgis-geocoding': '2.18',
57+
'qgisce': '0.9',
58+
'redistrict': '0.1'
59+
}
60+
61+
data_path = os.path.join(TESTDATA_PATH, 'plugindependencies_data.json')
62+
with open(data_path) as f:
63+
cls.plugin_data = json.loads(f.read())
64+
65+
def setUp(self):
66+
"""Runs before each test."""
67+
pass
68+
69+
def tearDown(self):
70+
"""Runs after each test."""
71+
pass
72+
73+
def test_find_dependencies(self):
74+
75+
to_install, to_upgrade, not_found = find_dependencies('qgisce', self.plugin_data, plugin_deps={'InaSAFE': None}, installed_plugins=self.installed_plugins)
76+
self.assertEqual(to_install, {})
77+
self.assertEqual(to_upgrade, {})
78+
self.assertEqual(not_found, {})
79+
80+
to_install, to_upgrade, not_found = find_dependencies('qgisce', self.plugin_data, plugin_deps={'InaSAFE': '110.1'}, installed_plugins=self.installed_plugins)
81+
self.assertEqual(to_install, {})
82+
self.assertEqual(to_upgrade, {})
83+
self.assertEqual(not_found['InaSAFE']['version_installed'], '5.0.0')
84+
85+
# QuickWkt is installed, version is not specified: ignore
86+
installed_plugins = self.installed_plugins
87+
installed_plugins['QuickWKT'] = '2.1'
88+
to_install, to_upgrade, not_found = find_dependencies('qgisce', self.plugin_data, plugin_deps={'QuickMapServices': '0.19.10.1', 'QuickWKT': None}, installed_plugins=self.installed_plugins)
89+
self.assertEqual(to_install['QuickMapServices']['version_required'], '0.19.10.1')
90+
self.assertEqual(to_upgrade, {})
91+
self.assertEqual(not_found, {})
92+
93+
# QuickWkt is installed, version requires upgrade and it's in the repo: upgrade
94+
to_install, to_upgrade, not_found = find_dependencies('qgisce', self.plugin_data, plugin_deps={'QuickWKT': '3.1'}, installed_plugins=installed_plugins)
95+
self.assertEqual(to_install, {})
96+
self.assertEqual(to_upgrade['QuickWKT']['version_required'], '3.1')
97+
self.assertEqual(not_found, {})
98+
99+
# QuickWkt is installed, version requires upgrade and it's NOT in the repo: not found
100+
to_install, to_upgrade, not_found = find_dependencies('qgisce', self.plugin_data, plugin_deps={'QuickWKT': '300.11234'}, installed_plugins=installed_plugins)
101+
self.assertEqual(to_install, {})
102+
self.assertEqual(to_upgrade, {})
103+
self.assertEqual(not_found['QuickWKT']['version_required'], '300.11234')
104+
self.assertEqual(not_found['QuickWKT']['version_installed'], '2.1')
105+
self.assertEqual(not_found['QuickWKT']['version_available'], '3.1')
106+
107+
# Installed version is > than required: ignore (no downgrade is currently possible)
108+
installed_plugins['QuickWKT'] = '300.1'
109+
to_install, to_upgrade, not_found = find_dependencies('qgisce', self.plugin_data, plugin_deps={'QuickWKT': '1.2'}, installed_plugins=installed_plugins)
110+
self.assertEqual(to_install, {})
111+
self.assertEqual(to_upgrade, {})
112+
self.assertEqual(not_found, {})
113+
114+
115+
def pluginSuite():
116+
return unittest.makeSuite(PluginDependenciesTest, 'test')
117+
118+
119+
if __name__ == "__main__":
120+
suite = unittest.makeSuite(PluginDependenciesTest)
121+
runner = unittest.TextTestRunner(verbosity=2)
122+
runner.run(suite)

‎tests/testdata/plugindependencies_data.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.