diff --git a/python/core/processing/qgsprocessingmodelalgorithm.sip b/python/core/processing/qgsprocessingmodelalgorithm.sip index 1948d83d443..7bc037e89b9 100644 --- a/python/core/processing/qgsprocessingmodelalgorithm.sip +++ b/python/core/processing/qgsprocessingmodelalgorithm.sip @@ -153,6 +153,12 @@ class QgsProcessingModelAlgorithm : QgsProcessingAlgorithm :rtype: bool %End + QString asPythonCode() const; +%Docstring + Attempts to convert the source to executable Python code. + :rtype: str +%End + }; class Component @@ -549,6 +555,12 @@ Copies are protected to avoid slicing :rtype: bool %End + QString asPythonCode() const; +%Docstring + Attempts to convert the child to executable Python code. + :rtype: str +%End + }; QgsProcessingModelAlgorithm( const QString &name = QString(), const QString &group = QString() ); @@ -802,6 +814,12 @@ Copies are protected to avoid slicing .. seealso:: sourceFilePath() %End + QString asPythonCode() const; +%Docstring + Attempts to convert the model to executable Python code. + :rtype: str +%End + protected: virtual QVariantMap processAlgorithm( const QVariantMap ¶meters, diff --git a/python/plugins/processing/modeler/ModelerAlgorithm.py b/python/plugins/processing/modeler/ModelerAlgorithm.py index 0753516189d..4a39437a973 100644 --- a/python/plugins/processing/modeler/ModelerAlgorithm.py +++ b/python/plugins/processing/modeler/ModelerAlgorithm.py @@ -66,47 +66,6 @@ from processing.gui.Help2Html import getHtmlFromDescriptionsDict pluginPath = os.path.split(os.path.dirname(__file__))[0] -class Algorithm(QgsProcessingModelAlgorithm.ChildAlgorithm): - - def __init__(self, consoleName=None): - super().__init__(consoleName) - - def todict(self): - return {k: v for k, v in list(self.__dict__.items()) if not k.startswith("_")} - - def getOutputType(self, outputName): - output = self.algorithm().outputDefinition(outputName) - return "output " + output.__class__.__name__.split(".")[-1][6:].lower() - - def toPython(self): - s = [] - params = [] - if not self.algorithm(): - return None - - for param in self.algorithm().parameterDefinitions(): - value = self.parameterSources()[param.name()] - - def _toString(v): - if isinstance(v, (ValueFromInput, ValueFromOutput)): - return v.asPythonParameter() - elif isinstance(v, str): - return "\\n".join(("'%s'" % v).splitlines()) - elif isinstance(v, list): - return "[%s]" % ",".join([_toString(val) for val in v]) - else: - return str(value) - params.append(_toString(value)) - for out in self.algorithm().outputs: - if not out.flags() & QgsProcessingParameterDefinition.FlagHidden: - 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.run('%s', %s)" % (self.childId(), self.algorithmId(), ",".join(params))) - return s - - class ValueFromInput(object): def __init__(self, name=""): @@ -124,9 +83,6 @@ class ValueFromInput(object): except: return False - def asPythonParameter(self): - return self.name - class ValueFromOutput(object): @@ -146,9 +102,6 @@ class ValueFromOutput(object): def __str__(self): return self.alg + ":" + self.output - def asPythonParameter(self): - return "outputs_%s['%s']" % (self.alg, self.output) - class CompoundValue(object): @@ -223,33 +176,3 @@ class ModelerAlgorithm(QgsProcessingModelAlgorithm): else: v = value return param.evaluateForModeler(v, self) - - def toPython(self): - s = ['##%s=name' % self.name()] - for param in list(self.parameterComponents().values()): - s.append(param.param.asScriptCode()) - for alg in list(self.algs.values()): - for name, out in list(alg.modelOutputs().items()): - s.append('##%s=%s' % (safeName(out.description()).lower(), alg.getOutputType(name))) - - executed = [] - toExecute = [alg for alg in list(self.algs.values()) if alg.isActive()] - while len(executed) < len(toExecute): - for alg in toExecute: - if alg.childId() not in executed: - canExecute = True - required = self.dependsOnChildAlgorithms(alg.childId()) - for requiredAlg in required: - if requiredAlg != alg.childId() and requiredAlg not in executed: - canExecute = False - break - if canExecute: - s.extend(alg.toPython()) - executed.append(alg.childId()) - - return '\n'.join(s) - - -def safeName(name): - validChars = 'abcdefghijklmnopqrstuvwxyz' - return ''.join(c for c in name.lower() if c in validChars) diff --git a/python/plugins/processing/modeler/ModelerDialog.py b/python/plugins/processing/modeler/ModelerDialog.py index c272a649a98..917459439c5 100644 --- a/python/plugins/processing/modeler/ModelerDialog.py +++ b/python/plugins/processing/modeler/ModelerDialog.py @@ -425,7 +425,7 @@ class ModelerDialog(BASE, WIDGET): if not filename.lower().endswith('.py'): filename += '.py' - text = self.model.toPython() + text = self.model.asPythonCode() with codecs.open(filename, 'w', encoding='utf-8') as fout: fout.write(text) diff --git a/src/core/processing/qgsprocessingmodelalgorithm.cpp b/src/core/processing/qgsprocessingmodelalgorithm.cpp index a82641ee469..4532eddd2db 100644 --- a/src/core/processing/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/qgsprocessingmodelalgorithm.cpp @@ -213,6 +213,33 @@ bool QgsProcessingModelAlgorithm::ChildAlgorithm::loadVariant( const QVariant &c return true; } +QString QgsProcessingModelAlgorithm::ChildAlgorithm::asPythonCode() const +{ + QStringList lines; + + if ( !algorithm() ) + return QString(); + + QStringList paramParts; + QMap< QString, QgsProcessingModelAlgorithm::ChildParameterSource >::const_iterator paramIt = mParams.constBegin(); + for ( ; paramIt != mParams.constEnd(); ++paramIt ) + { + QString part = paramIt->asPythonCode(); + if ( !part.isEmpty() ) + paramParts << QStringLiteral( "'%1':%2" ).arg( paramIt.key(), part ); + } + + lines << QStringLiteral( "outputs['%1']=processing.run('%2', {%3}, context=context, feedback=feedback)" ).arg( mId, mAlgorithmId, paramParts.join( ',' ) ); + + QMap< QString, QgsProcessingModelAlgorithm::ModelOutput >::const_iterator outputIt = mModelOutputs.constBegin(); + for ( ; outputIt != mModelOutputs.constEnd(); ++outputIt ) + { + lines << QStringLiteral( "results['%1']=outputs['%2']['%3']" ).arg( outputIt.key(), mId, outputIt.value().childOutputName() ); + } + + return lines.join( '\n' ); +} + bool QgsProcessingModelAlgorithm::ChildAlgorithm::parametersCollapsed() const { return mParametersCollapsed; @@ -491,6 +518,92 @@ void QgsProcessingModelAlgorithm::setSourceFilePath( const QString &sourceFile ) mSourceFile = sourceFile; } +QString QgsProcessingModelAlgorithm::asPythonCode() const +{ + QStringList lines; + lines << QStringLiteral( "##%1=name" ).arg( name() ); + + QMap< QString, ModelParameter >::const_iterator paramIt = mParameterComponents.constBegin(); + for ( ; paramIt != mParameterComponents.constEnd(); ++paramIt ) + { + QString name = paramIt.value().parameterName(); + if ( parameterDefinition( name ) ) + { + lines << parameterDefinition( name )->asScriptCode(); + } + } + + auto safeName = []( const QString & name )->QString + { + QString n = name.toLower().trimmed(); + QRegularExpression rx( "[^a-z_]" ); + n.replace( rx, QString() ); + return n; + }; + + QMap< QString, ChildAlgorithm >::const_iterator childIt = mChildAlgorithms.constBegin(); + for ( ; childIt != mChildAlgorithms.constEnd(); ++childIt ) + { + if ( !childIt->isActive() || !childIt->algorithm() ) + continue; + + // look through all outputs for child + QMap outputs = childIt->modelOutputs(); + QMap::const_iterator outputIt = outputs.constBegin(); + for ( ; outputIt != outputs.constEnd(); ++outputIt ) + { + const QgsProcessingOutputDefinition *output = childIt->algorithm()->outputDefinition( outputIt->childOutputName() ); + lines << QStringLiteral( "##%1=output %2" ).arg( safeName( outputIt->name() ), output->type() ); + } + } + + lines << QStringLiteral( "results={}" ); + + QSet< QString > toExecute; + childIt = mChildAlgorithms.constBegin(); + for ( ; childIt != mChildAlgorithms.constEnd(); ++childIt ) + { + if ( childIt->isActive() && childIt->algorithm() ) + toExecute.insert( childIt->childId() ); + } + + QSet< QString > executed; + bool executedAlg = true; + while ( executedAlg && executed.count() < toExecute.count() ) + { + executedAlg = false; + Q_FOREACH ( const QString &childId, toExecute ) + { + if ( executed.contains( childId ) ) + continue; + + bool canExecute = true; + Q_FOREACH ( const QString &dependency, dependsOnChildAlgorithms( childId ) ) + { + if ( !executed.contains( dependency ) ) + { + canExecute = false; + break; + } + } + + if ( !canExecute ) + continue; + + executedAlg = true; + + const ChildAlgorithm &child = mChildAlgorithms[ childId ]; + lines << child.asPythonCode(); + + executed.insert( childId ); + } + } + + lines << QStringLiteral( "return results" ); + + return lines.join( '\n' ); +} + QVariantMap QgsProcessingModelAlgorithm::helpContent() const { return mHelpContent; @@ -1014,6 +1127,22 @@ bool QgsProcessingModelAlgorithm::ChildParameterSource::loadVariant( const QVari return true; } +QString QgsProcessingModelAlgorithm::ChildParameterSource::asPythonCode() const +{ + switch ( mSource ) + { + case ModelParameter: + return QStringLiteral( "parameters['%1']" ).arg( mParameterName ); + + case ChildOutput: + return QStringLiteral( "outputs['%1']['%2']" ).arg( mChildId, mOutputName ); + + case StaticValue: + return mStaticValue.toString(); + } + return QString(); +} + QgsProcessingModelAlgorithm::ModelOutput::ModelOutput( const QString &name, const QString &description ) : QgsProcessingModelAlgorithm::Component( description ) , mName( name ) diff --git a/src/core/processing/qgsprocessingmodelalgorithm.h b/src/core/processing/qgsprocessingmodelalgorithm.h index f50df11a9a7..46517ede121 100644 --- a/src/core/processing/qgsprocessingmodelalgorithm.h +++ b/src/core/processing/qgsprocessingmodelalgorithm.h @@ -152,6 +152,11 @@ class CORE_EXPORT QgsProcessingModelAlgorithm : public QgsProcessingAlgorithm */ bool loadVariant( const QVariantMap &map ); + /** + * Attempts to convert the source to executable Python code. + */ + QString asPythonCode() const; + private: Source mSource = StaticValue; @@ -536,6 +541,11 @@ class CORE_EXPORT QgsProcessingModelAlgorithm : public QgsProcessingAlgorithm */ bool loadVariant( const QVariant &child ); + /** + * Attempts to convert the child to executable Python code. + */ + QString asPythonCode() const; + private: QString mId; @@ -793,6 +803,11 @@ class CORE_EXPORT QgsProcessingModelAlgorithm : public QgsProcessingAlgorithm */ void setSourceFilePath( const QString &path ); + /** + * Attempts to convert the model to executable Python code. + */ + QString asPythonCode() const; + protected: QVariantMap processAlgorithm( const QVariantMap ¶meters, diff --git a/tests/src/core/testqgsprocessing.cpp b/tests/src/core/testqgsprocessing.cpp index 36b38a3d9d9..5caa05f67de 100644 --- a/tests/src/core/testqgsprocessing.cpp +++ b/tests/src/core/testqgsprocessing.cpp @@ -4639,12 +4639,26 @@ void TestQgsProcessing::modelExecution() alg2c3.setAlgorithmId( "native:extractbyexpression" ); alg2c3.addParameterSource( "INPUT", QgsProcessingModelAlgorithm::ChildParameterSource::fromChildOutput( "cx1", "OUTPUT_LAYER" ) ); alg2c3.addParameterSource( "EXPRESSION", QgsProcessingModelAlgorithm::ChildParameterSource::fromStaticValue( "true" ) ); + alg2c3.setDependencies( QStringList() << "cx2" ); model2.addChildAlgorithm( alg2c3 ); params = model2.parametersForChildAlgorithm( model2.childAlgorithm( "cx3" ), modelInputs, childResults ); QCOMPARE( params.value( "INPUT" ).toString(), QStringLiteral( "dest.shp" ) ); QCOMPARE( params.value( "EXPRESSION" ).toString(), QStringLiteral( "true" ) ); QCOMPARE( params.value( "OUTPUT" ).toString(), QStringLiteral( "memory:" ) ); QCOMPARE( params.count(), 3 ); // don't want FAIL_OUTPUT set! + + QStringList actualParts = model2.asPythonCode().split( '\n' ); + QStringList expectedParts = QStringLiteral( "##model=name\n" + "##DIST=number\n" + "##SOURCE_LAYER=source\n" + "##model_out_layer=output outputVector\n" + "results={}\n" + "outputs['cx1']=processing.run('native:buffer', {'DISSOLVE':false,'DISTANCE':parameters['DIST'],'END_CAP_STYLE':1,'INPUT':parameters['SOURCE_LAYER'],'JOIN_STYLE':2,'SEGMENTS':16}, context=context, feedback=feedback)\n" + "results['MODEL_OUT_LAYER']=outputs['cx1']['OUTPUT_LAYER']\n" + "outputs['cx2']=processing.run('native:centroids', {'INPUT':outputs['cx1']['OUTPUT_LAYER']}, context=context, feedback=feedback)\n" + "outputs['cx3']=processing.run('native:extractbyexpression', {'EXPRESSION':true,'INPUT':outputs['cx1']['OUTPUT_LAYER']}, context=context, feedback=feedback)\n" + "return results" ).split( '\n' ); + QCOMPARE( actualParts, expectedParts ); } void TestQgsProcessing::tempUtils()