mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-23 00:02:38 -05:00
1233 lines
37 KiB
Python
1233 lines
37 KiB
Python
"""
|
|
***************************************************************************
|
|
utils.py
|
|
---------------------
|
|
Date : November 2009
|
|
Copyright : (C) 2009 by Martin Dobias
|
|
Email : wonder dot sk at gmail dot com
|
|
***************************************************************************
|
|
* *
|
|
* 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__ = "Martin Dobias"
|
|
__date__ = "November 2009"
|
|
__copyright__ = "(C) 2009, Martin Dobias"
|
|
|
|
"""
|
|
QGIS utilities module
|
|
|
|
"""
|
|
from typing import List, Dict, Optional
|
|
|
|
from qgis.PyQt.QtCore import (
|
|
QT_VERSION_STR,
|
|
QCoreApplication,
|
|
QLocale,
|
|
QThread,
|
|
qDebug,
|
|
QUrl,
|
|
)
|
|
from qgis.PyQt.QtGui import QDesktopServices
|
|
from qgis.PyQt.QtWidgets import QPushButton, QApplication
|
|
from qgis.core import Qgis, QgsMessageLog, qgsfunction, QgsMessageOutput
|
|
from qgis.gui import QgsMessageBar
|
|
|
|
import os
|
|
import sys
|
|
import traceback
|
|
import glob
|
|
import os.path
|
|
import configparser
|
|
import warnings
|
|
import codecs
|
|
import time
|
|
import functools
|
|
|
|
import builtins
|
|
|
|
builtins.__dict__["unicode"] = str
|
|
builtins.__dict__["basestring"] = str
|
|
builtins.__dict__["long"] = int
|
|
builtins.__dict__["Set"] = set
|
|
|
|
# ######################
|
|
# ERROR HANDLING
|
|
|
|
warnings.simplefilter("default")
|
|
warnings.filterwarnings("ignore", "the sets module is deprecated")
|
|
|
|
|
|
def showWarning(message, category, filename, lineno, file=None, line=None):
|
|
stk = ""
|
|
for s in traceback.format_stack()[:-2]:
|
|
if hasattr(s, "decode"):
|
|
stk += s.decode(sys.getfilesystemencoding())
|
|
else:
|
|
stk += s
|
|
if hasattr(filename, "decode"):
|
|
decoded_filename = filename.decode(sys.getfilesystemencoding())
|
|
else:
|
|
decoded_filename = filename
|
|
QgsMessageLog.logMessage(
|
|
f"warning:{warnings.formatwarning(message, category, decoded_filename, lineno)}\ntraceback:{stk}",
|
|
QCoreApplication.translate("Python", "Python warning"),
|
|
)
|
|
|
|
|
|
def showException(
|
|
type, value, tb, msg, messagebar=False, level=Qgis.MessageLevel.Warning
|
|
):
|
|
if msg is None:
|
|
msg = QCoreApplication.translate(
|
|
"Python", "An error has occurred while executing Python code:"
|
|
)
|
|
|
|
logmessage = ""
|
|
for s in traceback.format_exception(type, value, tb):
|
|
logmessage += s.decode("utf-8", "replace") if hasattr(s, "decode") else s
|
|
|
|
title = QCoreApplication.translate("Python", "Python error")
|
|
QgsMessageLog.logMessage(logmessage, title, level)
|
|
|
|
try:
|
|
blockingdialog = QApplication.instance().activeModalWidget()
|
|
window = QApplication.instance().activeWindow()
|
|
except:
|
|
blockingdialog = QApplication.activeModalWidget()
|
|
window = QApplication.activeWindow()
|
|
|
|
# Still show the normal blocking dialog in this case for now.
|
|
if blockingdialog or not window or not messagebar or not iface:
|
|
open_stack_dialog(type, value, tb, msg)
|
|
return
|
|
|
|
bar = iface.messageBar() if iface else None
|
|
|
|
# If it's not the main window see if we can find a message bar to report the error in
|
|
if not window.objectName() == "QgisApp":
|
|
widgets = window.findChildren(QgsMessageBar)
|
|
if widgets:
|
|
# Grab the first message bar for now
|
|
bar = widgets[0]
|
|
|
|
item = bar.currentItem()
|
|
if item and item.property("Error") == msg:
|
|
# Return of we already have a message with the same error message
|
|
return
|
|
|
|
widget = bar.createMessage(
|
|
title,
|
|
msg
|
|
+ " "
|
|
+ QCoreApplication.translate(
|
|
"Python", "See message log (Python Error) for more details."
|
|
),
|
|
)
|
|
widget.setProperty("Error", msg)
|
|
stackbutton = QPushButton(
|
|
QCoreApplication.translate("Python", "Stack trace"),
|
|
pressed=functools.partial(open_stack_dialog, type, value, tb, msg),
|
|
)
|
|
button = QPushButton(
|
|
QCoreApplication.translate("Python", "View message log"),
|
|
pressed=show_message_log,
|
|
)
|
|
widget.layout().addWidget(stackbutton)
|
|
widget.layout().addWidget(button)
|
|
bar.pushWidget(widget, Qgis.MessageLevel.Warning)
|
|
|
|
|
|
def show_message_log(pop_error=True):
|
|
if pop_error:
|
|
iface.messageBar().popWidget()
|
|
|
|
iface.openMessageLog()
|
|
|
|
|
|
def open_stack_dialog(type, value, tb, msg, pop_error=True):
|
|
if pop_error and iface is not None:
|
|
iface.messageBar().popWidget()
|
|
|
|
if msg is None:
|
|
msg = QCoreApplication.translate(
|
|
"Python", "An error has occurred while executing Python code:"
|
|
)
|
|
|
|
# TODO Move this to a template HTML file
|
|
txt = """<font color="red"><b>{msg}</b></font>
|
|
<br>
|
|
<h3>{main_error}</h3>
|
|
<pre>
|
|
{error}
|
|
</pre>
|
|
<br>
|
|
<b>{version_label}</b> {num}
|
|
<br>
|
|
<b>{qgis_label}</b> {qversion} {qgisrelease}, {devversion}
|
|
<br>
|
|
<h4>{pypath_label}</h4>
|
|
<ul>
|
|
{pypath}
|
|
</ul>"""
|
|
|
|
error = ""
|
|
lst = traceback.format_exception(type, value, tb)
|
|
for s in lst:
|
|
error += s.decode("utf-8", "replace") if hasattr(s, "decode") else s
|
|
error = error.replace("\n", "<br>")
|
|
|
|
main_error = (
|
|
lst[-1].decode("utf-8", "replace") if hasattr(lst[-1], "decode") else lst[-1]
|
|
)
|
|
|
|
version_label = QCoreApplication.translate("Python", "Python version:")
|
|
qgis_label = QCoreApplication.translate("Python", "QGIS version:")
|
|
pypath_label = QCoreApplication.translate("Python", "Python Path:")
|
|
txt = txt.format(
|
|
msg=msg,
|
|
main_error=main_error,
|
|
error=error,
|
|
version_label=version_label,
|
|
num=sys.version,
|
|
qgis_label=qgis_label,
|
|
qversion=Qgis.QGIS_VERSION,
|
|
qgisrelease=Qgis.QGIS_RELEASE_NAME,
|
|
devversion=Qgis.QGIS_DEV_VERSION,
|
|
pypath_label=pypath_label,
|
|
pypath="".join(f"<li>{path}</li>" for path in sys.path),
|
|
)
|
|
|
|
txt = txt.replace(" ", " ") # preserve whitespaces for nicer output
|
|
|
|
dlg = QgsMessageOutput.createMessageOutput()
|
|
dlg.setTitle(msg)
|
|
dlg.setMessage(txt, QgsMessageOutput.MessageType.MessageHtml)
|
|
dlg.showMessage()
|
|
|
|
|
|
def qgis_excepthook(type, value, tb):
|
|
# detect if running in the main thread
|
|
in_main_thread = (
|
|
QCoreApplication.instance() is None
|
|
or QThread.currentThread() == QCoreApplication.instance().thread()
|
|
)
|
|
|
|
# only use messagebar if running in main thread - otherwise it will crash!
|
|
showException(type, value, tb, None, messagebar=in_main_thread)
|
|
|
|
|
|
def installErrorHook():
|
|
"""
|
|
Installs the QGIS application error/warning hook. This causes Python exceptions
|
|
to be intercepted by the QGIS application and shown in the main window message bar
|
|
and in custom dialogs.
|
|
|
|
Generally you shouldn't call this method - it's automatically called by
|
|
the QGIS app on startup, and has no use in standalone applications and scripts.
|
|
"""
|
|
sys.excepthook = qgis_excepthook
|
|
warnings.showwarning = showWarning
|
|
|
|
|
|
def uninstallErrorHook():
|
|
sys.excepthook = sys.__excepthook__
|
|
|
|
|
|
# initialize 'iface' object
|
|
iface = None
|
|
|
|
|
|
def initInterface(pointer):
|
|
from qgis.gui import QgisInterface
|
|
from qgis.PyQt.sip import wrapinstance
|
|
|
|
global iface
|
|
iface = wrapinstance(pointer, QgisInterface)
|
|
|
|
|
|
#######################
|
|
# PLUGINS
|
|
|
|
# The current path for home directory Python plugins.
|
|
HOME_PLUGIN_PATH: Optional[str] = None
|
|
|
|
# list of plugin paths. it gets filled in by the QGIS python library
|
|
plugin_paths = []
|
|
|
|
# dictionary of plugins
|
|
plugins = {}
|
|
|
|
plugin_times = {}
|
|
|
|
# list of active (started) plugins
|
|
active_plugins = []
|
|
|
|
# list of plugins in plugin directory and home plugin directory
|
|
available_plugins = []
|
|
|
|
# dictionary of plugins providing metadata in a text file (metadata.txt)
|
|
# key = plugin package name, value = config parser instance
|
|
plugins_metadata_parser = {}
|
|
|
|
|
|
def findPlugins(path):
|
|
"""for internal use: return list of plugins in given path"""
|
|
for plugin in glob.glob(path + "/*"):
|
|
if not os.path.isdir(plugin):
|
|
continue
|
|
if not os.path.exists(os.path.join(plugin, "__init__.py")):
|
|
continue
|
|
|
|
metadataFile = os.path.join(plugin, "metadata.txt")
|
|
if not os.path.exists(metadataFile):
|
|
continue
|
|
|
|
cp = configparser.ConfigParser()
|
|
|
|
try:
|
|
with codecs.open(metadataFile, "r", "utf8") as f:
|
|
cp.read_file(f)
|
|
except:
|
|
cp = None
|
|
|
|
pluginName = os.path.basename(plugin)
|
|
yield (pluginName, cp)
|
|
|
|
|
|
def metadataParser() -> dict:
|
|
"""Used by other modules to access the local parser object"""
|
|
return plugins_metadata_parser
|
|
|
|
|
|
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 plugin_id, parser in findPlugins(pluginpath):
|
|
if parser is None:
|
|
continue
|
|
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 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 "==" in dep:
|
|
name, version_required = dep.split("==")
|
|
else:
|
|
name, version_required = dep, None
|
|
result[name.strip()] = version_required.strip() if version_required else None
|
|
|
|
return result
|
|
|
|
|
|
def pluginMetadata(packageName: str, fct: str) -> str:
|
|
"""fetch metadata from a plugin - use values from metadata.txt"""
|
|
try:
|
|
return plugins_metadata_parser[packageName].get("general", fct)
|
|
except Exception:
|
|
return "__error__"
|
|
|
|
|
|
def loadPlugin(packageName: str) -> bool:
|
|
"""load plugin's package"""
|
|
|
|
try:
|
|
__import__(packageName)
|
|
return True
|
|
except:
|
|
pass # continue...
|
|
|
|
# snake in the grass, we know it's there
|
|
sys.path_importer_cache.clear()
|
|
|
|
# retry
|
|
try:
|
|
__import__(packageName)
|
|
return True
|
|
except:
|
|
msg = QCoreApplication.translate("Python", "Couldn't load plugin '{0}'").format(
|
|
packageName
|
|
)
|
|
showException(
|
|
sys.exc_info()[0],
|
|
sys.exc_info()[1],
|
|
sys.exc_info()[2],
|
|
msg,
|
|
messagebar=True,
|
|
level=Qgis.MessageLevel.Critical,
|
|
)
|
|
return False
|
|
|
|
|
|
def _startPlugin(packageName: str) -> bool:
|
|
"""initializes a plugin, but does not load GUI"""
|
|
global plugins, active_plugins, iface, plugin_times
|
|
|
|
if packageName in active_plugins:
|
|
return False
|
|
|
|
if packageName not in sys.modules:
|
|
return False
|
|
|
|
package = sys.modules[packageName]
|
|
|
|
# create an instance of the plugin
|
|
try:
|
|
plugins[packageName] = package.classFactory(iface)
|
|
except:
|
|
_unloadPluginModules(packageName)
|
|
errMsg = QCoreApplication.translate(
|
|
"Python", "Couldn't load plugin '{0}'"
|
|
).format(packageName)
|
|
msg = QCoreApplication.translate(
|
|
"Python", "{0} due to an error when calling its classFactory() method"
|
|
).format(errMsg)
|
|
showException(
|
|
sys.exc_info()[0],
|
|
sys.exc_info()[1],
|
|
sys.exc_info()[2],
|
|
msg,
|
|
messagebar=True,
|
|
level=Qgis.MessageLevel.Critical,
|
|
)
|
|
return False
|
|
return True
|
|
|
|
|
|
def _addToActivePlugins(packageName: str, duration: int):
|
|
"""Adds a plugin to the list of active plugins"""
|
|
active_plugins.append(packageName)
|
|
plugin_times[packageName] = f"{duration:02f}s"
|
|
|
|
|
|
def startPlugin(packageName: str) -> bool:
|
|
"""initialize the plugin"""
|
|
global plugins, active_plugins, iface, plugin_times
|
|
start = time.process_time()
|
|
if not _startPlugin(packageName):
|
|
return False
|
|
|
|
# initGui
|
|
try:
|
|
plugins[packageName].initGui()
|
|
except:
|
|
del plugins[packageName]
|
|
_unloadPluginModules(packageName)
|
|
errMsg = QCoreApplication.translate(
|
|
"Python", "Couldn't load plugin '{0}'"
|
|
).format(packageName)
|
|
msg = QCoreApplication.translate(
|
|
"Python", "{0} due to an error when calling its initGui() method"
|
|
).format(errMsg)
|
|
showException(
|
|
sys.exc_info()[0],
|
|
sys.exc_info()[1],
|
|
sys.exc_info()[2],
|
|
msg,
|
|
messagebar=True,
|
|
level=Qgis.MessageLevel.Critical,
|
|
)
|
|
return False
|
|
|
|
end = time.process_time()
|
|
_addToActivePlugins(packageName, end - start)
|
|
return True
|
|
|
|
|
|
def startProcessingPlugin(packageName: str) -> bool:
|
|
"""initialize only the Processing components of a plugin"""
|
|
global plugins, active_plugins, iface, plugin_times
|
|
start = time.process_time()
|
|
if not _startPlugin(packageName):
|
|
return False
|
|
|
|
errMsg = QCoreApplication.translate("Python", "Couldn't load plugin '{0}'").format(
|
|
packageName
|
|
)
|
|
if not hasattr(plugins[packageName], "initProcessing"):
|
|
del plugins[packageName]
|
|
_unloadPluginModules(packageName)
|
|
msg = QCoreApplication.translate(
|
|
"Python", "{0} - plugin has no initProcessing() method"
|
|
).format(errMsg)
|
|
showException(
|
|
sys.exc_info()[0],
|
|
sys.exc_info()[1],
|
|
sys.exc_info()[2],
|
|
msg,
|
|
messagebar=True,
|
|
level=Qgis.MessageLevel.Critical,
|
|
)
|
|
return False
|
|
|
|
# initProcessing
|
|
try:
|
|
plugins[packageName].initProcessing()
|
|
except:
|
|
del plugins[packageName]
|
|
_unloadPluginModules(packageName)
|
|
msg = QCoreApplication.translate(
|
|
"Python", "{0} due to an error when calling its initProcessing() method"
|
|
).format(errMsg)
|
|
showException(
|
|
sys.exc_info()[0],
|
|
sys.exc_info()[1],
|
|
sys.exc_info()[2],
|
|
msg,
|
|
messagebar=True,
|
|
)
|
|
return False
|
|
|
|
end = time.process_time()
|
|
_addToActivePlugins(packageName, end - start)
|
|
|
|
return True
|
|
|
|
|
|
def finalizeProcessingStartup() -> bool:
|
|
"""
|
|
Finalizes the startup of the Processing plugin
|
|
|
|
This should only be called after the startProcessingPlugin() method has been called
|
|
for every installed and enabled plugin.
|
|
"""
|
|
global plugins, active_plugins, iface, plugin_times
|
|
if "processing" not in plugins:
|
|
return False
|
|
|
|
try:
|
|
plugins["processing"].finalizeStartup()
|
|
except:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def canUninstallPlugin(packageName: str) -> bool:
|
|
"""confirm that the plugin can be uninstalled"""
|
|
global plugins, active_plugins
|
|
|
|
if packageName not in plugins:
|
|
return False
|
|
if packageName not in active_plugins:
|
|
return False
|
|
|
|
try:
|
|
metadata = plugins[packageName]
|
|
if "canBeUninstalled" not in dir(metadata):
|
|
return True
|
|
return bool(metadata.canBeUninstalled())
|
|
except:
|
|
msg = "Error calling " + packageName + ".canBeUninstalled"
|
|
showException(
|
|
sys.exc_info()[0],
|
|
sys.exc_info()[1],
|
|
sys.exc_info()[2],
|
|
msg,
|
|
messagebar=True,
|
|
)
|
|
return True
|
|
|
|
|
|
def unloadPlugin(packageName: str) -> bool:
|
|
"""unload and delete plugin!"""
|
|
global plugins, active_plugins
|
|
|
|
if packageName not in plugins:
|
|
return False
|
|
if packageName not in active_plugins:
|
|
return False
|
|
|
|
try:
|
|
plugins[packageName].unload()
|
|
del plugins[packageName]
|
|
active_plugins.remove(packageName)
|
|
_unloadPluginModules(packageName)
|
|
return True
|
|
except Exception as e:
|
|
msg = QCoreApplication.translate(
|
|
"Python", "Error while unloading plugin {0}"
|
|
).format(packageName)
|
|
showException(
|
|
sys.exc_info()[0],
|
|
sys.exc_info()[1],
|
|
sys.exc_info()[2],
|
|
msg,
|
|
messagebar=True,
|
|
)
|
|
return False
|
|
|
|
|
|
def _unloadPluginModules(packageName: str):
|
|
"""unload plugin package with all its modules (files)"""
|
|
global _plugin_modules
|
|
|
|
if packageName not in _plugin_modules:
|
|
return
|
|
|
|
mods = _plugin_modules[packageName]
|
|
|
|
for mod in mods:
|
|
if mod not in sys.modules:
|
|
continue
|
|
|
|
# if it looks like a Qt resource file, try to do a cleanup
|
|
# otherwise we might experience a segfault next time the plugin is loaded
|
|
# because Qt will try to access invalid plugin resource data
|
|
try:
|
|
if hasattr(sys.modules[mod], "qCleanupResources"):
|
|
sys.modules[mod].qCleanupResources()
|
|
except:
|
|
# Print stack trace for debug
|
|
qDebug("qCleanupResources error:\n%s" % traceback.format_exc())
|
|
|
|
# try removing path
|
|
if hasattr(sys.modules[mod], "__path__"):
|
|
for path in sys.modules[mod].__path__:
|
|
try:
|
|
sys.path.remove(path)
|
|
except ValueError:
|
|
# Discard if path is not there
|
|
pass
|
|
|
|
# try to remove the module from python
|
|
try:
|
|
del sys.modules[mod]
|
|
except:
|
|
qDebug("Error when removing module:\n%s" % traceback.format_exc())
|
|
# remove the plugin entry
|
|
del _plugin_modules[packageName]
|
|
|
|
|
|
def isPluginLoaded(packageName: str) -> bool:
|
|
"""find out whether a plugin is active (i.e. has been started)"""
|
|
global plugins, active_plugins
|
|
|
|
if packageName not in plugins:
|
|
return False
|
|
return packageName in active_plugins
|
|
|
|
|
|
def reloadPlugin(packageName: str) -> bool:
|
|
"""unload and start again a plugin"""
|
|
global active_plugins
|
|
if packageName not in active_plugins:
|
|
return False # it's not active
|
|
|
|
unloadPlugin(packageName)
|
|
loadPlugin(packageName)
|
|
started = startPlugin(packageName)
|
|
return started
|
|
|
|
|
|
def showPluginHelp(packageName: str = None, filename: str = "index", section: str = ""):
|
|
"""Open help in the user's html browser. The help file should be named index-ll_CC.html or index-ll.html or index.html.
|
|
|
|
:param str packageName: name of package folder, if None it's using the current file package. Defaults to None. Optional.
|
|
:param str filename: name of file to open. It can be a path like 'doc/index' for example. Defaults to 'index'.
|
|
:param str section: URL path to open. Defaults to empty string.
|
|
"""
|
|
try:
|
|
source = ""
|
|
if packageName is None:
|
|
import inspect
|
|
|
|
source = inspect.currentframe().f_back.f_code.co_filename
|
|
else:
|
|
source = sys.modules[packageName].__file__
|
|
except:
|
|
return
|
|
path = os.path.dirname(source)
|
|
locale = str(QLocale().name())
|
|
helpfile = os.path.join(path, filename + "-" + locale + ".html")
|
|
if not os.path.exists(helpfile):
|
|
helpfile = os.path.join(path, filename + "-" + locale.split("_")[0] + ".html")
|
|
if not os.path.exists(helpfile):
|
|
helpfile = os.path.join(path, filename + "-en.html")
|
|
if not os.path.exists(helpfile):
|
|
helpfile = os.path.join(path, filename + "-en_US.html")
|
|
if not os.path.exists(helpfile):
|
|
helpfile = os.path.join(path, filename + ".html")
|
|
if os.path.exists(helpfile):
|
|
url = "file://" + helpfile
|
|
if section != "":
|
|
url = url + "#" + section
|
|
QDesktopServices.openUrl(QUrl.fromLocalFile(url))
|
|
|
|
|
|
def pluginDirectory(packageName: str) -> str:
|
|
"""return directory where the plugin resides. Plugin must be loaded already"""
|
|
return os.path.dirname(sys.modules[packageName].__file__)
|
|
|
|
|
|
def reloadProjectMacros():
|
|
# unload old macros
|
|
unloadProjectMacros()
|
|
|
|
from qgis.core import QgsProject
|
|
|
|
code, ok = QgsProject.instance().readEntry("Macros", "/pythonCode")
|
|
if not ok or not code or code == "":
|
|
return
|
|
|
|
# create a new empty python module
|
|
import importlib
|
|
|
|
mod = importlib.util.module_from_spec(
|
|
importlib.machinery.ModuleSpec("proj_macros_mod", None)
|
|
)
|
|
|
|
# set the module code and store it sys.modules
|
|
exec(str(code), mod.__dict__)
|
|
sys.modules["proj_macros_mod"] = mod
|
|
|
|
# load new macros
|
|
openProjectMacro()
|
|
|
|
|
|
def unloadProjectMacros():
|
|
if "proj_macros_mod" not in sys.modules:
|
|
return
|
|
# unload old macros
|
|
closeProjectMacro()
|
|
# destroy the reference to the module
|
|
del sys.modules["proj_macros_mod"]
|
|
|
|
|
|
def openProjectMacro():
|
|
if "proj_macros_mod" not in sys.modules:
|
|
return
|
|
mod = sys.modules["proj_macros_mod"]
|
|
if hasattr(mod, "openProject"):
|
|
mod.openProject()
|
|
|
|
|
|
def saveProjectMacro():
|
|
if "proj_macros_mod" not in sys.modules:
|
|
return
|
|
mod = sys.modules["proj_macros_mod"]
|
|
if hasattr(mod, "saveProject"):
|
|
mod.saveProject()
|
|
|
|
|
|
def closeProjectMacro():
|
|
if "proj_macros_mod" not in sys.modules:
|
|
return
|
|
mod = sys.modules["proj_macros_mod"]
|
|
if hasattr(mod, "closeProject"):
|
|
mod.closeProject()
|
|
|
|
|
|
#######################
|
|
|
|
|
|
def _list_project_expression_functions():
|
|
"""Get a list of expression functions stored in the current project"""
|
|
import ast
|
|
from qgis.core import QgsProject
|
|
|
|
functions = []
|
|
project_functions, ok = QgsProject.instance().readEntry(
|
|
"ExpressionFunctions", "/pythonCode"
|
|
)
|
|
if ok and project_functions:
|
|
code = ast.parse(project_functions)
|
|
|
|
for e in code.body:
|
|
if isinstance(e, ast.FunctionDef):
|
|
for d in e.decorator_list:
|
|
if d.func.id == "qgsfunction":
|
|
functions.append(e.name)
|
|
|
|
return functions
|
|
|
|
|
|
def clean_project_expression_functions():
|
|
"""
|
|
Unload expression functions from current project
|
|
and reload user expressions from profile folder
|
|
to avoid any potential overwrite from the
|
|
unloaded project functions
|
|
"""
|
|
project_functions = _list_project_expression_functions()
|
|
if project_functions:
|
|
from qgis.core import QgsExpression
|
|
|
|
for function in project_functions:
|
|
QgsExpression.unregisterFunction(function)
|
|
|
|
# Reload user expressions
|
|
from qgis.core import QgsApplication
|
|
import expressions
|
|
|
|
userpythonhome = os.path.join(QgsApplication.qgisSettingsDirPath(), "python")
|
|
expressionspath = os.path.join(userpythonhome, "expressions")
|
|
expressions.reload(expressionspath)
|
|
|
|
|
|
#######################
|
|
# SERVER PLUGINS
|
|
#
|
|
# TODO: move into server_utils.py ?
|
|
|
|
# list of plugin paths. it gets filled in by the QGIS python library
|
|
server_plugin_paths = []
|
|
|
|
# dictionary of plugins
|
|
server_plugins = {}
|
|
|
|
# list of active (started) plugins
|
|
server_active_plugins = []
|
|
|
|
|
|
# initialize 'serverIface' object
|
|
serverIface = None
|
|
|
|
|
|
def initServerInterface(pointer):
|
|
from qgis.server import QgsServerInterface
|
|
from sip import wrapinstance
|
|
|
|
sys.excepthook = sys.__excepthook__
|
|
global serverIface
|
|
serverIface = wrapinstance(pointer, QgsServerInterface)
|
|
|
|
|
|
def startServerPlugin(packageName: str):
|
|
"""initialize the plugin"""
|
|
global server_plugins, server_active_plugins, serverIface
|
|
|
|
if packageName in server_active_plugins:
|
|
return False
|
|
if packageName not in sys.modules:
|
|
return False
|
|
|
|
package = sys.modules[packageName]
|
|
|
|
errMsg = QCoreApplication.translate(
|
|
"Python", "Couldn't load server plugin {0}"
|
|
).format(packageName)
|
|
|
|
# create an instance of the plugin
|
|
try:
|
|
server_plugins[packageName] = package.serverClassFactory(serverIface)
|
|
except:
|
|
_unloadPluginModules(packageName)
|
|
msg = QCoreApplication.translate(
|
|
"Python", "{0} due to an error when calling its serverClassFactory() method"
|
|
).format(errMsg)
|
|
showException(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], msg)
|
|
return False
|
|
|
|
# add to active plugins
|
|
server_active_plugins.append(packageName)
|
|
return True
|
|
|
|
|
|
def spatialite_connect(*args, **kwargs):
|
|
"""returns a dbapi2.Connection to a SpatiaLite db
|
|
using the "mod_spatialite" extension (python3)"""
|
|
import sqlite3
|
|
import re
|
|
|
|
def fcnRegexp(pattern, string):
|
|
result = re.search(pattern, string)
|
|
return True if result else False
|
|
|
|
con = sqlite3.dbapi2.connect(*args, **kwargs)
|
|
con.enable_load_extension(True)
|
|
cur = con.cursor()
|
|
libs = [
|
|
# SpatiaLite >= 4.2 and Sqlite >= 3.7.17, should work on all platforms
|
|
("mod_spatialite", "sqlite3_modspatialite_init"),
|
|
# SpatiaLite >= 4.2 and Sqlite < 3.7.17 (Travis)
|
|
("mod_spatialite.so", "sqlite3_modspatialite_init"),
|
|
# SpatiaLite < 4.2 (linux)
|
|
("libspatialite.so", "sqlite3_extension_init"),
|
|
]
|
|
found = False
|
|
for lib, entry_point in libs:
|
|
try:
|
|
cur.execute(f"select load_extension('{lib}', '{entry_point}')")
|
|
except sqlite3.OperationalError:
|
|
continue
|
|
else:
|
|
found = True
|
|
break
|
|
if not found:
|
|
raise RuntimeError("Cannot find any suitable spatialite module")
|
|
if any([".gpkg" in arg for arg in args]):
|
|
try:
|
|
cur.execute("SELECT EnableGpkgAmphibiousMode()")
|
|
except (sqlite3.Error, sqlite3.DatabaseError, sqlite3.NotSupportedError):
|
|
QgsMessageLog.logMessage(
|
|
"warning:{}".format("Could not enable geopackage amphibious mode"),
|
|
QCoreApplication.translate("Python", "Python warning"),
|
|
)
|
|
|
|
cur.close()
|
|
con.enable_load_extension(False)
|
|
con.create_function("regexp", 2, fcnRegexp)
|
|
return con
|
|
|
|
|
|
class OverrideCursor:
|
|
"""
|
|
Executes a code block with a different cursor set and makes sure the cursor
|
|
is restored even if exceptions are raised or an intermediate ``return``
|
|
statement is hit.
|
|
|
|
Example:
|
|
```
|
|
with OverrideCursor(Qt.WaitCursor):
|
|
do_a_slow(operation)
|
|
```
|
|
"""
|
|
|
|
def __init__(self, cursor):
|
|
self.cursor = cursor
|
|
|
|
def __enter__(self):
|
|
QApplication.setOverrideCursor(self.cursor)
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
QApplication.restoreOverrideCursor()
|
|
return exc_type is None
|
|
|
|
|
|
#######################
|
|
# IMPORT wrapper
|
|
|
|
if os.name == "nt" and sys.version_info < (3, 8):
|
|
import ctypes
|
|
from ctypes import windll, wintypes
|
|
|
|
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
|
|
_hasAddDllDirectory = hasattr(kernel32, "AddDllDirectory")
|
|
if _hasAddDllDirectory:
|
|
_import_path = os.environ["PATH"]
|
|
_import_paths = {}
|
|
|
|
def _errcheck_zero(result, func, args):
|
|
if not result:
|
|
raise ctypes.WinError(ctypes.get_last_error())
|
|
return args
|
|
|
|
DLL_DIRECTORY_COOKIE = wintypes.LPVOID
|
|
|
|
_AddDllDirectory = kernel32.AddDllDirectory
|
|
_AddDllDirectory.errcheck = _errcheck_zero
|
|
_AddDllDirectory.restype = DLL_DIRECTORY_COOKIE
|
|
_AddDllDirectory.argtypes = (wintypes.LPCWSTR,)
|
|
|
|
_RemoveDllDirectory = kernel32.RemoveDllDirectory
|
|
_RemoveDllDirectory.errcheck = _errcheck_zero
|
|
_RemoveDllDirectory.argtypes = (DLL_DIRECTORY_COOKIE,)
|
|
|
|
_uses_builtins = True
|
|
try:
|
|
import builtins
|
|
|
|
_builtin_import = builtins.__import__
|
|
except AttributeError:
|
|
_uses_builtins = False
|
|
import __builtin__
|
|
|
|
_builtin_import = __builtin__.__import__
|
|
|
|
_plugin_modules = {}
|
|
|
|
|
|
def _import(name, globals={}, locals={}, fromlist=[], level=None):
|
|
"""
|
|
Wrapper around builtin import that keeps track of loaded plugin modules and blocks
|
|
certain unsafe imports
|
|
"""
|
|
if level is None:
|
|
level = 0
|
|
|
|
if "PyQt4" in name:
|
|
msg = (
|
|
"PyQt4 classes cannot be imported in QGIS 3.x.\n"
|
|
"Use {} or preferably the version independent {} import instead.".format(
|
|
name.replace("PyQt4", "PyQt5"), name.replace("PyQt4", "qgis.PyQt")
|
|
)
|
|
)
|
|
raise ImportError(msg)
|
|
qt_version = int(QT_VERSION_STR.split(".")[0])
|
|
if qt_version == 5 and "PyQt6" in name:
|
|
msg = (
|
|
"PyQt6 classes cannot be imported in a QGIS build based on Qt5.\n"
|
|
"Use {} or preferably the version independent {} import instead (where available).".format(
|
|
name.replace("PyQt6", "PyQt5"), name.replace("PyQt6", "qgis.PyQt")
|
|
)
|
|
)
|
|
raise ImportError(msg)
|
|
elif qt_version == 6 and "PyQt5" in name:
|
|
msg = (
|
|
"PyQt5 classes cannot be imported in a QGIS build based on Qt6.\n"
|
|
"Use {} or preferably the version independent {} import instead (where available).".format(
|
|
name.replace("PyQt5", "PyQt6"), name.replace("PyQt5", "qgis.PyQt")
|
|
)
|
|
)
|
|
raise ImportError(msg)
|
|
|
|
if os.name == "nt" and sys.version_info < (3, 8):
|
|
global _hasAddDllDirectory
|
|
if _hasAddDllDirectory:
|
|
global _import_path
|
|
global _import_paths
|
|
|
|
old_path = _import_path
|
|
new_path = os.environ["PATH"]
|
|
if old_path != new_path:
|
|
global _AddDllDirectory
|
|
global _RemoveDllDirectory
|
|
|
|
for p in set(new_path.split(";")) - set(old_path.split(";")):
|
|
if p is not None and p not in _import_path and os.path.isdir(p):
|
|
_import_paths[p] = _AddDllDirectory(p)
|
|
|
|
for p in set(old_path.split(";")) - set(new_path.split(";")):
|
|
if p in _import_paths:
|
|
_RemoveDllDirectory(_import_paths.pop(p))
|
|
|
|
_import_path = new_path
|
|
|
|
mod = _builtin_import(name, globals, locals, fromlist, level)
|
|
|
|
if mod and getattr(mod, "__file__", None):
|
|
module_name = mod.__name__ if fromlist else name
|
|
package_name = module_name.split(".")[0]
|
|
# check whether the module belongs to one of our plugins
|
|
if package_name in available_plugins:
|
|
if package_name not in _plugin_modules:
|
|
_plugin_modules[package_name] = set()
|
|
_plugin_modules[package_name].add(module_name)
|
|
# check the fromlist for additional modules (from X import Y,Z)
|
|
if fromlist:
|
|
for fromitem in fromlist:
|
|
frmod = module_name + "." + fromitem
|
|
if frmod in sys.modules:
|
|
_plugin_modules[package_name].add(frmod)
|
|
|
|
return mod
|
|
|
|
|
|
if not os.environ.get("QGIS_NO_OVERRIDE_IMPORT"):
|
|
if _uses_builtins:
|
|
builtins.__import__ = _import
|
|
else:
|
|
__builtin__.__import__ = _import
|
|
|
|
|
|
def processing_algorithm_from_script(filepath: str):
|
|
"""
|
|
Tries to import a Python processing algorithm from given file, and returns an instance
|
|
of the algorithm.
|
|
|
|
Warning -- this ALSO execs the file as a script, so treat with caution!
|
|
"""
|
|
import sys
|
|
import inspect
|
|
from qgis.processing import alg
|
|
|
|
filename = filepath.replace("\\\\", "/")
|
|
dirname = os.path.dirname(filename)
|
|
|
|
# Append the directory of the file to the path and set __file__ to the filename
|
|
added_script_dir_to_path = False
|
|
if dirname not in sys.path:
|
|
sys.path.append(dirname)
|
|
added_script_dir_to_path = True
|
|
|
|
alg_instance = None
|
|
|
|
try:
|
|
from qgis.core import (
|
|
QgsApplication,
|
|
QgsProcessingAlgorithm,
|
|
QgsProcessingFeatureBasedAlgorithm,
|
|
)
|
|
|
|
_locals = {"__file__": filename}
|
|
|
|
with open(filename.encode(sys.getfilesystemencoding())) as input_file:
|
|
code_object = compile(input_file.read(), filename, "exec")
|
|
exec(code_object, _locals)
|
|
alg_instance = None
|
|
try:
|
|
alg_instance = alg.instances.pop().createInstance()
|
|
except IndexError:
|
|
for name, attr in _locals.items():
|
|
if (
|
|
inspect.isclass(attr)
|
|
and issubclass(
|
|
attr,
|
|
(QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm),
|
|
)
|
|
and attr.__name__
|
|
not in (
|
|
"QgsProcessingAlgorithm",
|
|
"QgsProcessingFeatureBasedAlgorithm",
|
|
)
|
|
):
|
|
alg_instance = attr()
|
|
break
|
|
|
|
except ImportError:
|
|
pass
|
|
|
|
if alg_instance:
|
|
script_provider = QgsApplication.processingRegistry().providerById("script")
|
|
alg_instance.setProvider(script_provider)
|
|
alg_instance.initAlgorithm()
|
|
|
|
# gracefully clean up, restore previous system paths
|
|
|
|
if added_script_dir_to_path:
|
|
try:
|
|
sys.path.remove(dirname)
|
|
except ValueError:
|
|
pass
|
|
|
|
return alg_instance
|
|
|
|
|
|
def import_script_algorithm(filepath: str) -> Optional[str]:
|
|
"""
|
|
Imports a script algorithm from given file to the processing script provider, and returns the
|
|
ID of the imported algorithm.
|
|
|
|
Warning -- this ALSO execs the file as a script, so treat with caution!
|
|
"""
|
|
alg_instance = processing_algorithm_from_script(filepath)
|
|
if alg_instance:
|
|
from qgis.core import QgsApplication
|
|
|
|
script_provider = QgsApplication.processingRegistry().providerById("script")
|
|
script_provider.add_algorithm_class(type(alg_instance))
|
|
return alg_instance.id()
|
|
|
|
return None
|
|
|
|
|
|
def run_script_from_file(filepath: str):
|
|
"""
|
|
Runs a Python script from a given file. Supports loading processing scripts.
|
|
:param filepath: The .py file to load.
|
|
"""
|
|
try:
|
|
from qgis.processing import execAlgorithmDialog
|
|
except ImportError:
|
|
return
|
|
|
|
# Despite the misleading name, processing_algorithm_from_script will ALWAYS
|
|
# execute the script from the file!
|
|
alg_instance = processing_algorithm_from_script(filepath)
|
|
if alg_instance:
|
|
execAlgorithmDialog(alg_instance)
|