From 227af8ac8ea3b10f0700d92a59b65a3037b47161 Mon Sep 17 00:00:00 2001 From: volaya Date: Fri, 6 Nov 2015 14:02:11 +0100 Subject: [PATCH] [processing] brought back 'export model as python' functionality --- python/plugins/processing/core/parameters.py | 37 +++++++++++ .../processing/modeler/ModelerAlgorithm.py | 64 ++++++++++++++++++- .../processing/modeler/ModelerDialog.py | 20 +++++- .../ModelerParameterDefinitionDialog.py | 8 ++- python/plugins/processing/ui/DlgModeler.ui | 10 +++ 5 files changed, 134 insertions(+), 5 deletions(-) diff --git a/python/plugins/processing/core/parameters.py b/python/plugins/processing/core/parameters.py index 726f687099b0..933650d231e6 100644 --- a/python/plugins/processing/core/parameters.py +++ b/python/plugins/processing/core/parameters.py @@ -127,6 +127,9 @@ def setValue(self, value): self.value = bool(value) return True + def getAsScriptCode(self): + return '##' + self.name + '=boolean ' + str(self.default) + class ParameterCrs(Parameter): @@ -153,6 +156,8 @@ def setValue(self, value): def getValueAsCommandLineParameter(self): return '"' + unicode(self.value) + '"' + def getAsScriptCode(self): + return '##' + self.name + '=crs ' + str(self.default) class ParameterDataObject(Parameter): @@ -197,6 +202,8 @@ def setValue(self, text): def getValueAsCommandLineParameter(self): return '"' + unicode(self.value) + '"' + def getAsScriptCode(self): + return '##' + self.name + '=extent' class ParameterFile(Parameter): @@ -225,6 +232,12 @@ def typeName(self): else: return 'file' + def getAsScriptCode(self): + if self.isFolder: + return '##' + self.name + '=folder' + else: + return '##' + self.name + '=file' + class ParameterFixedTable(Parameter): @@ -406,6 +419,13 @@ def dataType(self): else: return 'any vectors' + def getAsScriptCode(self): + if self.datatype == self.TYPE_RASTER: + return '##' + self.name + '=multiple raster' + if self.datatype == self.TYPE_FILE: + return '##' + self.name + '=multiple file' + else: + return '##' + self.name + '=multiple vector' class ParameterNumber(Parameter): @@ -451,6 +471,9 @@ def setValue(self, n): return False + def getAsScriptCode(self): + return '##' + self.name + '=number ' + str(self.default) + class ParameterRange(Parameter): def __init__(self, name='', description='', default='0,1', optional=False): @@ -543,6 +566,9 @@ def getFileFilter(self): exts[i] = self.tr('%s files(*.%s)', 'ParameterRaster') % (exts[i].upper(), exts[i].lower()) return ';;'.join(exts) + def getAsScriptCode(self): + return '##' + self.name + '=raster' + class ParameterSelection(Parameter): @@ -609,6 +635,8 @@ def getValueAsCommandLineParameter(self): ParameterString.ESCAPED_NEWLINE)) + '"' if self.value is not None else unicode(None)) + def getAsScriptCode(self): + return '##' + self.name + '=string ' + self.default class ParameterTable(ParameterDataObject): @@ -674,6 +702,9 @@ def getFileFilter(self): exts[i] = self.tr('%s files(*.%s)', 'ParameterTable') % (exts[i].upper(), exts[i].lower()) return ';;'.join(exts) + def getAsScriptCode(self): + return '##' + self.name + '=table' + class ParameterTableField(Parameter): @@ -715,6 +746,9 @@ def dataType(self): else: return 'any' + def getAsScriptCode(self): + return '##' + self.name + '=field ' + self.parent + class ParameterVector(ParameterDataObject): @@ -800,6 +834,9 @@ def dataType(self): return types[:-2] + def getAsScriptCode(self): + return '##' + self.name + '=vector' + class ParameterGeometryPredicate(Parameter): diff --git a/python/plugins/processing/modeler/ModelerAlgorithm.py b/python/plugins/processing/modeler/ModelerAlgorithm.py index 921ea5432790..4d54a2b9fd8e 100644 --- a/python/plugins/processing/modeler/ModelerAlgorithm.py +++ b/python/plugins/processing/modeler/ModelerAlgorithm.py @@ -89,7 +89,7 @@ def __init__(self, consoleName=""): #A dict of Input object. keys are param names self.params = {} - #A dict of Output with final output descriptions. Keys are output names. + #A dict of ModelerOutput with final output descriptions. Keys are output names. #Outputs not final are not stored in this dict self.outputs = {} @@ -119,6 +119,33 @@ def setName(self, model): name = self.consoleName + "_" + unicode(i) self.name = name + def getOutputType(self, outputName): + output = self.algorithm.getOutputFromName(outputName) + return "output " + output.__class__.__name__.split(".")[-1][6:].lower() + + def toPython(self): + s = [] + params = [] + for param in self.algorithm.parameters: + value = self.params[param.name] + def _toString(v): + if isinstance(v, (ValueFromInput, ValueFromOutput)): + return v.asPythonParameter() + elif isinstance(v, basestring): + return "'%s'" % v + elif isinstance(v, list): + return "[%s]" % ",".join([_toString(val) for val in v]) + else: + return unicode(value) + params.append(_toString(value)) + for out in self.algorithm.outputs: + if out.name in self.outputs: + params.append(safeName(self.outputs[out.name].description).lower()) + else: + params.append(str(None)) + s.append("outputs_%s=processing.runalg('%s', %s)" % (self.name, self.consoleName, ",".join(params))) + return s + class ValueFromInput(): @@ -137,6 +164,8 @@ def __eq__(self, other): except: return False + def asPythonParameter(self): + return self.name class ValueFromOutput(): @@ -156,6 +185,9 @@ def __eq__(self, other): def __str__(self): return self.alg + "," + self.output + def asPythonParameter(self): + return "outputs_%s['%s']" % (self.alg, self.output) + class ModelerAlgorithm(GeoAlgorithm): @@ -654,3 +686,33 @@ def _tr(s): raise e else: raise WrongModelException(_tr('Error in model definition line: ') + '%s\n%s' % (line.strip(), traceback.format_exc())) + + + def toPython(self): + s = ['##%s=name' % self.name] + for param in self.inputs.values(): + s.append(param.param.getAsScriptCode()) + for alg in self.algs.values(): + for name, out in alg.outputs.iteritems(): + s.append('##%s=%s' % (safeName(out.description).lower(), alg.getOutputType(name))) + + executed = [] + toExecute = [alg for alg in self.algs.values() if alg.active] + while len(executed) < len(toExecute): + for alg in toExecute: + if alg.name not in executed: + canExecute = True + required = self.getDependsOnAlgorithms(alg.name) + for requiredAlg in required: + if requiredAlg != alg.name and requiredAlg not in executed: + canExecute = False + break + if canExecute: + s.extend(alg.toPython()) + executed.append(alg.name) + + return '\n'.join(s) + +def safeName(name): + validChars = 'abcdefghijklmnopqrstuvwxyz' + return ''.join(c for c in name.lower() if c in validChars) \ No newline at end of file diff --git a/python/plugins/processing/modeler/ModelerDialog.py b/python/plugins/processing/modeler/ModelerDialog.py index 96cb70820d80..8e37481798cb 100644 --- a/python/plugins/processing/modeler/ModelerDialog.py +++ b/python/plugins/processing/modeler/ModelerDialog.py @@ -39,7 +39,6 @@ from processing.core.ProcessingLog import ProcessingLog from processing.gui.HelpEditionDialog import HelpEditionDialog from processing.gui.AlgorithmDialog import AlgorithmDialog -import processing.gui.AlgorithmClassification from processing.modeler.ModelerParameterDefinitionDialog import ModelerParameterDefinitionDialog from processing.modeler.ModelerAlgorithm import ModelerAlgorithm, ModelerParameter from processing.modeler.ModelerParametersDialog import ModelerParametersDialog @@ -161,6 +160,7 @@ def _mimeDataAlgorithm(items): self.btnSave.setIcon(QgsApplication.getThemeIcon('/mActionFileSave.svg')) self.btnSaveAs.setIcon(QgsApplication.getThemeIcon('/mActionFileSaveAs.svg')) self.btnExportImage.setIcon(QgsApplication.getThemeIcon('/mActionSaveMapAsImage.png')) + self.btnExportPython.setIcon(QgsApplication.getThemeIcon('/console/iconSaveAsConsole.png')) self.btnEditHelp.setIcon(QIcon(os.path.join(pluginPath, 'images', 'edithelp.png'))) self.btnRun.setIcon(QIcon(os.path.join(pluginPath, 'images', 'runalgorithm.png'))) @@ -184,6 +184,7 @@ def _mimeDataAlgorithm(items): self.btnSave.clicked.connect(self.save) self.btnSaveAs.clicked.connect(self.saveAs) self.btnExportImage.clicked.connect(self.exportAsImage) + self.btnExportPython.clicked.connect(self.exportAsPython) self.btnEditHelp.clicked.connect(self.editHelp) self.btnRun.clicked.connect(self.runModel) @@ -281,6 +282,23 @@ def exportAsImage(self): img.save(filename) + def exportAsPython(self): + filename = unicode(QFileDialog.getSaveFileName(self, + self.tr('Save Model As Python Script'), '', + self.tr('Python files (*.py *.PY)'))) + if not filename: + return + + if not filename.lower().endswith('.py'): + filename += '.py' + + text = self.alg.toPython() + with codecs.open(filename, 'w', encoding='utf-8') as fout: + fout.write(text) + QMessageBox.information(self, self.tr('Model exported'), + self.tr('Model was correctly exported.')) + + def saveModel(self, saveAs): if unicode(self.textGroup.text()).strip() == '' \ or unicode(self.textName.text()).strip() == '': diff --git a/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py b/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py index d07da7a8d97c..d11989af3dd7 100644 --- a/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py +++ b/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py @@ -198,7 +198,7 @@ def setupUi(self): self.yesNoCombo.setCurrentIndex( 1 if self.param.optional else 0) self.verticalLayout.addLayout(self.horizontalLayout2) - + self.buttonBox = QDialogButtonBox(self) self.buttonBox.setOrientation(Qt.Horizontal) self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel @@ -221,8 +221,10 @@ def okPressed(self): validChars = \ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' safeName = ''.join(c for c in description if c in validChars) - name = self.paramType.upper().replace(' ', '') + '_' \ - + safeName.upper() + name = safeName.lower() + i = 2 + while name in self.alg.inputs: + name = safeName.lower() + str(i) else: name = self.param.name if self.paramType \ diff --git a/python/plugins/processing/ui/DlgModeler.ui b/python/plugins/processing/ui/DlgModeler.ui index e04f72e4ae67..a403d4f7e41d 100644 --- a/python/plugins/processing/ui/DlgModeler.ui +++ b/python/plugins/processing/ui/DlgModeler.ui @@ -96,6 +96,16 @@ + + + + ... + + + true + + +