[FEATURE][needs-docs] Add new @alg decorator for nicer python processing scripts. (#8586)

@alg()
@alg.help()
@alg.input()
@alg.output()
This commit is contained in:
Nathan Woodrow 2018-12-10 16:35:52 +10:00 committed by GitHub
parent d136e922fb
commit 87d2da13fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 778 additions and 40 deletions

View File

@ -66,6 +66,7 @@ ENDIF ()
ADD_SUBDIRECTORY(PyQt)
ADD_SUBDIRECTORY(ext-libs)
ADD_SUBDIRECTORY(testing)
ADD_SUBDIRECTORY(processing)
INCLUDE_DIRECTORIES(SYSTEM
${PYTHON_INCLUDE_PATH}

View File

@ -52,7 +52,7 @@ class DeleteScriptAction(ContextAction):
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if reply == QMessageBox.Yes:
filePath = ScriptUtils.findAlgorithmSource(self.itemData.__class__.__name__)
filePath = ScriptUtils.findAlgorithmSource(self.itemData.name())
if filePath is not None:
os.remove(filePath)
QgsApplication.processingRegistry().providerById("script").refreshAlgorithms()

View File

@ -27,7 +27,7 @@ __revision__ = '$Format:%H$'
import inspect
from qgis.core import QgsProcessingAlgorithm
from qgis.core import QgsProcessingAlgorithm, QgsMessageLog
from qgis.utils import iface
from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtWidgets import QMessageBox
@ -47,7 +47,7 @@ class EditScriptAction(ContextAction):
return isinstance(self.itemData, QgsProcessingAlgorithm) and self.itemData.provider().id() == "script"
def execute(self):
filePath = ScriptUtils.findAlgorithmSource(self.itemData.__class__.__name__)
filePath = ScriptUtils.findAlgorithmSource(self.itemData.name())
if filePath is not None:
dlg = ScriptEditorDialog(filePath, iface.mainWindow())
dlg.show()

View File

@ -44,6 +44,7 @@ from qgis.core import (QgsApplication,
QgsProcessingAlgorithm,
QgsProcessingFeatureBasedAlgorithm)
from qgis.utils import iface, OverrideCursor
from qgis.processing import alg as algfactory
from processing.gui.AlgorithmDialog import AlgorithmDialog
from processing.script import ScriptUtils
@ -222,9 +223,9 @@ class ScriptEditorDialog(BASE, WIDGET):
self.update_dialog_title()
def runAlgorithm(self):
d = {}
_locals = {}
try:
exec(self.editor.text(), d)
exec(self.editor.text(), _locals)
except Exception as e:
error = QgsError(traceback.format_exc(), "Processing")
QgsErrorDialog.show(error,
@ -233,10 +234,13 @@ class ScriptEditorDialog(BASE, WIDGET):
return
alg = None
for k, v in d.items():
if inspect.isclass(v) and issubclass(v, (QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm)) and v.__name__ not in ("QgsProcessingAlgorithm", "QgsProcessingFeatureBasedAlgorithm"):
alg = v()
break
try:
alg = algfactory.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 = attr()
break
if alg is None:
QMessageBox.warning(self,

View File

@ -25,6 +25,7 @@ __copyright__ = '(C) 2012, Victor Olaya'
__revision__ = '$Format:%H$'
from qgis.processing import alg as algfactory
import os
import inspect
import importlib
@ -66,20 +67,26 @@ def loadAlgorithm(moduleName, filePath):
spec = importlib.util.spec_from_file_location(moduleName, filePath)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
for x in dir(module):
obj = getattr(module, x)
if inspect.isclass(obj) and issubclass(obj, (QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm)) and obj.__name__ not in ("QgsProcessingAlgorithm", "QgsProcessingFeatureBasedAlgorithm"):
scriptsRegistry[x] = filePath
return obj()
try:
alg = algfactory.instances.pop().createInstance()
scriptsRegistry[alg.name()] = filePath
return alg
except IndexError:
for x in dir(module):
obj = getattr(module, x)
if inspect.isclass(obj) and issubclass(obj, (QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm)) and obj.__name__ not in ("QgsProcessingAlgorithm", "QgsProcessingFeatureBasedAlgorithm"):
o = obj()
scriptsRegistry[o.name()] = filePath
return o
except ImportError as e:
QgsMessageLog.logMessage(QCoreApplication.translate("ScriptUtils", "Could not import script algorithm '{}' from '{}'\n{}").format(moduleName, filePath, str(e)),
QCoreApplication.translate("ScriptUtils", "Processing"),
Qgis.Critical)
def findAlgorithmSource(className):
def findAlgorithmSource(name):
global scriptsRegistry
try:
return scriptsRegistry[className]
return scriptsRegistry[name]
except:
return None

View File

@ -0,0 +1,25 @@
# See ../CMakeLists.txt for info on staged-plugins* and clean-staged-plugins targets
SET(QGIS_PYTHON_DIR ${PYTHON_SITE_PACKAGES_DIR}/qgis)
SET (PYTHON_OUTPUT_DIRECTORY ${QGIS_OUTPUT_DIRECTORY}/python)
SET (NAME processing)
SET(PY_FILES
__init__.py
algfactory.py
)
FILE (MAKE_DIRECTORY ${QGIS_PYTHON_OUTPUT_DIRECTORY}/${NAME})
INSTALL(FILES ${PY_FILES} DESTINATION "${QGIS_PYTHON_DIR}/${NAME}")
ADD_CUSTOM_TARGET(py${NAME} ALL)
# stage to output to make available when QGIS is run from build directory
FOREACH(pyfile ${PY_FILES})
ADD_CUSTOM_COMMAND(TARGET py${NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy ${pyfile} "${QGIS_PYTHON_OUTPUT_DIRECTORY}/${NAME}"
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPENDS ${pyfile}
)
PY_COMPILE(pyutils "${QGIS_PYTHON_OUTPUT_DIRECTORY}/${NAME}/${pyfile}")
ENDFOREACH(pyfile)

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
"""
***************************************************************************
__init__.py
---------------------
Date : November 2018
Copyright : (C) 2018 by Nathan Woodrow
Email : woodrow dot nathan 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__ = 'Nathan Woodrow'
__date__ = 'November 2018'
__copyright__ = '(C) 2018, Nathan Woodrow'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
from .algfactory import ProcessingAlgFactory
alg = ProcessingAlgFactory()

View File

@ -0,0 +1,502 @@
# -*- coding: utf-8 -*-
"""
***************************************************************************
algfactory.py
---------------------
Date : November 2018
Copyright : (C) 2018 by Nathan Woodrow
Email : woodrow dot nathan 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__ = 'Nathan Woodrow'
__date__ = 'November 2018'
__copyright__ = '(C) 2018, Nathan Woodrow'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
from collections import OrderedDict
from functools import partial
from qgis.PyQt.QtCore import QCoreApplication
from qgis.PyQt.QtGui import QIcon
from qgis.core import (QgsProcessingParameterDefinition,
QgsProcessingAlgorithm,
QgsProcessingParameterString,
QgsProcessingParameterNumber,
QgsProcessingParameterDistance,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterFeatureSink,
QgsProcessingParameterFileDestination,
QgsProcessingParameterFolderDestination,
QgsProcessingParameterRasterDestination,
QgsProcessingParameterVectorDestination,
QgsProcessingParameterBand,
QgsProcessingParameterBoolean,
QgsProcessingParameterCrs,
QgsProcessingParameterEnum,
QgsProcessingParameterExpression,
QgsProcessingParameterExtent,
QgsProcessingParameterField,
QgsProcessingParameterFile,
QgsProcessingParameterMapLayer,
QgsProcessingParameterMatrix,
QgsProcessingParameterMultipleLayers,
QgsProcessingParameterPoint,
QgsProcessingParameterRange,
QgsProcessingParameterVectorLayer,
QgsProcessingOutputString,
QgsProcessingOutputFile,
QgsProcessingOutputFolder,
QgsProcessingOutputHtml,
QgsProcessingOutputLayerDefinition,
QgsProcessingOutputMapLayer,
QgsProcessingOutputMultipleLayers,
QgsProcessingOutputNumber,
QgsProcessingOutputRasterLayer,
QgsProcessingOutputVectorLayer,
QgsMessageLog)
def _log(*args, **kw):
"""
Log messages to the QgsMessageLog viewer
"""
QgsMessageLog.logMessage(" ".join(map(str, args)), "Factory")
def _make_output(**args):
"""
Create a processing output class type.
:param args: 'cls' The class object type.
'name' the name of the output
'description' The description used on the output
:return:
"""
cls = args['cls']
del args['cls']
newargs = {
"name": args['name'],
"description": args['description'],
}
return cls(**newargs)
class ProcessingAlgFactoryException(Exception):
"""
Exception raised when using @alg on a function.
"""
def __init__(self, message):
super(ProcessingAlgFactoryException, self).__init__(message)
class AlgWrapper(QgsProcessingAlgorithm):
"""
Wrapper object used to create new processing algorithms from @alg.
"""
def __init__(self, name=None, display=None,
group=None, group_id=None, inputs=None,
outputs=None, func=None, help=None, icon=None):
super(AlgWrapper, self).__init__()
self._inputs = OrderedDict(inputs or {})
self._outputs = OrderedDict(outputs or {})
self._icon = icon
self._name = name
self._group = group
self._group_id = group_id
self._display = display
self._func = func
self._help = help
def _get_parent_id(self, parent):
"""
Return the id of the parent object.
"""
if isinstance(parent, str):
return parent
else:
raise NotImplementedError()
# Wrapper logic
def define(self, name, label, group, group_label, help=None, icon=None):
self._name = name
self._display = label
self._group = group_label
self._group_id = group
self._help = help
self._icon = icon
def end(self):
"""
Finalize the wrapper logic and check for any invalid config.
"""
if not self.has_outputs:
raise ProcessingAlgFactoryException("No outputs defined for '{}' alg"
"At least one must be defined. Use @alg.output")
def add_output(self, type, **kwargs):
parm = self._create_param(type, output=True, **kwargs)
self._outputs[parm.name()] = parm
def add_help(self, helpstring, *args, **kwargs):
self._help = helpstring
def add_input(self, type, **kwargs):
parm = self._create_param(type, **kwargs)
self._inputs[parm.name()] = parm
@property
def inputs(self):
return self._inputs
@property
def outputs(self):
return self._outputs
def _create_param(self, type, output=False, **kwargs):
name = kwargs['name']
if name in self._inputs or name in self._outputs:
raise ProcessingAlgFactoryException("{} already defined".format(name))
parent = kwargs.get("parent")
if parent:
parentname = self._get_parent_id(parent)
if parentname == name:
raise ProcessingAlgFactoryException("{} can't depend on itself. "
"We know QGIS is smart but it's not that smart".format(name))
if parentname not in self._inputs or parentname not in self._outputs:
raise ProcessingAlgFactoryException("Can't find parent named {}".format(parentname))
kwargs['description'] = kwargs.pop("label", "")
kwargs['defaultValue'] = kwargs.pop("default", "")
advanced = kwargs.pop("advanced", False)
try:
if output:
try:
make_func = output_type_mapping[type]
except KeyError:
raise ProcessingAlgFactoryException("{} is a invalid output type".format(type))
else:
try:
make_func = input_type_mapping[type]
except KeyError:
raise ProcessingAlgFactoryException("{} is a invalid input type".format(type))
parm = make_func(**kwargs)
if advanced:
parm.setFlags(parm.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
return parm
except KeyError as ex:
raise NotImplementedError("{} not supported".format(str(type)))
def set_func(self, func):
self._func = func
# Only take the help from the function if it hasn't already been set.
if self._func and not self._help:
self._help = self._func.__doc__.strip()
@property
def has_outputs(self):
"""
True if this alg wrapper has outputs defined.
"""
return bool(self._outputs)
@property
def has_inputs(self):
"""
True if this alg wrapper has outputs defined.
"""
return bool(self._inputs)
def _get_parameter_value(self, parm, parameters, name, context):
"""
Extract the real value from the parameter.
"""
if isinstance(parm, QgsProcessingParameterString):
value = self.parameterAsString(parameters, name, context)
return value
elif isinstance(parm, QgsProcessingParameterNumber):
if parm.dataType() == QgsProcessingParameterNumber.Integer:
value = self.parameterAsInt(parameters, name, context)
return value
if parm.dataType() == QgsProcessingParameterNumber.Double:
value = self.parameterAsDouble(parameters, name, context)
return value
# Overloads
def name(self):
return self._name
def displayName(self):
return self._display
def group(self):
return self._group
def groupId(self):
return self._group_id
def processAlgorithm(self, parameters, context, feedback):
values = {}
for parm in self._inputs.values():
name = parm.name()
values[name] = self._get_parameter_value(parm, parameters, name, context)
output = self._func(self, parameters, context, feedback, values)
if output is None:
return {}
return output
def createInstance(self):
return AlgWrapper(self._name, self._display,
self._group, self._group_id,
inputs=self._inputs,
outputs=self._outputs,
func=self._func,
help=self._help,
icon=self._icon)
def initAlgorithm(self, configuration=None, p_str=None, Any=None, *args, **kwargs):
for parm in self._inputs.values():
self.addParameter(parm.clone())
for parm in self._outputs.values():
clsname = parm.__class__.__name__
klass = globals()[clsname]
clone = klass(parm.name(), parm.description())
self.addOutput(clone)
def shortHelpString(self):
return self._help
def icon(self):
return QIcon(self._icon)
class ProcessingAlgFactory():
STRING = "STRING",
INT = "INT",
NUMBER = "NUMBER",
DISTANCE = "DISTANCE",
SINK = "SINK"
SOURCE = "SOURCE"
FILE = "FILE",
FOLDER = "FOLDER",
HTML = "HTML",
LAYERDEF = "LAYERDEF",
MAPLAYER = "MAPLAYER",
MULTILAYER = "MULTILAYER",
RASTER_LAYER = "RASTER_LAYER",
VECTOR_LAYER = "VECTOR_LAYER",
FILE_DEST = "FILE_DEST",
FOLDER_DEST = "FOLDER_DEST",
RASTER_LAYER_DEST = "RASTER_LAYER_DEST",
VECTOR_LAYER_DEST = "VECTOR_LAYER_DEST",
BAND = "BAND",
BOOL = "BOOL",
CRS = "CRS",
ENUM = "ENUM",
EXPRESSION = "EXPRESSION",
EXTENT = "EXTENT",
FIELD = "FIELD",
MATRIX = "MATRIX",
POINT = "POINT",
RANGE = "RANGE",
def __init__(self):
self._current = None
self.instances = []
def tr(self, string):
"""
Returns a translatable string with the self.tr() function.
"""
return QCoreApplication.translate('Processing', string)
@property
def current(self):
return self._current
@property
def current_defined(self):
return self._current is not None
def __call__(self, *args, **kwargs):
return self._define(*args, **kwargs)
def _initnew(self):
self._current = AlgWrapper()
def _pop(self):
self.instances.append(self.current)
self._current = None
def _define(self, *args, **kwargs):
self._initnew()
self.current.define(*args, **kwargs)
def dec(f):
self.current.set_func(f)
self.current.end()
self._pop()
return f
return dec
def output(self, type, *args, **kwargs):
"""
Define a output parameter for this algorithm using @alg.output.
Apart from `type` this method will take all arguments and pass them though to the correct `QgsProcessingOutputDefinition ` type.
Types:
str: QgsProcessingOutputString
int: QgsProcessingOutputNumber
float: QgsProcessingOutputNumber
alg.NUMBER: QgsProcessingOutputNumber
alg.DISTANCE: QgsProcessingOutputNumber
alg.INT: QgsProcessingOutputNumber
alg.STRING: QgsProcessingOutputString
alg.FILE: QgsProcessingOutputFile
alg.FOLDER: QgsProcessingOutputFolder
alg.HTML: QgsProcessingOutputHtml
alg.LAYERDEF: QgsProcessingOutputLayerDefinition
alg.MAPLAYER: QgsProcessingOutputMapLayer
alg.MULTILAYER: QgsProcessingOutputMultipleLayers
alg.RASTER_LAYER: QgsProcessingOutputRasterLayer
alg.VECTOR_LAYER: QgsProcessingOutputVectorLayer
:param type: The type of the input. This should be a type define on `alg` like alg.STRING, alg.DISTANCE
:keyword label: The label of the output. Will convert into `description` arg.
:keyword parent: The string ID of the parent parameter. Parent parameter must be defined before its here.
"""
def dec(f):
return f
self.current.add_output(type, *args, **kwargs)
return dec
def help(self, helpstring, *args, **kwargs):
"""
Define the help for the algorithm using @alg.help. This method will
be used instead of the doc string on the function as the help in the processing dialogs.
:param helpstring: The help text. Use alg.tr() for translation support.
"""
def dec(f):
return f
self.current.add_help(helpstring, *args, **kwargs)
return dec
def input(self, type, *args, **kwargs):
"""
Define a input parameter for this algorithm using @alg.input.
Apart from `type` this method will take all arguments and pass them though to the correct `QgsProcessingParameterDefinition` type.
Types:
str: QgsProcessingParameterString
int: QgsProcessingParameterNumber
float: QgsProcessingParameterNumber
bool: QgsProcessingParameterBoolean
alg.NUMBER: QgsProcessingParameterNumber
alg.INT: QgsProcessingParameterNumber
alg.STRING: QgsProcessingParameterString
alg.DISTANCE: QgsProcessingParameterDistance
alg.SINK: QgsProcessingParameterFeatureSink
alg.SOURCE: QgsProcessingParameterFeatureSource
alg.FILE_DEST: QgsProcessingParameterFileDestination
alg.FOLDER_DEST: QgsProcessingParameterFolderDestination
alg.RASTER_LAYER_DEST: QgsProcessingParameterRasterDestination
alg.VECTOR_LAYER_DEST: QgsProcessingParameterVectorDestination
alg.BAND: QgsProcessingParameterBand
alg.BOOL: QgsProcessingParameterBoolean
alg.CRS: QgsProcessingParameterCrs
alg.ENUM: QgsProcessingParameterEnum
alg.EXPRESSION: QgsProcessingParameterExpression
alg.EXTENT: QgsProcessingParameterExtent
alg.FIELD: QgsProcessingParameterField
alg.FILE: QgsProcessingParameterFile
alg.MAPLAYER: QgsProcessingParameterMapLayer
alg.MATRIX: QgsProcessingParameterMatrix
alg.MULTILAYER: QgsProcessingParameterMultipleLayers
alg.POINT: QgsProcessingParameterPoint
alg.RANGE: QgsProcessingParameterRange
alg.VECTOR_LAYER: QgsProcessingParameterVectorLayer
:param type: The type of the input. This should be a type define on `alg` like alg.STRING, alg.DISTANCE
:keyword label: The label of the output. Translates into `description` arg.
:keyword parent: The string ID of the parent parameter. Parent parameter must be defined before its here.
:keyword default: The default value set for that parameter. Translates into `defaultValue` arg.
"""
def dec(f):
return f
self.current.add_input(type, *args, **kwargs)
return dec
input_type_mapping = {
str: QgsProcessingParameterString,
int: partial(QgsProcessingParameterNumber, type=QgsProcessingParameterNumber.Integer),
float: partial(QgsProcessingParameterNumber, type=QgsProcessingParameterNumber.Double),
bool: QgsProcessingParameterBoolean,
ProcessingAlgFactory.NUMBER: partial(QgsProcessingParameterNumber, type=QgsProcessingParameterNumber.Double),
ProcessingAlgFactory.INT: partial(QgsProcessingParameterNumber, type=QgsProcessingParameterNumber.Integer),
ProcessingAlgFactory.STRING: QgsProcessingParameterString,
ProcessingAlgFactory.DISTANCE: QgsProcessingParameterDistance,
ProcessingAlgFactory.SINK: QgsProcessingParameterFeatureSink,
ProcessingAlgFactory.SOURCE: QgsProcessingParameterFeatureSource,
ProcessingAlgFactory.FILE_DEST: QgsProcessingParameterFileDestination,
ProcessingAlgFactory.FOLDER_DEST: QgsProcessingParameterFolderDestination,
ProcessingAlgFactory.RASTER_LAYER_DEST: QgsProcessingParameterRasterDestination,
ProcessingAlgFactory.VECTOR_LAYER_DEST: QgsProcessingParameterVectorDestination,
ProcessingAlgFactory.BAND: QgsProcessingParameterBand,
ProcessingAlgFactory.BOOL: QgsProcessingParameterBoolean,
ProcessingAlgFactory.CRS: QgsProcessingParameterCrs,
ProcessingAlgFactory.ENUM: QgsProcessingParameterEnum,
ProcessingAlgFactory.EXPRESSION: QgsProcessingParameterExpression,
ProcessingAlgFactory.EXTENT: QgsProcessingParameterExtent,
ProcessingAlgFactory.FIELD: QgsProcessingParameterField,
ProcessingAlgFactory.FILE: QgsProcessingParameterFile,
ProcessingAlgFactory.MAPLAYER: QgsProcessingParameterMapLayer,
ProcessingAlgFactory.MATRIX: QgsProcessingParameterMatrix,
ProcessingAlgFactory.MULTILAYER: QgsProcessingParameterMultipleLayers,
ProcessingAlgFactory.POINT: QgsProcessingParameterPoint,
ProcessingAlgFactory.RANGE: QgsProcessingParameterRange,
ProcessingAlgFactory.VECTOR_LAYER: QgsProcessingParameterVectorLayer,
}
output_type_mapping = {
str: partial(_make_output, cls=QgsProcessingOutputString),
int: partial(_make_output, cls=QgsProcessingOutputNumber),
float: partial(_make_output, cls=QgsProcessingOutputNumber),
ProcessingAlgFactory.NUMBER: partial(_make_output, cls=QgsProcessingOutputNumber),
ProcessingAlgFactory.DISTANCE: partial(_make_output, cls=QgsProcessingOutputNumber),
ProcessingAlgFactory.INT: partial(_make_output, cls=QgsProcessingOutputNumber),
ProcessingAlgFactory.STRING: partial(_make_output, cls=QgsProcessingOutputString),
ProcessingAlgFactory.FILE: partial(_make_output, cls=QgsProcessingOutputFile),
ProcessingAlgFactory.FOLDER: partial(_make_output, cls=QgsProcessingOutputFolder),
ProcessingAlgFactory.HTML: partial(_make_output, cls=QgsProcessingOutputHtml),
ProcessingAlgFactory.LAYERDEF: partial(_make_output, cls=QgsProcessingOutputLayerDefinition),
ProcessingAlgFactory.MAPLAYER: partial(_make_output, cls=QgsProcessingOutputMapLayer),
ProcessingAlgFactory.MULTILAYER: partial(_make_output, cls=QgsProcessingOutputMultipleLayers),
ProcessingAlgFactory.RASTER_LAYER: partial(_make_output, cls=QgsProcessingOutputRasterLayer),
ProcessingAlgFactory.VECTOR_LAYER: partial(_make_output, cls=QgsProcessingOutputVectorLayer),
}

View File

@ -694,3 +694,33 @@ if not os.environ.get('QGIS_NO_OVERRIDE_IMPORT'):
builtins.__import__ = _import
else:
__builtin__.__import__ = _import
def run_script_from_file(filepath):
"""
Runs a Python script from a given file. Supports loading processing scripts.
:param filepath: The .py file to load.
"""
import sys
import inspect
from qgis.processing import alg
try:
from qgis.core import QgsApplication, QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm
from processing.gui.AlgorithmDialog import AlgorithmDialog
_locals = {}
exec(open(filepath.replace("\\\\", "/").encode(sys.getfilesystemencoding())).read(), _locals)
alginstance = None
try:
alginstance = 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"):
alginstance = attr()
break
if alginstance:
alginstance.setProvider(QgsApplication.processingRegistry().providerById("script"))
alginstance.initAlgorithm()
dlg = AlgorithmDialog(alginstance)
dlg.show()
except ImportError:
pass

View File

@ -6259,30 +6259,8 @@ void QgisApp::runScript( const QString &filePath )
if ( !showScriptWarning || msgbox.result() == QMessageBox::Yes )
{
mPythonUtils->runString(
QString( "import sys\n"
"import inspect\n"
"from qgis.utils import iface\n"
"try:\n"
" from qgis.core import QgsApplication, QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm\n"
" from processing.gui.AlgorithmDialog import AlgorithmDialog\n"
"except ImportError:\n"
" processing_found = False\n"
"else:\n"
" processing_found = True\n"
"d={}\n"
"exec(open(\"%1\".replace(\"\\\\\", \"/\").encode(sys.getfilesystemencoding())).read(), d)\n"
"if processing_found:\n"
" alg = None\n"
" for k, v in d.items():\n"
" if inspect.isclass(v) and issubclass(v, (QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm)) and v.__name__ not in (\"QgsProcessingAlgorithm\", \"QgsProcessingFeatureBasedAlgorithm\"):\n"
" alg = v()\n"
" break\n"
" if alg:\n"
" alg.setProvider(QgsApplication.processingRegistry().providerById(\"script\"))\n"
" alg.initAlgorithm()\n"
" dlg = AlgorithmDialog(alg)\n"
" dlg.show()\n" ).arg( filePath ), tr( "Failed to run Python script:" ), false );
mPythonUtils->runString( QString( "qgis.utils.run_script_from_file(\"%1\")" ).arg( filePath ),
tr( "Failed to run Python script:" ), false );
}
#else
Q_UNUSED( filePath );

View File

@ -152,6 +152,7 @@ ADD_PYTHON_TEST(PyQgsPointDisplacementRenderer test_qgspointdisplacementrenderer
ADD_PYTHON_TEST(PyQgsPostgresDomain test_qgspostgresdomain.py)
ADD_PYTHON_TEST(PyQgsProcessingRecentAlgorithmLog test_qgsprocessingrecentalgorithmslog.py)
ADD_PYTHON_TEST(PyQgsProcessingInPlace test_qgsprocessinginplace.py)
ADD_PYTHON_TEST(PyQgsProcessingAlgDecorator test_processing_alg_decorator.py)
ADD_PYTHON_TEST(PyQgsProjectionSelectionWidgets test_qgsprojectionselectionwidgets.py)
ADD_PYTHON_TEST(PyQgsProjectMetadata test_qgsprojectmetadata.py)
ADD_PYTHON_TEST(PyQgsRange test_qgsrange.py)

View File

@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for the @alg processing algorithm.
.. note:: 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__ = 'Nathan Woodrow'
__date__ = '10.12.2018'
__copyright__ = 'Copyright 2018, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import sys
import os
import qgis # NOQA
from qgis.testing import unittest, start_app
from qgis.processing import alg
from qgis.core import QgsSettings
from qgis.PyQt.QtCore import QCoreApplication
start_app()
ARGNAME = "TEST_ALG{0}"
HELPSTRING = "TEST_HELP STRING{0}"
def define_new_no_inputs(newid=1):
@alg(name="noinputs", label=alg.tr("Test func"), group="unittest",
group_label=alg.tr("Test label"))
@alg.output(type=str, name="DISTANCE_OUT", label="Distance out")
def testalg(instance, parameters, context, feedback, inputs):
"""
Test doc string text
"""
def define_new_doc_string(newid=1):
@alg(name=ARGNAME.format(newid), label=alg.tr("Test func"), group="unittest",
group_label=alg.tr("Test label"))
@alg.input(type=alg.SOURCE, name="INPUT", label="Input layer")
@alg.output(type=str, name="DISTANCE_OUT", label="Distance out")
def testalg(instance, parameters, context, feedback, inputs):
"""
Test doc string text
"""
def define_new(newid=1):
@alg(name=ARGNAME.format(newid), label=alg.tr("Test func"), group="unittest",
group_label=alg.tr("Test label"))
@alg.help(HELPSTRING.format(newid))
@alg.input(type=alg.SOURCE, name="INPUT", label="Input layer")
@alg.input(type=alg.DISTANCE, name="DISTANCE", label="Distance", default=30)
@alg.input(type=alg.SINK, name="SINK", label="Output layer")
@alg.output(type=str, name="DISTANCE_OUT", label="Distance out")
def testalg(instance, parameters, context, feedback, inputs):
"""
Given a distance will split a line layer into segments of the distance
"""
def cleanup():
alg.instances.clear()
class AlgNoInputs(unittest.TestCase):
def setUp(self):
cleanup()
def test_can_have_no_inputs(self):
define_new_no_inputs()
class AlgInstanceTests(unittest.TestCase):
"""
Tests to check the createInstance method will work as expected.
"""
def setUp(self):
cleanup()
define_new()
self.current = alg.instances.pop().createInstance()
def test_correct_number_of_inputs_and_outputs(self):
self.assertEqual(3, len(self.current.inputs))
self.assertEqual(1, len(self.current.outputs))
def test_correct_number_of_inputs_and_outputs_after_init(self):
self.current.initAlgorithm()
defs = self.current.parameterDefinitions()
self.assertEqual(3, len(defs))
inputs = [
("INPUT", "Input layer"),
("DISTANCE", "Distance"),
("SINK", "Output layer"),
]
for count, data in enumerate(inputs):
parmdef = defs[count]
self.assertEqual(data[0], parmdef.name())
self.assertEqual(data[1], parmdef.description())
def test_func_is_set(self):
self.assertIsNotNone(self.current._func)
def test_has_help_from_help_decorator(self):
self.assertEqual(HELPSTRING.format(1), self.current.shortHelpString())
def test_name_and_label(self):
self.assertEqual(ARGNAME.format(1), self.current.name())
self.assertEqual("Test func", self.current.displayName())
def test_group(self):
self.assertEqual("Test label", self.current.group())
self.assertEqual("unittest", self.current.groupId())
class AlgHelpTests(unittest.TestCase):
def test_has_help_from_help_decorator(self):
cleanup()
define_new()
current = alg.instances.pop()
self.assertEqual(HELPSTRING.format(1), current.shortHelpString())
def test_has_help_from_docstring(self):
define_new_doc_string()
current = alg.instances.pop()
self.assertEqual("Test doc string text", current.shortHelpString())
class TestAlg(unittest.TestCase):
def setUp(self):
cleanup()
define_new()
def test_correct_number_of_inputs_and_outputs(self):
current = alg.instances.pop()
self.assertEqual(3, len(current.inputs))
self.assertEqual(1, len(current.outputs))
self.assertTrue(current.has_inputs)
self.assertTrue(current.has_outputs)
def test_correct_number_defined_in_stack_before_and_after(self):
self.assertEqual(1, len(alg.instances))
alg.instances.pop()
self.assertEqual(0, len(alg.instances))
def test_current_has_correct_name(self):
alg.instances.pop()
for i in range(3):
define_new(i)
self.assertEqual(3, len(alg.instances))
for i in range(3, 1):
current = alg.instances.pop()
self.assertEqual(ARGNAME.format(i), current.name())
if __name__ == "__main__":
unittest.main()