mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-17 00:04:02 -04:00
[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:
parent
d136e922fb
commit
87d2da13fb
@ -66,6 +66,7 @@ ENDIF ()
|
||||
ADD_SUBDIRECTORY(PyQt)
|
||||
ADD_SUBDIRECTORY(ext-libs)
|
||||
ADD_SUBDIRECTORY(testing)
|
||||
ADD_SUBDIRECTORY(processing)
|
||||
|
||||
INCLUDE_DIRECTORIES(SYSTEM
|
||||
${PYTHON_INCLUDE_PATH}
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
25
python/processing/CMakeLists.txt
Normal file
25
python/processing/CMakeLists.txt
Normal 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)
|
28
python/processing/__init__.py
Normal file
28
python/processing/__init__.py
Normal 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()
|
502
python/processing/algfactory.py
Normal file
502
python/processing/algfactory.py
Normal 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),
|
||||
}
|
@ -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
|
||||
|
@ -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 );
|
||||
|
@ -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)
|
||||
|
162
tests/src/python/test_processing_alg_decorator.py
Normal file
162
tests/src/python/test_processing_alg_decorator.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user