Skip to content

Commit

Permalink
[processing] Update QgsProcessingModelAlgorithm::asPythonCode for 3.x…
Browse files Browse the repository at this point in the history
… API
  • Loading branch information
nyalldawson committed Feb 1, 2019
1 parent fb519ea commit 9b2e601
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,13 @@ Sets the source file ``path`` for the model, if available.
.. seealso:: :py:func:`sourceFilePath`
%End

QString asPythonCode() const;
QStringList asPythonCode( QgsProcessing::PythonOutputType outputType, int indentSize ) const;
%Docstring
Attempts to convert the model to executable Python code.
Attempts to convert the model to executable Python code, and returns the generated lines of code.

The ``outputType`` argument dictates the desired script type.

The ``indentSize`` arguments specifies the size of indented lines.
%End

QList< QgsProcessingModelChildParameterSource > availableSourcesForChild( const QString &childId, const QStringList &parameterTypes = QStringList(),
Expand Down
201 changes: 167 additions & 34 deletions src/core/processing/models/qgsprocessingmodelalgorithm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -353,50 +353,132 @@ void QgsProcessingModelAlgorithm::setSourceFilePath( const QString &sourceFile )
mSourceFile = sourceFile;
}

QString QgsProcessingModelAlgorithm::asPythonCode() const
QStringList QgsProcessingModelAlgorithm::asPythonCode( const QgsProcessing::PythonOutputType outputType, const int indentSize ) const
{
QStringList lines;
lines << QStringLiteral( "##%1=name" ).arg( name() );

QMap< QString, QgsProcessingModelParameter >::const_iterator paramIt = mParameterComponents.constBegin();
for ( ; paramIt != mParameterComponents.constEnd(); ++paramIt )
QString indent = QString( ' ' ).repeated( indentSize );
QString currentIndent;
switch ( outputType )
{
QString name = paramIt.value().parameterName();
if ( parameterDefinition( name ) )
case QgsProcessing::PythonQgsProcessingAlgorithmSubclass:
{
lines << parameterDefinition( name )->asScriptCode();
}
}
lines << QStringLiteral( "from qgis.core import QgsProcessing" );
lines << QStringLiteral( "from qgis.core import QgsProcessingAlgorithm" );
// add specific parameter type imports
const auto params = parameterDefinitions();
QStringList importLines; // not a set - we need regular ordering
for ( const QgsProcessingParameterDefinition *def : params )
{
const QString importString = QgsApplication::processingRegistry()->parameterType( def->type() )->pythonImportString();
if ( !importString.isEmpty() && !importLines.contains( importString ) )
importLines << importString;
}
lines << importLines;
lines << QStringLiteral( "import processing" );
lines << QString() << QString();

auto safeName = []( const QString & name )->QString
{
QString n = name.toLower().trimmed();
QRegularExpression rx( QStringLiteral( "[^a-z_]" ) );
n.replace( rx, QString() );
return n;
};
auto safeName = []( const QString & name )->QString
{
QString n = name.toLower().trimmed();
QRegularExpression rx( QStringLiteral( "[^a-z_A-Z0-9]" ) );
n.replace( rx, QString() );
return n;
};

const QString algorithmClassName = safeName( name() );
lines << QStringLiteral( "class %1(QgsProcessingAlgorithm):" ).arg( algorithmClassName );

// createInstance
lines << indent + QStringLiteral( "def createInstance(self):" );
lines << indent + indent + QStringLiteral( "return %1()" ).arg( algorithmClassName );
lines << QString();

// name, displayName
lines << indent + QStringLiteral( "def name(self):" );
lines << indent + indent + QStringLiteral( "return '%1'" ).arg( mModelName );
lines << QString();
lines << indent + QStringLiteral( "def displayName(self):" );
lines << indent + indent + QStringLiteral( "return '%1'" ).arg( mModelName );
lines << QString();

// group, groupId
lines << indent + QStringLiteral( "def group(self):" );
lines << indent + indent + QStringLiteral( "return '%1'" ).arg( mModelGroup );
lines << QString();
lines << indent + QStringLiteral( "def groupId(self):" );
lines << indent + indent + QStringLiteral( "return '%1'" ).arg( mModelGroupId );
lines << QString();

// help
if ( !shortHelpString().isEmpty() )
{
lines << indent + QStringLiteral( "def shortHelpString(self):" );
lines << indent + indent + QStringLiteral( "return '%1'" ).arg( shortHelpString() );
lines << QString();
}
if ( !helpUrl().isEmpty() )
{
lines << indent + QStringLiteral( "def helpUrl(self):" );
lines << indent + indent + QStringLiteral( "return '%1'" ).arg( helpUrl() );
lines << QString();
}

QMap< QString, QgsProcessingModelChildAlgorithm >::const_iterator childIt = mChildAlgorithms.constBegin();
for ( ; childIt != mChildAlgorithms.constEnd(); ++childIt )
{
if ( !childIt->isActive() || !childIt->algorithm() )
continue;
// initAlgorithm, parameter definitions
lines << indent + QStringLiteral( "def initAlgorithm(self, config=None):" );
lines.reserve( lines.size() + params.size() );
for ( const QgsProcessingParameterDefinition *def : params )
{
lines << indent + indent + QStringLiteral( "self.addParameter(%1)" ).arg( def->asPythonString() );
}

// look through all outputs for child
QMap<QString, QgsProcessingModelOutput> outputs = childIt->modelOutputs();
QMap<QString, QgsProcessingModelOutput>::const_iterator outputIt = outputs.constBegin();
for ( ; outputIt != outputs.constEnd(); ++outputIt )
lines << QString();
lines << indent + QStringLiteral( "def processAlgorithm(self, parameters, context, feedback):" );
currentIndent = indent + indent;
break;
}
#if 0
case Script:
{
const QgsProcessingOutputDefinition *output = childIt->algorithm()->outputDefinition( outputIt->childOutputName() );
lines << QStringLiteral( "##%1=output %2" ).arg( safeName( outputIt->name() ), output->type() );
QgsStringMap params;
QgsProcessingContext context;
QMap< QString, QgsProcessingModelParameter >::const_iterator paramIt = mParameterComponents.constBegin();
for ( ; paramIt != mParameterComponents.constEnd(); ++paramIt )
{
QString name = paramIt.value().parameterName();
if ( parameterDefinition( name ) )
{
// TODO - generic value to string method
params.insert( name, parameterDefinition( name )->valueAsPythonString( parameterDefinition( name )->defaultValue(), context ) );
}
}

if ( !params.isEmpty() )
{
lines << QStringLiteral( "parameters = {" );
for ( auto it = params.constBegin(); it != params.constEnd(); ++it )
{
lines << QStringLiteral( " '%1':%2," ).arg( it.key(), it.value() );
}
lines << QStringLiteral( "}" )
<< QString();
}

lines << QStringLiteral( "context = QgsProcessingContext()" )
<< QStringLiteral( "context.setProject(QgsProject.instance())" )
<< QStringLiteral( "feedback = QgsProcessingFeedback()" )
<< QString();

break;
}
#endif

}

lines << QStringLiteral( "results={}" );
lines << currentIndent + QStringLiteral( "results={}" );
lines << currentIndent + QStringLiteral( "outputs={}" );

QSet< QString > toExecute;
childIt = mChildAlgorithms.constBegin();
for ( ; childIt != mChildAlgorithms.constEnd(); ++childIt )
for ( auto childIt = mChildAlgorithms.constBegin(); childIt != mChildAlgorithms.constEnd(); ++childIt )
{
if ( childIt->isActive() && childIt->algorithm() )
toExecute.insert( childIt->childId() );
Expand Down Expand Up @@ -428,15 +510,66 @@ QString QgsProcessingModelAlgorithm::asPythonCode() const
executedAlg = true;

const QgsProcessingModelChildAlgorithm &child = mChildAlgorithms[ childId ];
lines << child.asPythonCode();

// fill in temporary outputs
const QgsProcessingParameterDefinitions childDefs = child.algorithm()->parameterDefinitions();
QgsStringMap childParams;
for ( const QgsProcessingParameterDefinition *def : childDefs )
{
if ( def->isDestination() )
{
const QgsProcessingDestinationParameter *destParam = static_cast< const QgsProcessingDestinationParameter * >( def );

// is destination linked to one of the final outputs from this model?
bool isFinalOutput = false;
QMap<QString, QgsProcessingModelOutput> outputs = child.modelOutputs();
QMap<QString, QgsProcessingModelOutput>::const_iterator outputIt = outputs.constBegin();
for ( ; outputIt != outputs.constEnd(); ++outputIt )
{
if ( outputIt->childOutputName() == destParam->name() )
{
QString paramName = child.childId() + ':' + outputIt.key();
childParams.insert( destParam->name(), QStringLiteral( "parameters['%1']" ).arg( paramName ) );
isFinalOutput = true;
break;
}
}

if ( !isFinalOutput )
{
// output is temporary

// check whether it's optional, and if so - is it required?
bool required = true;
if ( destParam->flags() & QgsProcessingParameterDefinition::FlagOptional )
{
required = childOutputIsRequired( child.childId(), destParam->name() );
}

// not optional, or required elsewhere in model
if ( required )
{

childParams.insert( destParam->name(), QStringLiteral( "QgsProcessing.TEMPORARY_OUTPUT" ) );
}
}
}
}

lines << child.asPythonCode( outputType, childParams, currentIndent.size(), indentSize );

executed.insert( childId );
}
}

lines << QStringLiteral( "return results" );
switch ( outputType )
{
case QgsProcessing::PythonQgsProcessingAlgorithmSubclass:
lines << currentIndent + QStringLiteral( "return results" );
break;
}

return lines.join( '\n' );
return lines;
}

QMap<QString, QgsProcessingModelAlgorithm::VariableDefinition> QgsProcessingModelAlgorithm::variablesForChildAlgorithm( const QString &childId, QgsProcessingContext &context, const QVariantMap &modelParameters, const QVariantMap &results ) const
Expand Down
8 changes: 6 additions & 2 deletions src/core/processing/models/qgsprocessingmodelalgorithm.h
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,13 @@ class CORE_EXPORT QgsProcessingModelAlgorithm : public QgsProcessingAlgorithm
void setSourceFilePath( const QString &path );

/**
* Attempts to convert the model to executable Python code.
* Attempts to convert the model to executable Python code, and returns the generated lines of code.
*
* The \a outputType argument dictates the desired script type.
*
* The \a indentSize arguments specifies the size of indented lines.
*/
QString asPythonCode() const;
QStringList asPythonCode( QgsProcessing::PythonOutputType outputType, int indentSize ) const;

/**
* Returns a list of possible sources which can be used for the parameters for a child
Expand Down
75 changes: 60 additions & 15 deletions tests/src/analysis/testqgsprocessing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6244,10 +6244,8 @@ void TestQgsProcessing::modelerAlgorithm()
child.removeModelOutput( QStringLiteral( "a" ) );
QCOMPARE( child.modelOutputs().count(), 1 );


// model algorithm tests


QgsProcessingModelAlgorithm alg( "test", "testGroup" );
QCOMPARE( alg.name(), QStringLiteral( "test" ) );
QCOMPARE( alg.displayName(), QStringLiteral( "test" ) );
Expand Down Expand Up @@ -6948,19 +6946,66 @@ void TestQgsProcessing::modelExecution()
QGSCOMPARENEAR( variables.value( "SOURCE_LAYER_maxx" ).value.toDouble(), -83.3333, 0.001 );
QGSCOMPARENEAR( variables.value( "SOURCE_LAYER_maxy" ).value.toDouble(), 46.8719, 0.001 );

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"
"##my_out=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':QgsExpression('@myvar*2').evaluate()}, context=context, feedback=feedback)\n"
"results['MODEL_OUT_LAYER']=outputs['cx1']['OUTPUT']\n"
"outputs['cx2']=processing.run('native:centroids', {'INPUT':outputs['cx1']['OUTPUT']}, context=context, feedback=feedback)\n"
"outputs['cx3']=processing.run('native:extractbyexpression', {'EXPRESSION':true,'INPUT':outputs['cx1']['OUTPUT'],'OUTPUT':parameters['MY_OUT']}, context=context, feedback=feedback)\n"
"results['MY_OUT']=outputs['cx3']['OUTPUT']\n"
"return results" ).split( '\n' );
QStringList actualParts = model2.asPythonCode( QgsProcessing::PythonQgsProcessingAlgorithmSubclass, 2 );
QgsDebugMsg( actualParts.join( '\n' ) );
QStringList expectedParts = QStringLiteral( "from qgis.core import QgsProcessing\n"
"from qgis.core import QgsProcessingAlgorithm\n"
"from qgis.core import QgsProcessingParameterFeatureSource\n"
"from qgis.core import QgsProcessingParameterNumber\n"
"from qgis.core import QgsProcessingParameterFeatureSink\n"
"import processing\n"
"\n"
"\n"
"class model(QgsProcessingAlgorithm):\n"
" def createInstance(self):\n"
" return model()\n"
"\n"
" def name(self):\n"
" return 'model'\n"
"\n"
" def displayName(self):\n"
" return 'model'\n"
"\n"
" def group(self):\n"
" return ''\n"
"\n"
" def groupId(self):\n"
" return ''\n"
"\n"
" def initAlgorithm(self, config=None):\n"
" self.addParameter(QgsProcessingParameterFeatureSource('SOURCE_LAYER', '', defaultValue=None))\n"
" self.addParameter(QgsProcessingParameterNumber('DIST', '', type=QgsProcessingParameterNumber.Double, defaultValue=None))\n"
" self.addParameter(QgsProcessingParameterFeatureSink('cx1:MODEL_OUT_LAYER', '', type=QgsProcessing.TypeVectorPolygon, createByDefault=True, defaultValue=None))\n"
" self.addParameter(QgsProcessingParameterFeatureSink('cx3:MY_OUT', '', type=QgsProcessing.TypeVectorAnyGeometry, createByDefault=True, defaultValue=None))\n"
"\n"
" def processAlgorithm(self, parameters, context, feedback):\n"
" results={}\n"
" outputs={}\n"
" alg_params = {\n"
" 'DISSOLVE':False,\n"
" 'DISTANCE':parameters['DIST'],\n"
" 'END_CAP_STYLE':1,\n"
" 'INPUT':parameters['SOURCE_LAYER'],\n"
" 'JOIN_STYLE':2,\n"
" 'SEGMENTS':QgsExpression('@myvar*2').evaluate(),\n"
" 'OUTPUT':parameters['cx1:MODEL_OUT_LAYER'],\n"
" }\n"
" outputs['cx1']=processing.run('native:buffer', alg_params, context=context, feedback=feedback, is_child_algorithm=True)\n"
" results['cx1:MODEL_OUT_LAYER']=outputs['cx1']['OUTPUT']\n"
" alg_params = {\n"
" 'INPUT':outputs['cx1']['OUTPUT'],\n"
" 'OUTPUT':QgsProcessing.TEMPORARY_OUTPUT,\n"
" }\n"
" outputs['cx2']=processing.run('native:centroids', alg_params, context=context, feedback=feedback, is_child_algorithm=True)\n"
" alg_params = {\n"
" 'EXPRESSION':'true',\n"
" 'INPUT':outputs['cx1']['OUTPUT'],\n"
" 'OUTPUT':parameters['MY_OUT'],\n"
" 'OUTPUT':parameters['cx3:MY_OUT'],\n"
" }\n"
" outputs['cx3']=processing.run('native:extractbyexpression', alg_params, context=context, feedback=feedback, is_child_algorithm=True)\n"
" results['cx3:MY_OUT']=outputs['cx3']['OUTPUT']\n"
" return results" ).split( '\n' );
QCOMPARE( actualParts, expectedParts );
}

Expand Down

0 comments on commit 9b2e601

Please sign in to comment.