5

I'm writing some scripts in QGIS 3 to automate processing using a number of the inbuilt algorithms. I'm using memory layers to store the intermediate results of the algorithms. I'm looking for a simple way to persist the layer into the layer list in the project once the algorithms have been executed.

At the moment, the only way I can work out how to persist the final processed layer is to use one of the algorithms (such as native:merge) as a post-processing step. The issue I have is that it adds it the the layer list with the name "merged". I would like to be able to define the name that it gets in the layers list.

I've looked at alternatives such as addMapLayer() but it doesn't add the layer to the project. In the example below I want the layer to be added with the name "layername".

The script is being added as an algorithm under the processing toolbox.

# -*- coding: utf-8 -*-

from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (QgsProcessing,
                       QgsFeatureSink,
                       QgsProcessingException,
                       QgsProcessingAlgorithm,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterFeatureSink,
                       QgsVectorLayer,
                       QgsProject)
from qgis import processing


class ExampleProcessingAlgorithm(QgsProcessingAlgorithm):
    INPUT1 = 'INPUT1'
    def tr(self, string):
        return QCoreApplication.translate('Processing', string)
    def createInstance(self):
        return ExampleProcessingAlgorithm()
    def name(self):
        return 'testpersistence'
    def displayName(self):
        return self.tr('Testpersistence')
    def group(self):
        return self.tr('Example scripts')
    def groupId(self):
        return 'examplescripts'
    def shortHelpString(self):
        return self.tr("Testing the script")

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT1,
                self.tr('Input layer 1'),
                [QgsProcessing.TypeVectorAnyGeometry]
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        source1 = self.parameterAsSource(parameters,self.INPUT1,context)
        fixgeometries1_result = processing.run(
            'native:fixgeometries',
            {
                'INPUT': parameters['INPUT1'],
                'OUTPUT': 'memory:'
            },
            is_child_algorithm=True,
            context=context,
            feedback=feedback)

        uri = fixgeometries1_result['OUTPUT'].dataProvider().dataSourceUri()
        layer1 = QgsVectorLayer(uri,"layername","memory")
        QgsProject.instance().addMapLayer(layer1)

        return {}

      

The code fails with an error:

AttributeError: 'str' object has no attribute 'dataProvider'

3
  • We definitely need some more details about how the snippet of code you have posted here fits into the the rest of the script you are running and how you are trying to run it! Looking at what you have posted, it looks like either code generated from a processing model which you have exported as a Python script or you are writing your own processing script which inherits from QgsProcessingAlgorithm. In either case it should be added as a script to the Processing toolbox, to be available as an algorithm. Is that what you are doing? Or are you trying to run a script from the Python console?
    – Ben W
    Commented Jul 26, 2021 at 7:59
  • @Ben W - I've updated the original text with the full example from my processing toolbox script. As you suggested, I am running it as an algorithm (not running from the Python console).
    – Danny
    Commented Jul 26, 2021 at 8:26
  • yes- that helps a lot. In this case renaming the output layer and loading it to the Table of Contents is a little more involved. I have edited my answer with a working, modified version of your code.
    – Ben W
    Commented Jul 26, 2021 at 9:37

2 Answers 2

4

There are a few problems with your script. Firstly, to define a layer to load on completion, you must an additional parameter of type QgsProcessingParameterFeatureSink in the initAlgorithm() method.

Then, when you call the native fix geometries algorithm inside the processAlgorithm() method, the output parameter should be: parameters['OUTPUT'].

That will give you the checkbox in the dialog: 'Open output file after running algorithm'

Renaming the output file is a little more involved. To do this you need to add a Layer Post Processor class and rename the layer inside this class. Then create an instance of this class at the end of the processAlgorithm() method and pass it to the setPostProcessor() method chained to the layerToLoadOnCompletionDetails() method of the QgsProcessingContext object.

Working modified script below (maybe not perfect but it's working!):

# -*- coding: utf-8 -*-

from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (QgsProcessing,
                       QgsFeatureSink,
                       QgsProcessingException,
                       QgsProcessingAlgorithm,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterFeatureSink,
                       QgsVectorLayer,
                       QgsProcessingLayerPostProcessorInterface)
from qgis import processing


class ExampleProcessingAlgorithm(QgsProcessingAlgorithm):
    INPUT1 = 'INPUT1'
    OUTPUT = 'OUTPUT'
    def tr(self, string):
        return QCoreApplication.translate('Processing', string)
    def createInstance(self):
        return ExampleProcessingAlgorithm()
    def name(self):
        return 'testpersistence'
    def displayName(self):
        return self.tr('Testpersistence')
    def group(self):
        return self.tr('Example scripts')
    def groupId(self):
        return 'examplescripts'
    def shortHelpString(self):
        return self.tr("Testing the script")

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT1,
                self.tr('Input layer 1'),
                [QgsProcessing.TypeVectorAnyGeometry]
            )
        )
        self.addParameter(QgsProcessingParameterFeatureSink(
            self.OUTPUT,
            self.tr("Output layer"),
            QgsProcessing.TypeVectorAnyGeometry))
        
    def processAlgorithm(self, parameters, context, feedback):
        source1 = self.parameterAsSource(parameters,self.INPUT1,context)
        fixgeometries1_result = processing.run(
            'native:fixgeometries',
            {
                'INPUT': parameters['INPUT1'],
                'OUTPUT': parameters['OUTPUT']
            },
            is_child_algorithm=True,
            context=context,
            feedback=feedback)

        dest_id = fixgeometries1_result['OUTPUT']
        
        if context.willLoadLayerOnCompletion(dest_id):
            context.layerToLoadOnCompletionDetails(dest_id).setPostProcessor(MyLayerPostProcessor.create())

        return {}
        
        
class MyLayerPostProcessor(QgsProcessingLayerPostProcessorInterface):
    # Courtesy of Nyall Dawson: https://gist.github.com/nyalldawson/26c091dd48b4f8bf56f172efe22cf75f
    instance = None

    def postProcessLayer(self, layer, context, feedback):  # pylint: disable=unused-argument
        if not isinstance(layer, QgsVectorLayer):
            return

        layer.setName('Renamed layer')
        

    # Hack to work around sip bug!
    @staticmethod
    def create() -> 'MyLayerPostProcessor':
        """
        Returns a new instance of the post processor, keeping a reference to the sip
        wrapper so that sip doesn't get confused with the Python subclass and call
        the base wrapper implementation instead... ahhh sip, you wonderful piece of sip
        """
        MyLayerPostProcessor.instance = MyLayerPostProcessor()
        return MyLayerPostProcessor.instance

The code for the MyLayerPostProcessor() class I adapted from a github gist of Nyall Dawson's here.

See a gif below showing this script working and loading a renamed layer:

enter image description here

4
  • Thanks @Ben W. I've stripped my code down to the very basics to test this approach and it is still not working. When take the approach above, I get the error: AttributeError: 'str' object has no attribute 'setName'. I've had a look at what is being returned by processing.run and the fixgeometries1_result appears to be the string 'output_9b8939ac_9708_4dd8_8798_0cbac48a4d09'
    – Danny
    Commented Jul 25, 2021 at 22:55
  • @Danny, I have updated my answer.
    – Ben W
    Commented Jul 26, 2021 at 21:42
  • Thanks @BenW. I'll give that a go. My issue all along was that, whilst I knew I could use the final algorithm to persist the layer (I have a much more complex script that looped through many algorithm executions, and I ended up with a layer in memory at the end, which I then persisted using one final merge algorithm). The issue I had was not being able to control the name of the layer when it was persisted in the menu. Your Post processor should allow me to take care of that issue which gets me where I need to be. Cheers!
    – Danny
    Commented Jul 26, 2021 at 22:41
  • @Danny, no worries. Might I suggest creating a processing model using the Graphical Modeler, get it working the way you want, then export it as a Python script. That will take care of most of the required code. You would just need to implement the post processor to rename the final layer. Here is a working example I created yesterday, while I was playing around with this. gist.github.com/benwirf/5dd676a3b794c649d9a289af8447b928
    – Ben W
    Commented Jul 26, 2021 at 23:41
4

You were almost there.
Your problem was you were reloading the layer instead of taking its data and reloading it as a new named layer.

This should work for you:

fixgeometries1_result = processing.run(
        'native:fixgeometries',
        {
            # passing the input
            'INPUT': parameters['INPUT1'],
            'OUTPUT': 'memory:'
        },
        is_child_algorithm=True,
        context=context,
        feedback=feedback)

uri = fixgeometries1_result['OUTPUT'].dataProvider().dataSourceUri()

layer1 = QgsVectorLayer(uri,"layername","memory")

QgsProject.instance().addMapLayer(layer1)
5
  • Thanks for the quick response! I've just tried this and it is throwing the error: AttributeError: 'str' object has no attribute 'dataProvider'
    – Danny
    Commented Jul 25, 2021 at 6:35
  • Then there is probably a problem in the way you are running the algorithm. You can try running it without is_child_algorithm=True,context=context and feedback=feedback as I don't have yours, I ran the algorithm in the most basic way without those.
    – Dror Bogin
    Commented Jul 25, 2021 at 6:42
  • It should be 'memory:' not 'memory'
    – Dror Bogin
    Commented Jul 25, 2021 at 7:28
  • Thanks @Dror Bogin. Tried this approach by stripping out everything from the script but the basics to run. I'm still getting the'str' object has no attribute 'dataProvider'. I've had a look at what is being returned from the processing.run call and it is 'OUTPUT': 'output_654d9c65_40a6_4ba0_8003_28176113fec0'
    – Danny
    Commented Jul 25, 2021 at 23:08
  • That is wierd, but I assume that means your input is not a QgsVectroLayer, If you are getting a string back from an algorithm that should return a layer, than your parameters are probably not right.
    – Dror Bogin
    Commented Jul 26, 2021 at 5:03

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.