1# -*- coding: utf-8 -*-
2
3"""
4***************************************************************************
5    utils.py
6    ---------------------
7    Date                 : November 2009
8    Copyright            : (C) 2009 by Martin Dobias
9    Email                : wonder dot sk at gmail dot com
10***************************************************************************
11*                                                                         *
12*   This program is free software; you can redistribute it and/or modify  *
13*   it under the terms of the GNU General Public License as published by  *
14*   the Free Software Foundation; either version 2 of the License, or     *
15*   (at your option) any later version.                                   *
16*                                                                         *
17***************************************************************************
18"""
19
20__author__ = 'Martin Dobias'
21__date__ = 'November 2009'
22__copyright__ = '(C) 2009, Martin Dobias'
23
24"""
25QGIS utilities module
26
27"""
28from typing import List, Dict, Optional
29
30from qgis.PyQt.QtCore import QCoreApplication, QLocale, QThread, qDebug, QUrl
31from qgis.PyQt.QtGui import QDesktopServices
32from qgis.PyQt.QtWidgets import QPushButton, QApplication
33from qgis.core import Qgis, QgsMessageLog, qgsfunction, QgsMessageOutput
34from qgis.gui import QgsMessageBar
35
36import os
37import sys
38import traceback
39import glob
40import os.path
41import configparser
42import warnings
43import codecs
44import time
45import functools
46
47import builtins
48builtins.__dict__['unicode'] = str
49builtins.__dict__['basestring'] = str
50builtins.__dict__['long'] = int
51builtins.__dict__['Set'] = set
52
53# ######################
54# ERROR HANDLING
55
56warnings.simplefilter('default')
57warnings.filterwarnings("ignore", "the sets module is deprecated")
58
59
60def showWarning(message, category, filename, lineno, file=None, line=None):
61    stk = ""
62    for s in traceback.format_stack()[:-2]:
63        if hasattr(s, 'decode'):
64            stk += s.decode(sys.getfilesystemencoding())
65        else:
66            stk += s
67    if hasattr(filename, 'decode'):
68        decoded_filename = filename.decode(sys.getfilesystemencoding())
69    else:
70        decoded_filename = filename
71    QgsMessageLog.logMessage(
72        u"warning:{}\ntraceback:{}".format(warnings.formatwarning(message, category, decoded_filename, lineno), stk),
73        QCoreApplication.translate("Python", "Python warning")
74    )
75
76
77def showException(type, value, tb, msg, messagebar=False, level=Qgis.Warning):
78    if msg is None:
79        msg = QCoreApplication.translate('Python', 'An error has occurred while executing Python code:')
80
81    logmessage = ''
82    for s in traceback.format_exception(type, value, tb):
83        logmessage += s.decode('utf-8', 'replace') if hasattr(s, 'decode') else s
84
85    title = QCoreApplication.translate('Python', 'Python error')
86    QgsMessageLog.logMessage(logmessage, title, level)
87
88    try:
89        blockingdialog = QApplication.instance().activeModalWidget()
90        window = QApplication.instance().activeWindow()
91    except:
92        blockingdialog = QApplication.activeModalWidget()
93        window = QApplication.activeWindow()
94
95    # Still show the normal blocking dialog in this case for now.
96    if blockingdialog or not window or not messagebar or not iface:
97        open_stack_dialog(type, value, tb, msg)
98        return
99
100    bar = iface.messageBar() if iface else None
101
102    # If it's not the main window see if we can find a message bar to report the error in
103    if not window.objectName() == "QgisApp":
104        widgets = window.findChildren(QgsMessageBar)
105        if widgets:
106            # Grab the first message bar for now
107            bar = widgets[0]
108
109    item = bar.currentItem()
110    if item and item.property("Error") == msg:
111        # Return of we already have a message with the same error message
112        return
113
114    widget = bar.createMessage(title, msg + " " + QCoreApplication.translate("Python", "See message log (Python Error) for more details."))
115    widget.setProperty("Error", msg)
116    stackbutton = QPushButton(QCoreApplication.translate("Python", "Stack trace"), pressed=functools.partial(open_stack_dialog, type, value, tb, msg))
117    button = QPushButton(QCoreApplication.translate("Python", "View message log"), pressed=show_message_log)
118    widget.layout().addWidget(stackbutton)
119    widget.layout().addWidget(button)
120    bar.pushWidget(widget, Qgis.Warning)
121
122
123def show_message_log(pop_error=True):
124    if pop_error:
125        iface.messageBar().popWidget()
126
127    iface.openMessageLog()
128
129
130def open_stack_dialog(type, value, tb, msg, pop_error=True):
131    if pop_error and iface is not None:
132        iface.messageBar().popWidget()
133
134    if msg is None:
135        msg = QCoreApplication.translate('Python', 'An error has occurred while executing Python code:')
136
137    # TODO Move this to a template HTML file
138    txt = u'''<font color="red"><b>{msg}</b></font>
139<br>
140<h3>{main_error}</h3>
141<pre>
142{error}
143</pre>
144<br>
145<b>{version_label}</b> {num}
146<br>
147<b>{qgis_label}</b> {qversion} {qgisrelease}, {devversion}
148<br>
149<h4>{pypath_label}</h4>
150<ul>
151{pypath}
152</ul>'''
153
154    error = ''
155    lst = traceback.format_exception(type, value, tb)
156    for s in lst:
157        error += s.decode('utf-8', 'replace') if hasattr(s, 'decode') else s
158    error = error.replace('\n', '<br>')
159
160    main_error = lst[-1].decode('utf-8', 'replace') if hasattr(lst[-1], 'decode') else lst[-1]
161
162    version_label = QCoreApplication.translate('Python', 'Python version:')
163    qgis_label = QCoreApplication.translate('Python', 'QGIS version:')
164    pypath_label = QCoreApplication.translate('Python', 'Python Path:')
165    txt = txt.format(msg=msg,
166                     main_error=main_error,
167                     error=error,
168                     version_label=version_label,
169                     num=sys.version,
170                     qgis_label=qgis_label,
171                     qversion=Qgis.QGIS_VERSION,
172                     qgisrelease=Qgis.QGIS_RELEASE_NAME,
173                     devversion=Qgis.QGIS_DEV_VERSION,
174                     pypath_label=pypath_label,
175                     pypath=u"".join(u"<li>{}</li>".format(path) for path in sys.path))
176
177    txt = txt.replace('  ', '&nbsp; ')  # preserve whitespaces for nicer output
178
179    dlg = QgsMessageOutput.createMessageOutput()
180    dlg.setTitle(msg)
181    dlg.setMessage(txt, QgsMessageOutput.MessageHtml)
182    dlg.showMessage()
183
184
185def qgis_excepthook(type, value, tb):
186    # detect if running in the main thread
187    in_main_thread = QCoreApplication.instance() is None or QThread.currentThread() == QCoreApplication.instance().thread()
188
189    # only use messagebar if running in main thread - otherwise it will crash!
190    showException(type, value, tb, None, messagebar=in_main_thread)
191
192
193def installErrorHook():
194    """
195    Installs the QGIS application error/warning hook. This causes Python exceptions
196    to be intercepted by the QGIS application and shown in the main window message bar
197    and in custom dialogs.
198
199    Generally you shouldn't call this method - it's automatically called by
200    the QGIS app on startup, and has no use in standalone applications and scripts.
201    """
202    sys.excepthook = qgis_excepthook
203    warnings.showwarning = showWarning
204
205
206def uninstallErrorHook():
207    sys.excepthook = sys.__excepthook__
208
209
210# initialize 'iface' object
211iface = None
212
213
214def initInterface(pointer):
215    from qgis.gui import QgisInterface
216    from sip import wrapinstance
217
218    global iface
219    iface = wrapinstance(pointer, QgisInterface)
220
221
222#######################
223# PLUGINS
224
225# list of plugin paths. it gets filled in by the QGIS python library
226plugin_paths = []
227
228# dictionary of plugins
229plugins = {}
230
231plugin_times = {}
232
233# list of active (started) plugins
234active_plugins = []
235
236# list of plugins in plugin directory and home plugin directory
237available_plugins = []
238
239# dictionary of plugins providing metadata in a text file (metadata.txt)
240# key = plugin package name, value = config parser instance
241plugins_metadata_parser = {}
242
243
244def findPlugins(path):
245    """ for internal use: return list of plugins in given path """
246    for plugin in glob.glob(path + "/*"):
247        if not os.path.isdir(plugin):
248            continue
249        if not os.path.exists(os.path.join(plugin, '__init__.py')):
250            continue
251
252        metadataFile = os.path.join(plugin, 'metadata.txt')
253        if not os.path.exists(metadataFile):
254            continue
255
256        cp = configparser.ConfigParser()
257
258        try:
259            with codecs.open(metadataFile, "r", "utf8") as f:
260                cp.read_file(f)
261        except:
262            cp = None
263
264        pluginName = os.path.basename(plugin)
265        yield (pluginName, cp)
266
267
268def metadataParser() -> dict:
269    """Used by other modules to access the local parser object"""
270    return plugins_metadata_parser
271
272
273def updateAvailablePlugins(sort_by_dependencies=False):
274    """ Go through the plugin_paths list and find out what plugins are available. """
275    # merge the lists
276    plugins = []
277    metadata_parser = {}
278    plugin_name_map = {}
279    for pluginpath in plugin_paths:
280        for plugin_id, parser in findPlugins(pluginpath):
281            if parser is None:
282                continue
283            if plugin_id not in plugins:
284                plugins.append(plugin_id)
285                metadata_parser[plugin_id] = parser
286                plugin_name_map[parser.get('general', 'name')] = plugin_id
287
288    global plugins_metadata_parser
289    plugins_metadata_parser = metadata_parser
290
291    global available_plugins
292    available_plugins = _sortAvailablePlugins(plugins, plugin_name_map) if sort_by_dependencies else plugins
293
294
295def _sortAvailablePlugins(plugins: List[str], plugin_name_map: Dict[str, str]) -> List[str]:
296    """Place dependent plugins after their dependencies
297
298    1. Make a copy of plugins list to modify it.
299    2. Get a plugin dependencies dict.
300    3. Iterate plugins and leave the real work to _move_plugin()
301
302    :param list plugins: List of available plugin ids
303    :param dict plugin_name_map: Map of plugin_names and plugin_ids, because
304                                 get_plugin_deps() only returns plugin names
305    :return: List of plugins sorted by dependencies.
306    """
307    sorted_plugins = plugins.copy()
308    visited_plugins = []
309
310    deps = {}
311    for plugin in plugins:
312        deps[plugin] = [plugin_name_map.get(dep, '') for dep in get_plugin_deps(plugin)]
313
314    for plugin in plugins:
315        _move_plugin(plugin, deps, visited_plugins, sorted_plugins)
316
317    return sorted_plugins
318
319
320def _move_plugin(plugin: str, deps: Dict[str, List[str]], visited: List[str], sorted_plugins: List[str]):
321    """Use recursion to move a plugin after its dependencies in a list of
322    sorted plugins.
323
324    Notes:
325    This function modifies both visited and sorted_plugins lists.
326    This function will not get trapped in circular dependencies. We avoid a
327    maximum recursion error by calling return when revisiting a plugin.
328    Therefore, if a plugin A depends on B and B depends on A, the order will
329    work in one direction (e.g., A depends on B), but the other direction won't
330    be satisfied. After all, a circular plugin dependency should not exist.
331
332    :param str plugin: Id of the plugin that should be moved in sorted_plugins.
333    :param dict deps: Dictionary of plugin dependencies.
334    :param list visited: List of plugins already visited.
335    :param list sorted_plugins: List of plugins to be modified and sorted.
336    """
337    if plugin in visited:
338        return
339    elif plugin not in deps or not deps[plugin]:
340        visited.append(plugin)  # Plugin with no dependencies
341    else:
342        visited.append(plugin)
343
344        # First move dependencies
345        for dep in deps[plugin]:
346            _move_plugin(dep, deps, visited, sorted_plugins)
347
348        # Remove current plugin from sorted
349        # list to get dependency indices
350        max_index = sorted_plugins.index(plugin)
351        sorted_plugins.pop(max_index)
352
353        for dep in deps[plugin]:
354            idx = sorted_plugins.index(dep) + 1 if dep in sorted_plugins else -1
355            max_index = max(idx, max_index)
356
357        # Finally, insert after dependencies
358        sorted_plugins.insert(max_index, plugin)
359
360
361def get_plugin_deps(plugin_id: str) -> Dict[str, Optional[str]]:
362    result = {}
363    try:
364        parser = plugins_metadata_parser[plugin_id]
365        plugin_deps = parser.get('general', 'plugin_dependencies')
366    except (configparser.NoOptionError, configparser.NoSectionError, KeyError):
367        return result
368
369    for dep in plugin_deps.split(','):
370        if dep.find('==') > 0:
371            name, version_required = dep.split('==')
372        else:
373            name = dep
374            version_required = None
375        result[name] = version_required
376    return result
377
378
379def pluginMetadata(packageName: str, fct: str) -> str:
380    """ fetch metadata from a plugin - use values from metadata.txt """
381    try:
382        return plugins_metadata_parser[packageName].get('general', fct)
383    except Exception:
384        return "__error__"
385
386
387def loadPlugin(packageName: str) -> bool:
388    """ load plugin's package """
389
390    try:
391        __import__(packageName)
392        return True
393    except:
394        pass  # continue...
395
396    # snake in the grass, we know it's there
397    sys.path_importer_cache.clear()
398
399    # retry
400    try:
401        __import__(packageName)
402        return True
403    except:
404        msg = QCoreApplication.translate("Python", "Couldn't load plugin '{0}'").format(packageName)
405        showException(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], msg, messagebar=True, level=Qgis.Critical)
406        return False
407
408
409def _startPlugin(packageName: str) -> bool:
410    """ initializes a plugin, but does not load GUI """
411    global plugins, active_plugins, iface, plugin_times
412
413    if packageName in active_plugins:
414        return False
415
416    if packageName not in sys.modules:
417        return False
418
419    package = sys.modules[packageName]
420
421    # create an instance of the plugin
422    try:
423        plugins[packageName] = package.classFactory(iface)
424    except:
425        _unloadPluginModules(packageName)
426        errMsg = QCoreApplication.translate("Python", "Couldn't load plugin '{0}'").format(packageName)
427        msg = QCoreApplication.translate("Python", "{0} due to an error when calling its classFactory() method").format(errMsg)
428        showException(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], msg, messagebar=True, level=Qgis.Critical)
429        return False
430    return True
431
432
433def _addToActivePlugins(packageName: str, duration: int):
434    """ Adds a plugin to the list of active plugins """
435    active_plugins.append(packageName)
436    plugin_times[packageName] = "{0:02f}s".format(duration)
437
438
439def startPlugin(packageName: str) -> bool:
440    """ initialize the plugin """
441    global plugins, active_plugins, iface, plugin_times
442    start = time.process_time()
443    if not _startPlugin(packageName):
444        return False
445
446    # initGui
447    try:
448        plugins[packageName].initGui()
449    except:
450        del plugins[packageName]
451        _unloadPluginModules(packageName)
452        errMsg = QCoreApplication.translate("Python", "Couldn't load plugin '{0}'").format(packageName)
453        msg = QCoreApplication.translate("Python", "{0} due to an error when calling its initGui() method").format(errMsg)
454        showException(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], msg, messagebar=True, level=Qgis.Critical)
455        return False
456
457    end = time.process_time()
458    _addToActivePlugins(packageName, end - start)
459    return True
460
461
462def startProcessingPlugin(packageName: str) -> bool:
463    """ initialize only the Processing components of a plugin """
464    global plugins, active_plugins, iface, plugin_times
465    start = time.process_time()
466    if not _startPlugin(packageName):
467        return False
468
469    errMsg = QCoreApplication.translate("Python", "Couldn't load plugin '{0}'").format(packageName)
470    if not hasattr(plugins[packageName], 'initProcessing'):
471        del plugins[packageName]
472        _unloadPluginModules(packageName)
473        msg = QCoreApplication.translate("Python", "{0} - plugin has no initProcessing() method").format(errMsg)
474        showException(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], msg, messagebar=True, level=Qgis.Critical)
475        return False
476
477    # initProcessing
478    try:
479        plugins[packageName].initProcessing()
480    except:
481        del plugins[packageName]
482        _unloadPluginModules(packageName)
483        msg = QCoreApplication.translate("Python", "{0} due to an error when calling its initProcessing() method").format(errMsg)
484        showException(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], msg, messagebar=True)
485        return False
486
487    end = time.process_time()
488    _addToActivePlugins(packageName, end - start)
489
490    return True
491
492
493def canUninstallPlugin(packageName: str) -> bool:
494    """ confirm that the plugin can be uninstalled """
495    global plugins, active_plugins
496
497    if packageName not in plugins:
498        return False
499    if packageName not in active_plugins:
500        return False
501
502    try:
503        metadata = plugins[packageName]
504        if "canBeUninstalled" not in dir(metadata):
505            return True
506        return bool(metadata.canBeUninstalled())
507    except:
508        msg = "Error calling " + packageName + ".canBeUninstalled"
509        showException(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], msg, messagebar=True)
510        return True
511
512
513def unloadPlugin(packageName: str) -> bool:
514    """ unload and delete plugin! """
515    global plugins, active_plugins
516
517    if packageName not in plugins:
518        return False
519    if packageName not in active_plugins:
520        return False
521
522    try:
523        plugins[packageName].unload()
524        del plugins[packageName]
525        active_plugins.remove(packageName)
526        _unloadPluginModules(packageName)
527        return True
528    except Exception as e:
529        msg = QCoreApplication.translate("Python", "Error while unloading plugin {0}").format(packageName)
530        showException(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], msg, messagebar=True)
531        return False
532
533
534def _unloadPluginModules(packageName: str):
535    """ unload plugin package with all its modules (files) """
536    global _plugin_modules
537    mods = _plugin_modules[packageName]
538
539    for mod in mods:
540        if mod not in sys.modules:
541            continue
542
543        # if it looks like a Qt resource file, try to do a cleanup
544        # otherwise we might experience a segfault next time the plugin is loaded
545        # because Qt will try to access invalid plugin resource data
546        try:
547            if hasattr(sys.modules[mod], 'qCleanupResources'):
548                sys.modules[mod].qCleanupResources()
549        except:
550            # Print stack trace for debug
551            qDebug("qCleanupResources error:\n%s" % traceback.format_exc())
552
553        # try removing path
554        if hasattr(sys.modules[mod], '__path__'):
555            for path in sys.modules[mod].__path__:
556                try:
557                    sys.path.remove(path)
558                except ValueError:
559                    # Discard if path is not there
560                    pass
561
562        # try to remove the module from python
563        try:
564            del sys.modules[mod]
565        except:
566            qDebug("Error when removing module:\n%s" % traceback.format_exc())
567    # remove the plugin entry
568    del _plugin_modules[packageName]
569
570
571def isPluginLoaded(packageName: str) -> bool:
572    """ find out whether a plugin is active (i.e. has been started) """
573    global plugins, active_plugins
574
575    if packageName not in plugins:
576        return False
577    return (packageName in active_plugins)
578
579
580def reloadPlugin(packageName: str) -> bool:
581    """ unload and start again a plugin """
582    global active_plugins
583    if packageName not in active_plugins:
584        return False  # it's not active
585
586    unloadPlugin(packageName)
587    loadPlugin(packageName)
588    started = startPlugin(packageName)
589    return started
590
591
592def showPluginHelp(packageName: str = None, filename: str = "index", section: str = ""):
593    """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.
594
595    :param str packageName: name of package folder, if None it's using the current file package. Defaults to None. Optional.
596    :param str filename: name of file to open. It can be a path like 'doc/index' for example. Defaults to 'index'.
597    :param str section: URL path to open. Defaults to empty string.
598    """
599    try:
600        source = ""
601        if packageName is None:
602            import inspect
603
604            source = inspect.currentframe().f_back.f_code.co_filename
605        else:
606            source = sys.modules[packageName].__file__
607    except:
608        return
609    path = os.path.dirname(source)
610    locale = str(QLocale().name())
611    helpfile = os.path.join(path, filename + "-" + locale + ".html")
612    if not os.path.exists(helpfile):
613        helpfile = os.path.join(path, filename + "-" + locale.split("_")[0] + ".html")
614    if not os.path.exists(helpfile):
615        helpfile = os.path.join(path, filename + "-en.html")
616    if not os.path.exists(helpfile):
617        helpfile = os.path.join(path, filename + "-en_US.html")
618    if not os.path.exists(helpfile):
619        helpfile = os.path.join(path, filename + ".html")
620    if os.path.exists(helpfile):
621        url = "file://" + helpfile
622        if section != "":
623            url = url + "#" + section
624        QDesktopServices.openUrl(QUrl(url))
625
626
627def pluginDirectory(packageName: str) -> str:
628    """ return directory where the plugin resides. Plugin must be loaded already """
629    return os.path.dirname(sys.modules[packageName].__file__)
630
631
632def reloadProjectMacros():
633    # unload old macros
634    unloadProjectMacros()
635
636    from qgis.core import QgsProject
637
638    code, ok = QgsProject.instance().readEntry("Macros", "/pythonCode")
639    if not ok or not code or code == '':
640        return
641
642    # create a new empty python module
643    import importlib
644    mod = importlib.util.module_from_spec(importlib.machinery.ModuleSpec("proj_macros_mod", None))
645
646    # set the module code and store it sys.modules
647    exec(str(code), mod.__dict__)
648    sys.modules["proj_macros_mod"] = mod
649
650    # load new macros
651    openProjectMacro()
652
653
654def unloadProjectMacros():
655    if "proj_macros_mod" not in sys.modules:
656        return
657    # unload old macros
658    closeProjectMacro()
659    # destroy the reference to the module
660    del sys.modules["proj_macros_mod"]
661
662
663def openProjectMacro():
664    if "proj_macros_mod" not in sys.modules:
665        return
666    mod = sys.modules["proj_macros_mod"]
667    if hasattr(mod, 'openProject'):
668        mod.openProject()
669
670
671def saveProjectMacro():
672    if "proj_macros_mod" not in sys.modules:
673        return
674    mod = sys.modules["proj_macros_mod"]
675    if hasattr(mod, 'saveProject'):
676        mod.saveProject()
677
678
679def closeProjectMacro():
680    if "proj_macros_mod" not in sys.modules:
681        return
682    mod = sys.modules["proj_macros_mod"]
683    if hasattr(mod, 'closeProject'):
684        mod.closeProject()
685
686
687#######################
688# SERVER PLUGINS
689#
690# TODO: move into server_utils.py ?
691
692# list of plugin paths. it gets filled in by the QGIS python library
693server_plugin_paths = []
694
695# dictionary of plugins
696server_plugins = {}
697
698# list of active (started) plugins
699server_active_plugins = []
700
701
702# initialize 'serverIface' object
703serverIface = None
704
705
706def initServerInterface(pointer):
707    from qgis.server import QgsServerInterface
708    from sip import wrapinstance
709    sys.excepthook = sys.__excepthook__
710    global serverIface
711    serverIface = wrapinstance(pointer, QgsServerInterface)
712
713
714def startServerPlugin(packageName: str):
715    """ initialize the plugin """
716    global server_plugins, server_active_plugins, serverIface
717
718    if packageName in server_active_plugins:
719        return False
720    if packageName not in sys.modules:
721        return False
722
723    package = sys.modules[packageName]
724
725    errMsg = QCoreApplication.translate("Python", "Couldn't load server plugin {0}").format(packageName)
726
727    # create an instance of the plugin
728    try:
729        server_plugins[packageName] = package.serverClassFactory(serverIface)
730    except:
731        _unloadPluginModules(packageName)
732        msg = QCoreApplication.translate("Python",
733                                         "{0} due to an error when calling its serverClassFactory() method").format(errMsg)
734        showException(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], msg)
735        return False
736
737    # add to active plugins
738    server_active_plugins.append(packageName)
739    return True
740
741
742def spatialite_connect(*args, **kwargs):
743    """returns a dbapi2.Connection to a SpatiaLite db
744using the "mod_spatialite" extension (python3)"""
745    import sqlite3
746    import re
747
748    def fcnRegexp(pattern, string):
749        result = re.search(pattern, string)
750        return True if result else False
751
752    con = sqlite3.dbapi2.connect(*args, **kwargs)
753    con.enable_load_extension(True)
754    cur = con.cursor()
755    libs = [
756        # SpatiaLite >= 4.2 and Sqlite >= 3.7.17, should work on all platforms
757        ("mod_spatialite", "sqlite3_modspatialite_init"),
758        # SpatiaLite >= 4.2 and Sqlite < 3.7.17 (Travis)
759        ("mod_spatialite.so", "sqlite3_modspatialite_init"),
760        # SpatiaLite < 4.2 (linux)
761        ("libspatialite.so", "sqlite3_extension_init")
762    ]
763    found = False
764    for lib, entry_point in libs:
765        try:
766            cur.execute("select load_extension('{}', '{}')".format(lib, entry_point))
767        except sqlite3.OperationalError:
768            continue
769        else:
770            found = True
771            break
772    if not found:
773        raise RuntimeError("Cannot find any suitable spatialite module")
774    if any(['.gpkg' in arg for arg in args]):
775        try:
776            cur.execute("SELECT EnableGpkgAmphibiousMode()")
777        except (sqlite3.Error, sqlite3.DatabaseError, sqlite3.NotSupportedError):
778            QgsMessageLog.logMessage(u"warning:{}".format("Could not enable geopackage amphibious mode"),
779                                     QCoreApplication.translate("Python", "Python warning"))
780
781    cur.close()
782    con.enable_load_extension(False)
783    con.create_function("regexp", 2, fcnRegexp)
784    return con
785
786
787class OverrideCursor():
788    """
789    Executes a code block with a different cursor set and makes sure the cursor
790    is restored even if exceptions are raised or an intermediate ``return``
791    statement is hit.
792
793    Example:
794    ```
795    with OverrideCursor(Qt.WaitCursor):
796        do_a_slow(operation)
797    ```
798    """
799
800    def __init__(self, cursor):
801        self.cursor = cursor
802
803    def __enter__(self):
804        QApplication.setOverrideCursor(self.cursor)
805
806    def __exit__(self, exc_type, exc_val, exc_tb):
807        QApplication.restoreOverrideCursor()
808        return exc_type is None
809
810
811#######################
812# IMPORT wrapper
813
814if os.name == 'nt' and sys.version_info < (3, 8):
815    import ctypes
816    from ctypes import windll, wintypes
817
818    kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
819
820    _hasAddDllDirectory = hasattr(kernel32, 'AddDllDirectory')
821    if _hasAddDllDirectory:
822        _import_path = os.environ['PATH']
823        _import_paths = {}
824
825        def _errcheck_zero(result, func, args):
826            if not result:
827                raise ctypes.WinError(ctypes.get_last_error())
828            return args
829
830        DLL_DIRECTORY_COOKIE = wintypes.LPVOID
831
832        _AddDllDirectory = kernel32.AddDllDirectory
833        _AddDllDirectory.errcheck = _errcheck_zero
834        _AddDllDirectory.restype = DLL_DIRECTORY_COOKIE
835        _AddDllDirectory.argtypes = (wintypes.LPCWSTR,)
836
837        _RemoveDllDirectory = kernel32.RemoveDllDirectory
838        _RemoveDllDirectory.errcheck = _errcheck_zero
839        _RemoveDllDirectory.argtypes = (DLL_DIRECTORY_COOKIE,)
840
841_uses_builtins = True
842try:
843    import builtins
844    _builtin_import = builtins.__import__
845except AttributeError:
846    _uses_builtins = False
847    import __builtin__
848    _builtin_import = __builtin__.__import__
849
850_plugin_modules = {}
851
852
853def _import(name, globals={}, locals={}, fromlist=[], level=None):
854    """
855    Wrapper around builtin import that keeps track of loaded plugin modules and blocks
856    certain unsafe imports
857    """
858    if level is None:
859        level = 0
860
861    if 'PyQt4' in name:
862        msg = 'PyQt4 classes cannot be imported in QGIS 3.x.\n' \
863              'Use {} or the version independent {} import instead.'.format(name.replace('PyQt4', 'PyQt5'), name.replace('PyQt4', 'qgis.PyQt'))
864        raise ImportError(msg)
865
866    if os.name == 'nt' and sys.version_info < (3, 8):
867        global _hasAddDllDirectory
868        if _hasAddDllDirectory:
869            global _import_path
870            global _import_paths
871
872            old_path = _import_path
873            new_path = os.environ['PATH']
874            if old_path != new_path:
875                global _AddDllDirectory
876                global _RemoveDllDirectory
877
878                for p in set(new_path.split(';')) - set(old_path.split(';')):
879                    if p is not None and p not in _import_path and os.path.isdir(p):
880                        _import_paths[p] = _AddDllDirectory(p)
881
882                for p in set(old_path.split(';')) - set(new_path.split(';')):
883                    if p in _import_paths:
884                        _RemoveDllDirectory(_import_paths.pop(p))
885
886                _import_path = new_path
887
888    mod = _builtin_import(name, globals, locals, fromlist, level)
889
890    if mod and getattr(mod, '__file__', None):
891        module_name = mod.__name__ if fromlist else name
892        package_name = module_name.split('.')[0]
893        # check whether the module belongs to one of our plugins
894        if package_name in available_plugins:
895            if package_name not in _plugin_modules:
896                _plugin_modules[package_name] = set()
897            _plugin_modules[package_name].add(module_name)
898            # check the fromlist for additional modules (from X import Y,Z)
899            if fromlist:
900                for fromitem in fromlist:
901                    frmod = module_name + "." + fromitem
902                    if frmod in sys.modules:
903                        _plugin_modules[package_name].add(frmod)
904
905    return mod
906
907
908if not os.environ.get('QGIS_NO_OVERRIDE_IMPORT'):
909    if _uses_builtins:
910        builtins.__import__ = _import
911    else:
912        __builtin__.__import__ = _import
913
914
915def run_script_from_file(filepath: str):
916    """
917    Runs a Python script from a given file. Supports loading processing scripts.
918    :param filepath: The .py file to load.
919    """
920    import sys
921    import inspect
922    from qgis.processing import alg
923    try:
924        from qgis.core import QgsApplication, QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm
925        from qgis.processing import execAlgorithmDialog
926        _locals = {}
927        exec(open(filepath.replace("\\\\", "/").encode(sys.getfilesystemencoding())).read(), _locals)
928        alginstance = None
929        try:
930            alginstance = alg.instances.pop().createInstance()
931        except IndexError:
932            for name, attr in _locals.items():
933                if inspect.isclass(attr) and issubclass(attr, (QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm)) and attr.__name__ not in ("QgsProcessingAlgorithm", "QgsProcessingFeatureBasedAlgorithm"):
934                    alginstance = attr()
935                    break
936        if alginstance:
937            alginstance.setProvider(QgsApplication.processingRegistry().providerById("script"))
938            alginstance.initAlgorithm()
939            execAlgorithmDialog(alginstance)
940    except ImportError:
941        pass
942