Skip to content

Commit

Permalink
Plugin dependencies followup: load dependent plugins after their depe…
Browse files Browse the repository at this point in the history
…ndencies on QGIS startup
  • Loading branch information
gacarrillor authored and nyalldawson committed Sep 6, 2021
1 parent ac07b4f commit c1dcb2c
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 29 deletions.
24 changes: 3 additions & 21 deletions python/pyplugin_installer/plugindependencies.py
Expand Up @@ -15,7 +15,7 @@
from configparser import NoOptionError, NoSectionError
from .version_compare import compareVersions
from . import installer as plugin_installer
from qgis.utils import updateAvailablePlugins, metadataParser
from qgis.utils import updateAvailablePlugins, metadataParser, get_plugin_deps


def __plugin_name_map(plugin_data_values):
Expand All @@ -25,25 +25,6 @@ def __plugin_name_map(plugin_data_values):
}


def __get_plugin_deps(plugin_id):
result = {}
updateAvailablePlugins()
try:
parser = metadataParser()[plugin_id]
plugin_deps = parser.get('general', 'plugin_dependencies')
except (NoOptionError, NoSectionError, KeyError):
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
Expand All @@ -64,7 +45,8 @@ def find_dependencies(plugin_id, plugin_data=None, plugin_deps=None, installed_p
not_found = {}

if plugin_deps is None:
plugin_deps = __get_plugin_deps(plugin_id)
updateAvailablePlugins()
plugin_deps = get_plugin_deps(plugin_id)

if installed_plugins is None:
updateAvailablePlugins()
Expand Down
102 changes: 95 additions & 7 deletions python/utils.py
Expand Up @@ -25,6 +25,7 @@
QGIS utilities module
"""
from typing import List, Dict, Optional

from qgis.PyQt.QtCore import QCoreApplication, QLocale, QThread, qDebug, QUrl
from qgis.PyQt.QtGui import QDesktopServices
Expand Down Expand Up @@ -269,24 +270,111 @@ def metadataParser() -> dict:
return plugins_metadata_parser


def updateAvailablePlugins():
def updateAvailablePlugins(sort_by_dependencies=False):
""" Go through the plugin_paths list and find out what plugins are available. """
# merge the lists
plugins = []
metadata_parser = {}
plugin_name_map = {}
for pluginpath in plugin_paths:
for pluginName, parser in findPlugins(pluginpath):
for plugin_id, parser in findPlugins(pluginpath):
if parser is None:
continue
if pluginName not in plugins:
plugins.append(pluginName)
metadata_parser[pluginName] = parser
if plugin_id not in plugins:
plugins.append(plugin_id)
metadata_parser[plugin_id] = parser
plugin_name_map[parser.get('general', 'name')] = plugin_id

global available_plugins
available_plugins = plugins
global plugins_metadata_parser
plugins_metadata_parser = metadata_parser

global available_plugins
available_plugins = _sortAvailablePlugins(plugins, plugin_name_map) if sort_by_dependencies else plugins


def _sortAvailablePlugins(plugins: List[str], plugin_name_map: Dict[str, str]) -> List[str]:
"""Place dependent plugins after their dependencies
1. Make a copy of plugins list to modify it.
2. Get a plugin dependencies dict.
3. Iterate plugins and leave the real work to _move_plugin()
:param list plugins: List of available plugin ids
:param dict plugin_name_map: Map of plugin_names and plugin_ids, because
get_plugin_deps() only returns plugin names
:return: List of plugins sorted by dependencies.
"""
sorted_plugins = plugins.copy()
visited_plugins = []

deps = {}
for plugin in plugins:
deps[plugin] = [plugin_name_map.get(dep, '') for dep in get_plugin_deps(plugin)]

for plugin in plugins:
_move_plugin(plugin, deps, visited_plugins, sorted_plugins)

return sorted_plugins


def _move_plugin(plugin: str, deps: Dict[str, List[str]], visited: List[str], sorted_plugins: List[str]):
"""Use recursion to move a plugin after its dependencies in a list of
sorted plugins.
Notes:
This function modifies both visited and sorted_plugins lists.
This function will not get trapped in circular dependencies. We avoid a
maximum recursion error by calling return when revisiting a plugin.
Therefore, if a plugin A depends on B and B depends on A, the order will
work in one direction (e.g., A depends on B), but the other direction won't
be satisfied. After all, a circular plugin dependency should not exist.
:param str plugin: Id of the plugin that should be moved in sorted_plugins.
:param dict deps: Dictionary of plugin dependencies.
:param list visited: List of plugins already visited.
:param list sorted_plugins: List of plugins to be modified and sorted.
"""
if plugin in visited:
return
elif plugin not in deps or not deps[plugin]:
visited.append(plugin) # Plugin with no dependencies
else:
visited.append(plugin)

# First move dependencies
for dep in deps[plugin]:
_move_plugin(dep, deps, visited, sorted_plugins)

# Remove current plugin from sorted
# list to get dependency indices
max_index = sorted_plugins.index(plugin)
sorted_plugins.pop(max_index)

for dep in deps[plugin]:
idx = sorted_plugins.index(dep) + 1 if dep in sorted_plugins else -1
max_index = max(idx, max_index)

# Finally, insert after dependencies
sorted_plugins.insert(max_index, plugin)


def get_plugin_deps(plugin_id: str) -> Dict[str, Optional[str]]:
result = {}
try:
parser = plugins_metadata_parser[plugin_id]
plugin_deps = parser.get('general', 'plugin_dependencies')
except (configparser.NoOptionError, configparser.NoSectionError, KeyError):
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 pluginMetadata(packageName: str, fct: str) -> str:
""" fetch metadata from a plugin - use values from metadata.txt """
Expand Down
1 change: 1 addition & 0 deletions src/app/qgspluginregistry.cpp
Expand Up @@ -561,6 +561,7 @@ void QgsPluginRegistry::restoreSessionPlugins( const QString &pluginDirString )
// check for python plugins system-wide
const QStringList pluginList = mPythonUtils->pluginList();
QgsDebugMsgLevel( QStringLiteral( "Loading python plugins" ), 2 );
QgsDebugMsg( QStringLiteral( "Python plugins will be loaded in the following order: " ) + pluginList.join( "," ) );

QStringList corePlugins = QStringList();
corePlugins << QStringLiteral( "GdalTools" );
Expand Down
2 changes: 1 addition & 1 deletion src/python/qgspythonutilsimpl.cpp
Expand Up @@ -623,7 +623,7 @@ QStringList QgsPythonUtilsImpl::extraPluginsPaths() const

QStringList QgsPythonUtilsImpl::pluginList()
{
runString( QStringLiteral( "qgis.utils.updateAvailablePlugins()" ) );
runString( QStringLiteral( "qgis.utils.updateAvailablePlugins(sort_by_dependencies=True)" ) );

QString output;
evalString( QStringLiteral( "'\\n'.join(qgis.utils.available_plugins)" ), output );
Expand Down

0 comments on commit c1dcb2c

Please sign in to comment.