QGIS/python/utils.py

671 lines
20 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2012-10-04 19:33:47 +02:00
"""
***************************************************************************
utils.py
---------------------
Date : November 2009
Copyright : (C) 2009 by Martin Dobias
2012-10-08 00:29:13 +02:00
Email : wonder dot sk at gmail dot com
2012-10-04 19:33:47 +02:00
***************************************************************************
* *
* 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. *
* *
***************************************************************************
"""
2016-03-14 23:58:06 +01:00
from __future__ import absolute_import
from future import standard_library
standard_library.install_aliases()
from builtins import str
2012-10-04 19:33:47 +02:00
__author__ = 'Martin Dobias'
__date__ = 'November 2009'
__copyright__ = '(C) 2009, Martin Dobias'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
"""
QGIS utilities module
"""
from qgis.PyQt.QtCore import QCoreApplication, QLocale, QThread
2016-04-22 10:38:48 +02:00
from qgis.PyQt.QtWidgets import QPushButton, QApplication
from qgis.core import Qgis, QgsExpression, QgsMessageLog, qgsfunction, QgsMessageOutput, QgsWkbTypes, QgsApplication
from qgis.gui import QgsMessageBar
import sys
import traceback
import glob
import os.path
2016-03-14 23:58:06 +01:00
try:
import configparser
except ImportError:
import ConfigParser as configparser
import warnings
2013-06-09 10:56:35 +02:00
import codecs
2014-09-18 09:38:52 +10:00
import time
import functools
2017-03-04 19:41:23 +01:00
import builtins
builtins.__dict__['unicode'] = str
builtins.__dict__['basestring'] = str
builtins.__dict__['long'] = int
builtins.__dict__['Set'] = set
2014-11-29 23:18:18 +10:00
# ######################
# ERROR HANDLING
warnings.simplefilter('default')
warnings.filterwarnings("ignore", "the sets module is deprecated")
2014-11-29 23:18:18 +10:00
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
2014-11-29 23:18:18 +10:00
QgsMessageLog.logMessage(
u"warning:{}\ntraceback:{}".format(warnings.formatwarning(message, category, decoded_filename, lineno), stk),
2014-11-29 23:18:18 +10:00
QCoreApplication.translate("Python", "Python warning")
)
warnings.showwarning = showWarning
2014-11-29 23:18:18 +10:00
def showException(type, value, tb, msg, messagebar=False):
if msg is None:
2016-01-24 20:16:19 +01:00
msg = QCoreApplication.translate('Python', 'An error has occurred while executing Python code:')
2014-11-29 23:18:18 +10:00
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)
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
2017-03-04 19:41:23 +01:00
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, QgsMessageBar.WARNING)
2015-10-02 21:06:19 +02:00
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:
iface.messageBar().popWidget()
if msg is None:
2016-01-24 20:16:19 +01:00
msg = QCoreApplication.translate('Python', 'An error has occurred while executing Python code:')
# TODO Move this to a template HTML file
txt = u'''<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,
2015-10-02 21:06:19 +02:00
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,
2015-10-02 21:06:19 +02:00
pypath_label=pypath_label,
pypath=u"".join(u"<li>{}</li>".format(path) for path in sys.path))
txt = txt.replace(' ', '&nbsp; ') # preserve whitespaces for nicer output
dlg = QgsMessageOutput.createMessageOutput()
dlg.setTitle(msg)
dlg.setMessage(txt, QgsMessageOutput.MessageHtml)
dlg.showMessage()
2014-11-29 23:18:18 +10:00
def qgis_excepthook(type, value, tb):
# detect if running in the main thread
2017-03-04 19:41:23 +01:00
in_main_thread = QThread.currentThread() == QgsApplication.instance().thread()
# only use messagebar if running in main thread - otherwise it will crash!
showException(type, value, tb, None, messagebar=in_main_thread)
2014-11-29 23:18:18 +10:00
def installErrorHook():
2014-11-29 23:18:18 +10:00
sys.excepthook = qgis_excepthook
def uninstallErrorHook():
2014-11-29 23:18:18 +10:00
sys.excepthook = sys.__excepthook__
2017-03-04 19:41:23 +01:00
# install error hook() on module load
installErrorHook()
# initialize 'iface' object
iface = None
2014-11-29 23:18:18 +10:00
def initInterface(pointer):
2014-11-29 23:18:18 +10:00
from qgis.gui import QgisInterface
from sip import wrapinstance
global iface
iface = wrapinstance(pointer, QgisInterface)
2017-03-04 19:41:23 +01:00
#######################
# PLUGINS
# list of plugin paths. it gets filled in by the QGIS python library
plugin_paths = []
# dictionary of plugins
plugins = {}
2014-09-18 09:38:52 +10:00
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 = {}
2014-11-29 23:18:18 +10:00
def findPlugins(path):
2014-11-29 23:18:18 +10:00
""" 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
2014-11-29 23:18:18 +10:00
metadataFile = os.path.join(plugin, 'metadata.txt')
if not os.path.exists(metadataFile):
continue
2016-03-14 23:58:06 +01:00
cp = configparser.ConfigParser()
2013-06-09 10:56:35 +02:00
2014-11-29 23:18:18 +10:00
try:
with codecs.open(metadataFile, "r", "utf8") as f:
cp.read_file(f)
2014-11-29 23:18:18 +10:00
except:
cp = None
2014-11-29 23:18:18 +10:00
pluginName = os.path.basename(plugin)
yield (pluginName, cp)
def updateAvailablePlugins():
2014-11-29 23:18:18 +10:00
""" Go through the plugin_paths list and find out what plugins are available. """
# merge the lists
plugins = []
metadata_parser = {}
for pluginpath in plugin_paths:
for pluginName, parser in findPlugins(pluginpath):
if parser is None:
continue
2014-11-29 23:18:18 +10:00
if pluginName not in plugins:
plugins.append(pluginName)
metadata_parser[pluginName] = parser
global available_plugins
available_plugins = plugins
global plugins_metadata_parser
plugins_metadata_parser = metadata_parser
def pluginMetadata(packageName, fct):
2014-11-29 23:18:18 +10:00
""" 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):
2014-11-29 23:18:18 +10:00
""" load plugin's package """
2014-11-29 23:18:18 +10:00
try:
__import__(packageName)
return True
except:
pass # continue...
2014-11-29 23:18:18 +10:00
# snake in the grass, we know it's there
sys.path_importer_cache.clear()
2014-11-29 23:18:18 +10:00
# retry
try:
__import__(packageName)
return True
except:
2017-03-04 16:23:36 +01:00
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)
2014-11-29 23:18:18 +10:00
return False
def startPlugin(packageName):
2014-11-29 23:18:18 +10:00
""" initialize the plugin """
global plugins, active_plugins, iface, plugin_times
if packageName in active_plugins:
return False
2017-03-04 16:23:36 +01:00
if packageName not in sys.modules:
return False
2014-11-29 23:18:18 +10:00
package = sys.modules[packageName]
2017-03-04 16:23:36 +01:00
errMsg = QCoreApplication.translate("Python", "Couldn't load plugin '{0}'").format(packageName)
2014-11-29 23:18:18 +10:00
start = time.clock()
# create an instance of the plugin
try:
plugins[packageName] = package.classFactory(iface)
except:
_unloadPluginModules(packageName)
2017-03-04 16:23:36 +01:00
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)
2014-11-29 23:18:18 +10:00
return False
2014-11-29 23:18:18 +10:00
# initGui
try:
plugins[packageName].initGui()
except:
del plugins[packageName]
_unloadPluginModules(packageName)
2017-03-04 16:23:36 +01:00
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)
2014-11-29 23:18:18 +10:00
return False
2014-11-29 23:18:18 +10:00
# add to active plugins
active_plugins.append(packageName)
end = time.clock()
plugin_times[packageName] = "{0:02f}s".format(end - start)
2014-11-29 23:18:18 +10:00
return True
def canUninstallPlugin(packageName):
2014-11-29 23:18:18 +10:00
""" 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
2014-11-29 23:18:18 +10:00
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)
2014-11-29 23:18:18 +10:00
return True
def unloadPlugin(packageName):
2014-11-29 23:18:18 +10:00
""" unload and delete plugin! """
global plugins, active_plugins
2012-12-10 00:12:07 +01:00
if packageName not in plugins:
return False
if packageName not in active_plugins:
return False
2014-11-29 23:18:18 +10:00
try:
plugins[packageName].unload()
del plugins[packageName]
active_plugins.remove(packageName)
_unloadPluginModules(packageName)
return True
except Exception as e:
2017-03-04 16:23:36 +01:00
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)
2014-11-29 23:18:18 +10:00
return False
def _unloadPluginModules(packageName):
2014-11-29 23:18:18 +10:00
""" unload plugin package with all its modules (files) """
global _plugin_modules
mods = _plugin_modules[packageName]
for mod in mods:
# 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:
pass
# try to remove the module from python
try:
del sys.modules[mod]
except:
pass
# remove the plugin entry
del _plugin_modules[packageName]
def isPluginLoaded(packageName):
2014-11-29 23:18:18 +10:00
""" find out whether a plugin is active (i.e. has been started) """
global plugins, active_plugins
if packageName not in plugins:
return False
2014-11-29 23:18:18 +10:00
return (packageName in active_plugins)
def reloadPlugin(packageName):
2014-11-29 23:18:18 +10:00
""" unload and start again a plugin """
global active_plugins
if packageName not in active_plugins:
return # it's not active
unloadPlugin(packageName)
loadPlugin(packageName)
startPlugin(packageName)
def showPluginHelp(packageName=None, filename="index", section=""):
""" show a help in the user's html browser. The help file should be named index-ll_CC.html or index-ll.html"""
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
iface.openURL(url, False)
def pluginDirectory(packageName):
2014-11-29 23:18:18 +10:00
""" return directory where the plugin resides. Plugin must be loaded already """
return os.path.dirname(sys.modules[packageName].__file__)
def reloadProjectMacros():
2014-11-29 23:18:18 +10:00
# unload old macros
unloadProjectMacros()
2014-11-29 23:18:18 +10:00
from qgis.core import QgsProject
2014-11-29 23:18:18 +10:00
code, ok = QgsProject.instance().readEntry("Macros", "/pythonCode")
if not ok or not code or code == '':
return
2014-11-29 23:18:18 +10:00
# create a new empty python module
import imp
mod = imp.new_module("proj_macros_mod")
# set the module code and store it sys.modules
2016-03-14 23:58:06 +01:00
exec(str(code), mod.__dict__)
2014-11-29 23:18:18 +10:00
sys.modules["proj_macros_mod"] = mod
# load new macros
openProjectMacro()
def unloadProjectMacros():
2014-11-29 23:18:18 +10:00
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():
2014-11-29 23:18:18 +10:00
if "proj_macros_mod" not in sys.modules:
return
mod = sys.modules["proj_macros_mod"]
if hasattr(mod, 'openProject'):
mod.openProject()
def saveProjectMacro():
2014-11-29 23:18:18 +10:00
if "proj_macros_mod" not in sys.modules:
return
mod = sys.modules["proj_macros_mod"]
if hasattr(mod, 'saveProject'):
mod.saveProject()
def closeProjectMacro():
2014-11-29 23:18:18 +10:00
if "proj_macros_mod" not in sys.modules:
return
mod = sys.modules["proj_macros_mod"]
if hasattr(mod, 'closeProject'):
mod.closeProject()
2014-10-09 15:05:11 +02:00
#######################
# 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
2014-11-29 23:18:18 +10:00
2014-10-09 15:05:11 +02:00
def initServerInterface(pointer):
2014-11-29 23:18:18 +10:00
from qgis.server import QgsServerInterface
from sip import wrapinstance
2015-12-16 15:13:11 +01:00
sys.excepthook = sys.__excepthook__
2014-11-29 23:18:18 +10:00
global serverIface
serverIface = wrapinstance(pointer, QgsServerInterface)
2014-10-09 15:05:11 +02:00
def startServerPlugin(packageName):
2014-11-29 23:18:18 +10:00
""" initialize the plugin """
global server_plugins, server_active_plugins, serverIface
2014-10-09 15:05:11 +02:00
if packageName in server_active_plugins:
return False
if packageName not in sys.modules:
return False
2014-10-09 15:05:11 +02:00
2014-11-29 23:18:18 +10:00
package = sys.modules[packageName]
2014-10-09 15:05:11 +02:00
2017-03-04 16:23:36 +01:00
errMsg = QCoreApplication.translate("Python", "Couldn't load server plugin {0}").format(packageName)
2014-10-09 15:05:11 +02:00
2014-11-29 23:18:18 +10:00
# create an instance of the plugin
try:
server_plugins[packageName] = package.serverClassFactory(serverIface)
except:
_unloadPluginModules(packageName)
msg = QCoreApplication.translate("Python",
2017-03-04 16:23:36 +01:00
"{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)
2014-11-29 23:18:18 +10:00
return False
# add to active plugins
server_active_plugins.append(packageName)
return True
2014-10-09 15:05:11 +02:00
2016-09-01 12:18:45 +02:00
def spatialite_connect(*args, **kwargs):
"""returns a dbapi2.Connection to a spatialite db
either using pyspatialite if it is present
or using the "mod_spatialite" extension (python3)"""
try:
from pyspatialite import dbapi2
except ImportError:
import sqlite3
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("select load_extension('{}', '{}')".format(lib, entry_point))
except sqlite3.OperationalError:
continue
else:
found = True
break
if not found:
raise RuntimeError("Cannot find any suitable spatialite module")
cur.close()
con.enable_load_extension(False)
return con
return dbapi2.connect(*args, **kwargs)
2017-03-04 19:41:23 +01:00
#######################
# IMPORT wrapper
_uses_builtins = True
2016-03-14 23:58:06 +01:00
try:
import builtins
_builtin_import = builtins.__import__
except AttributeError:
_uses_builtins = False
2016-03-14 23:58:06 +01:00
import __builtin__
_builtin_import = __builtin__.__import__
2014-11-29 23:18:18 +10:00
_plugin_modules = {}
def _import(name, globals={}, locals={}, fromlist=[], level=None):
2014-11-29 23:18:18 +10:00
""" wrapper around builtin import that keeps track of loaded plugin modules """
if level is None:
2017-03-03 17:40:13 +01:00
level = 0
2014-11-29 23:18:18 +10:00
mod = _builtin_import(name, globals, locals, fromlist, level)
if mod and '__file__' in mod.__dict__:
module_name = mod.__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 _uses_builtins:
2016-03-14 23:58:06 +01:00
builtins.__import__ = _import
else:
2016-03-14 23:58:06 +01:00
__builtin__.__import__ = _import