2

I wrote a python plugin whose purpose is to import a raster and put it in a group. My code works perfectly when I run it in the Python console of QGIS, but not when I run my plugin. The code is however identical...

Below the plugin code:

from qgis.PyQt.QtCore import QCoreApplication

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

from qgis.core import (QgsProject,
                       QgsLayerTree,
                       QgsRasterLayer,
                       QgsLayerTreeGroup,
                       QgsLayerTreeLayer,
                       QgsLayerTreeNode)

class Algorithm(QgsProcessingAlgorithm):

    OUTPUT = 'OUTPUT'
    INPUT = 'INPUT'

    def initAlgorithm(self, config):

        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT,
                self.tr('Input layer'),
                [QgsProcessing.TypeVectorAnyGeometry]
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr('Output layer')
            )
        )

    def processAlgorithm(self, parameters, context, feedback):

        project = QgsProject.instance()
        root = project.layerTreeRoot()

        layer = QgsVectorLayer("Point?crs=EPSG:2154", "Layer", "memory")
        project.addMapLayer(layer)
        node_layer = root.findLayer(layer.id())
        feedback.pushInfo(str(node_layer))
        node_layer_clone = node_layer.clone()

        node_group = root.insertGroup(0, 'Group')
        node_group.insertChildNode(0, node_layer_clone)

        parent = node_layer.parent()
        parent.removeChildNode(node_layer)

    def name(self):
        return 'Algorithm'

    def displayName(self):
        return self.tr(self.name())

    def group(self):
        return self.tr(self.groupId())

    def groupId(self):
        return ''

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return Algorithm()

Below the plugin log output:

QGIS version: 3.22.12-Białowieża
QGIS code revision: b8534cb1
Qt version: 5.15.3
Python version: 3.9.5
GDAL version: 3.5.2
GEOS version: 3.10.3-CAPI-1.16.1
PROJ version: Rel. 9.1.0, September 1st, 2022
PDAL version: 2.4.3 (git-version: 434757)
Algorithm started at: 2023-02-03T16:46:19
Algorithm 'Algorithm' starting…
Input parameters:
{ 'INPUT' : 'memory://Point?crs=EPSG:2154&uid={86d92c21-b1db-47ac-b1fc-e20c1bdb8075}', 'OUTPUT' : 'TEMPORARY_OUTPUT' }

None
traceback.print_exception() failed
Execution failed after 0.07 seconds

Loading resulting layers
Algorithm 'Algorithm' finished

Below the code that I run in the QGIS Python console:

from qgis.core import (QgsProject, QgsRasterLayer)

project = QgsProject.instance()
root = project.layerTreeRoot()

layer = QgsVectorLayer("Point?crs=EPSG:2154", "Layer", "memory")
project.addMapLayer(layer)
node_layer = root.findLayer(layer.id())
feedback.pushInfo(str(node_layer))
node_layer_clone = node_layer.clone()

node_group = root.insertGroup(0, 'Group')
node_group.insertChildNode(0, node_layer_clone)

parent = node_layer.parent()
parent.removeChildNode(node_layer)

The plugin log output indicates that node_layer is None, while it takes the value <QgsLayerTreeLayer: Layer> when I run it in the Python console.

I wonder if my plugin contains all the necessary classes (but which ones to add?), or if there would conflicts between classes?

1
  • i do not see QgsVectorLayer in your plugin imports, but I can see QgsRasterLayer. are you importing raster or vector ?
    – Taras
    Feb 3 at 20:36

1 Answer 1

8

What you are referring to here as a plugin is a custom processing algorithm script. This in itself is not a plugin, though it could be added to a 3rd party provider as part of a processing plugin (and maybe that is what you're doing?), or added directly to the processing toolbox as a Python script.

Part of your question is a little bit confusing, because you say that the purpose of the script is to 'import a raster layer' but your script is not importing, but creating a temporary vector layer (without importing the QgsVectorLayer class as pointed out by Taras).

In any case, simply pasting a script which works in the Python console into the processAlgorithm() method of a processing script can be asking for trouble in many cases, because there are a few 'gotchas' to be aware of. I would definitely recommend reading Writing new Processing algorithms as Python scripts especially Handing algorithm output, Flags and Best practices for writing script algorithms.

This explains that you should not load layers created by your algorithm. Rather, you should declare an output parameter and let Processing handle the loading of the output layer. Also, you should not interact with the GUI from inside the processAlgorithm() method. So, if you want to do something GUI-related with your output layer (like setting a style, or creating a layer tree group and adding the layer to it- as you are here) the best way to do this is to use the QgsProcessingLayerPostProcessorInterface class, check if the layer will be loaded, and set the post-processor to it which will be run after the execution of the processing, so you can safely do stuff which is not thread-safe. Here is an example incorporating your script logic which should do what you want.

from qgis.PyQt.QtCore import QCoreApplication, QVariant
from qgis.core import (QgsFields, QgsField, QgsVectorLayer,
                        QgsProcessing, QgsProcessingAlgorithm,
                        QgsProcessingParameterFeatureSink,
                        QgsWkbTypes, QgsCoordinateReferenceSystem,
                        QgsProcessingLayerPostProcessorInterface)
                      
class ExampleAlgorithm(QgsProcessingAlgorithm):
    OUTPUT = 'OUTPUT'
    
    post_processor = None

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

    def name(self):
        return "create_empty_layer"
    
    def tr(self, text):
        return QCoreApplication.translate("Processing", text)
        
    def displayName(self):
        return self.tr("Create empty layer")

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

    def groupId(self):
        return "examples"

    def shortHelpString(self):
        return self.tr("Creates an empty layer and adds it to a new group")

    def helpUrl(self):
        return "https://qgis.org"
        
    def createInstance(self):
        return ExampleAlgorithm()
  
    def initAlgorithm(self, config=None):
        self.addParameter(QgsProcessingParameterFeatureSink(
            self.OUTPUT,
            self.tr("Output layer"),
            QgsProcessing.TypeVectorPoint))

    def processAlgorithm(self, parameters, context, feedback):
        flds = QgsFields()
        flds.append(QgsField('ID', QVariant.Int))
        (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
                                               flds,
                                               QgsWkbTypes.Point,
                                               QgsCoordinateReferenceSystem('epsg:2154'))
        
        self.post_processor = self.postProcessorClassFactory()
        if context.willLoadLayerOnCompletion(dest_id):
            context.layerToLoadOnCompletionDetails(dest_id).setPostProcessor(self.post_processor)

        return {self.OUTPUT: dest_id}
        
        
    def postProcessorClassFactory(self):
        
        def postProcessLayer(self, layer, context, feedback):
            group_name = 'group1'
            project = context.project()
            root_group = project.layerTreeRoot()
            if not root_group.findGroup(group_name):
                root_group.insertGroup(0, group_name)
            group1 = root_group.findGroup(group_name)
            if group1:
                lyr_node = root_group.findLayer(layer.id())
                if lyr_node:
                    node_clone = lyr_node.clone()
                    group1.addChildNode(node_clone)
                    lyr_node.parent().removeChildNode(lyr_node)
            feedback.pushInfo(f'{layer.name()} post processed')
            
        def constructor(self):
            super(type(self), self).__init__()
            
        processor = type('MyPostProcessor', (QgsProcessingLayerPostProcessorInterface, ), {"__init__": constructor, 'postProcessLayer': postProcessLayer})

        return processor()

'Bonus answer' of different question asked in comments...

Loading multiple raster outputs into a group:

from PyQt5.QtCore import QCoreApplication, QVariant

from qgis.core import (QgsProcessing,
                        QgsProcessingAlgorithm,
                        QgsProcessingParameterFile,
                        QgsRasterLayer,
                        QgsProcessingContext,
                        QgsProcessingOutputMultipleLayers,
                        QgsProcessingLayerPostProcessorInterface)

from pathlib import Path
import os
                       
class AddRastersToGroup(QgsProcessingAlgorithm):

    INPUT_FOLDER = 'INPUT_FOLDER'
    OUTPUT_LAYERS = 'OUTPUT_LAYERS'
    
    post_processors = {}

    def __init__(self):
        super().__init__()
 
    def name(self):
        return "addoutputrasterstogroup"
     
    def tr(self, text):
        return QCoreApplication.translate("Processing", text)
         
    def displayName(self):
        return self.tr("Add output rasters")
 
    def group(self):
        return self.tr("Examples")
 
    def groupId(self):
        return "examples"
 
    def shortHelpString(self):
        return self.tr("Load multiple raster layers added to a created group")
 
    def helpUrl(self):
        return "https://qgis.org"
         
    def createInstance(self):
        return type(self)()
   
    def initAlgorithm(self, config=None):
        self.addParameter(QgsProcessingParameterFile(
            self.INPUT_FOLDER,
            self.tr("Source directory"),
            behavior=QgsProcessingParameterFile.Folder))
            
        self.addOutput(QgsProcessingOutputMultipleLayers(
            self.OUTPUT_LAYERS,
            "Output layers"))
 
    def processAlgorithm(self, parameters, context, feedback):
        source_folder = self.parameterAsString(parameters, self.INPUT_FOLDER, context)
        output_layers = []
        for file in os.scandir(source_folder):
            file_path = os.path.join(source_folder, file.name)
            file_name = Path(file_path).stem
            rl = QgsRasterLayer(file_path, file_name, 'gdal')
            feedback.pushInfo(repr(rl.isValid()))
            if rl.isValid():
                context.temporaryLayerStore().addMapLayer(rl)
                context.addLayerToLoadOnCompletion(rl.id(), QgsProcessingContext.LayerDetails(rl.name(),
                                                                                        context.project(),
                                                                                        rl.name()))
                output_layers.append(rl)
                pp = self.postProcessorClassFactory()
                self.post_processors[rl.id()] = pp
                if context.willLoadLayerOnCompletion(rl.id()):
                    context.layerToLoadOnCompletionDetails(rl.id()).setPostProcessor(self.post_processors[rl.id()])
            
        return {self.OUTPUT_LAYERS: output_layers}

    def postProcessorClassFactory(self):
        
        def postProcessLayer(self, layer, context, feedback):
            group_name = 'group1'
            project = context.project()
            root_group = project.layerTreeRoot()
            if not root_group.findGroup(group_name):
                root_group.insertGroup(0, group_name)
            group1 = root_group.findGroup(group_name)
            if group1:
                lyr_node = root_group.findLayer(layer.id())
                if lyr_node:
                    node_clone = lyr_node.clone()
                    group1.addChildNode(node_clone)
                    lyr_node.parent().removeChildNode(lyr_node)
            feedback.pushInfo(f'{layer.name()} post processed')
            
        def constructor(self):
            super(type(self), self).__init__()
            
        processor = type('MyPostProcessor', (QgsProcessingLayerPostProcessorInterface, ), {"__init__": constructor, 'postProcessLayer': postProcessLayer})

        return processor()

References:

Create Classes Dynamically in Python

https://github.com/qgis/QGIS/issues/47533

8
  • If I understand correctly, all the operations aimed at processing the data must be done in processAlgorithm(), while the operations relating to the GUI must be done in a new class which inherits from QgsProcessingLayerPostProcessorInterface. The problem is that layerToLoadOnCompletionDetails and postProcessLayer acting only on one layer, I don't understand how to create (only once) a group and iteratively add several layers to it? Indeed, my goal is to 1) download several rasters from the internet and 2) add all the rasters in a group (in fact I work with rasters and not vectors...).
    – ZKB
    Feb 6 at 17:41
  • To summarize: is it possible to declare a variable of type QgsProcessingOutputRasterLayer, iteratively download the rasters from the internet, save them temporarily in the variable associated with QgsProcessingOutputRasterLayer and display them in the canvas? If so, an example would be more than appreciated. By the way I'm new to PyQGIS.
    – ZKB
    Feb 6 at 17:42
  • I'm not sure I fully understand when to use postProcessAlgorithm and QgsProcessingLayerPostProcessorInterface, but generally speaking I feel that postProcessAlgorithm works just fine for what I want to do, even without using FlagNoThreading. Thank you for the explanations!
    – ZKB
    Feb 7 at 16:17
  • What exact bug is "Hack to work around sip bug!" referencing? Jul 24 at 9:33
  • 1
    @bugmenot123, FYI you can see some related discussion here about post-processing/loading multiple output layers. That's about all I can tell you at the moment.
    – Ben W
    Jul 24 at 11:02

Your Answer

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

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