4

The output of processing.run() in QGIS3 seems to be a dictionary where the 'OUTPUT' variable is a string. I am trying to access the features of the resulting layer of the following code in PyQGIS:

layer2_exploded = processing.run("native:explodelines", {
    'INPUT':layer1_line,
    'OUTPUT':'TEMPORARY_OUTPUT'
}, is_child_algorithm=True, context=context, feedback=feedback)['OUTPUT']

If I try layer2_exploded.getFeatures() I get the error that 'str' object has no attribute 'getFeatures', and if I remove the ['OUTPUT'] at the end of the code block I get 'dict' object has no attribute 'getFeatures'. So how do I access the layer, to be used with e.g. getFeatures?

2
  • If you put is_child_algorithm=False, then it shall work
    – Taras
    Jan 11, 2023 at 15:31
  • @Taras, I tried that now, but it didn't work. Still getting 'str' object has no attribute 'getFeatures' error.
    – Joel
    Jan 12, 2023 at 7:48

2 Answers 2

11

Regarding the question: "do I need to use is_child_algorithm=True?", you need to use it if the algorithm you are running is being run as a child or sub-algorithm, if you like, inside the processAlgorithm() method of a class which extends the QgsProcessingAlgorithm class. Based on your comments, I believe this is what you are doing here, however this fairly important detail is missing from your question.

Because, in the Python console you can simply do something like:

layer2_exploded = processing.run("native:explodelines",
                                {'INPUT':iface.activeLayer(),
                                'OUTPUT':'TEMPORARY_OUTPUT'})['OUTPUT']

And the return value of the run() call stored in the layer2_exploded variable will be a QgsVectorLayer object; because is_child_algorithm defaults to False, so ownership of the result layer is transferred from the processing context back to the caller.

However, when you are doing processing.run() calls to child algorithms as steps in the processAlgorithm() method of a QgsProcessingAlgorithm subclass, you must set the is_child_algorithm argument to True. This will mean that ownership remains with the context and that return value of the run() call will be a map layer id string reference. This why you are getting the error you describe.

If you need to work with a QgsVectorLayer object, you can use the method QgsProcessingContext.getMapLayer() method which returns a QgsMapLayer object from a layer id string. You can see in the source code, that this method calls the QgsProcessingUtils.mapLayerFromString() method, which you could also use.

So an example for your case is:

layer2_exploded_id = processing.run("native:explodelines",
                                {'INPUT':layer1_line,
                                'OUTPUT':'TEMPORARY_OUTPUT'},
                                context=context,
                                feedback=feedback,
                                is_child_algorithm=True)['OUTPUT']

vlyr = context.getMapLayer(layer2_exploded_id)# QgsVectorLayer object

Here is a trivial but complete, minimal example of a QgsProcessingAlgorithm making use of this method. Here we check the feature count of the vector layer returned by context.getMapLayer(), and if it is over 1000, we create a feature request limited to 1000 features, and add those to the output feature sink.

from qgis.PyQt.QtCore import QCoreApplication, QVariant

from qgis.core import (QgsFeatureSink, QgsFeatureRequest,
        QgsProcessing, QgsProcessingAlgorithm, QgsProcessingParameterFeatureSource,
        QgsProcessingParameterFeatureSink, QgsProcessingUtils)
        
import processing

class ExAlgo(QgsProcessingAlgorithm):
    INPUT = 'INPUT'
    OUTPUT = 'OUTPUT'

    def __init__(self):
        super().__init__()

    def name(self):
        return "exalgo"
    
    def tr(self, text):
        return QCoreApplication.translate("exalgo", text)
        
    def displayName(self):
        return self.tr("Example script")

    def group(self):
        return self.tr("Examples")

    def groupId(self):
        return "examples"

    def shortHelpString(self):
        return self.tr("Example script without logic")

    def helpUrl(self):
        return "https://qgis.org"
        
    def createInstance(self):
        return type(self)()
  
    def initAlgorithm(self, config=None):
        self.addParameter(QgsProcessingParameterFeatureSource(
            self.INPUT,
            self.tr("Input layer"),
            [QgsProcessing.TypeVectorLine]))

        self.addParameter(QgsProcessingParameterFeatureSink(
            self.OUTPUT,
            self.tr("Output layer"),
            QgsProcessing.TypeVectorLine))

    def processAlgorithm(self, parameters, context, feedback):
        source = self.parameterAsSource(parameters, self.INPUT, context)
        
        layer1_line = source.materialize(QgsFeatureRequest(), feedback=feedback)

        (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
                                               source.fields(), source.wkbType(), source.sourceCrs())

        layer2_exploded_id = processing.run("native:explodelines",
                                        {'INPUT':layer1_line,
                                        'OUTPUT':'TEMPORARY_OUTPUT'},
                                        context=context,
                                        feedback=feedback,
                                        is_child_algorithm=True)['OUTPUT']
        
        vlyr = context.getMapLayer(layer2_exploded_id)
        
        feat_request = QgsFeatureRequest()
        
        if vlyr.featureCount() > 1000:
            feat_request.setLimit(1000)
            
        for f in vlyr.getFeatures(feat_request):
            sink.addFeature(f, QgsFeatureSink.FastInsert)

        return {'self.OUTPUT': dest_id}

The QGIS processing framework is a wonderful and versatile tool, but there is a fairly steep learning curve with it and many gotchas. For example, QGIS Processing Algorithms are run by default in a background thread. Therefore it is not thread-safe to interact with the canvas or interface from inside the processAlgorithm() method. While it is possible to re-implement the flags() method and return the FlagNoThreading flag, I don't recommend doing anything non thread safe with your QgsVectorLayer object from inside the processAlgorithm() method. While getFeatures() is fine, don't do anything which interacts with the interface such as applying a style to the layer object etc. For post processing of any output layers which will be loaded on completion, there is the QgsProcessingLayerPostProcessorInterface class. You can see an example here.

There is also the postProcessAlgorithm method, which should be called from the main thread and can be re-implemented to perform any thread-sensitive cleanup tasks.

I hope all this information does not confuse you too much. Most of this is probably not relevant to you at the moment. I am just trying to make you aware of things to watch out for if you get more in depth with writing processing scripts.

2
  • Thank you very much for this in-depth answer! Solved the problem, and makes it much easier to remember when you start to understand the reasons for the different approaches. +1!
    – Joel
    Jan 16, 2023 at 8:53
  • Thanks @ben-w! Couldn't figure out why the output of Extract by Location is a string, but I could get the layer by using QgsProcessingUtils.mapLayerFromString() with context=context
    – Andre Geo
    Jun 5, 2023 at 9:26
0

Do you need "is_child_algorithm=True, context=context, feedback=feedback"?

You can use the output of a processing algorithm with:

algo = "native:explodelines"
params = {'INPUT':iface.activeLayer(),'OUTPUT':'TEMPORARY_OUTPUT'}

output = processing.run(algo, params)['OUTPUT']
print(output.featureCount())
1
  • I'm not really sure whether I need them, I'm a beginner just trying to follow the guidelines that I read about. I have lots of other processing.run() blocks in a sequence, and therefore I should set it to True, according to what I read here. Although I tried to change only this code block to is_child_algorithm=True and the whole thing crashed when I ran it. How do I know when to use True and when to use False for this parameter? What effect does it have?
    – Joel
    Jan 11, 2023 at 16:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.