PyQGIS – Fixing ‘AttributeError: str object has no attribute dataProvider’ When Persisting Layer

attributeerrorpyqgisqgis-3qgis-processing

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'

Best Answer

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