PyQGIS – How to Run a QGIS Plugin on Its Python API

batchpyqgisqgis-plugins

I have about 40 layers with attribute tables that have many NULL columns. I downloaded the plugin "Delete NULL Fields from Vector Data", which works. I'd like to automate it using the QGIS Python API. The script I tried returned the following:

Traceback (most recent call last): File
"C:\PROGRA~1\QGIS32~2.3\apps\Python39\lib\code.py", line 90, in
runcode
exec(code, self.locals) File "", line 1, in File "", line 17, in File
"C:\PROGRA~1/QGIS32~2.3/apps/qgis/./python/plugins\processing\tools\general.py",
line 108, in run
return Processing.runAlgorithm(algOrName, parameters, onFinish, feedback, context) File
"C:\PROGRA~1/QGIS32~2.3/apps/qgis/./python/plugins\processing\core\Processing.py",
line 169, in runAlgorithm
raise QgsProcessingException(msg)
_core.QgsProcessingException: Error: Algorithm qgis:Delete_Null_Fields not found

This is my script:

# specify output folder and new filename
    output_folder = '/path/to/output/folder'
    new_name = layer.name() + '_nonull'

# get a list of all layers in the project
    layers = QgsProject.instance().mapLayers().values()
    
    # loop through each layer and delete null fields
    for layer in layers:
    
        # run the plugin
        alg_params = {
            'INPUT': layer,
            'FIELDS': None,
            'OUTPUT': output_folder + '/' + new_name + '.shp'
        }
        nonull_layer = processing.run('qgis:Delete_Null_Fields', alg_params)['OUTPUT']
    
        # add the new layer to the project
        QgsProject.instance().addMapLayer(nonull_layer, False)
    
        # save the new layer to a file
        file_info = QFileInfo(layer.source())
        output_path = file_info.path() + '/' + new_name + '.shp'
        QgsVectorLayer.exportLayer(nonull_layer, output_path, 'ESRI Shapefile')

From what I understood, the API was unable to find the plugin from the name I gave. I got it from the dev's git page. I also tried initialising it first using the dev's script before my code starts:

def classFactory(iface):  # pylint: disable=invalid-name
    """Load Delete_Null_Fields class from file Delete_Null_Fields.
    :param iface: A QGIS interface instance.
    :type iface: QgsInterface
    """
    #
    from .delete_null_fields import Delete_Null_Fields
    return Delete_Null_Fields(iface)

But it didn't work. Importantly, I am very new to programming. What is wrong with the code and how could I achieve my batch processing?

Best Answer

It looks like you are trying to run a gui based Python plugin as a processing algorithm, which simply isn't going to work. To use a processing.run() call like this, you must pass the method an actual processing algorithm which has been added to the processing toolbox by a processing provider (either native or 3rd party).

While it is technically possible to programmatically access the classes and methods of an installed Python plugin, for example the following will open the dialog of the plugin:

plugin = qgis.utils.plugins['delete_null_fields']
# print(plugin)
plugin.run()

how useful this actually is depends very much on the structure of the plugin.

In this instance, to achieve what you want, I think it is easier to write our own script.

Fortunately, we can write a fairly quick and simple script containing a function which will create a copy of the input layer, delete any fields with all NULL values and save the output to a specified folder, leaving the original input layer unchanged. We can then iterate over all the project layers, skipping any non-vector layers, and call our function, passing the layer object, the output path and output format as arguments.

Paste the script below into a new editor in the Python console, edit the output path and click run.

import os

# Define a function with a few parameters to do what what we want

def removeNullColumns(layer, output_dir, file_format):
    # Create an in memory copy of the input layer
    temp_layer = layer.materialize(QgsFeatureRequest())
    # Compile list of fields with all NULL values
    fld_ids = []
    for fld in temp_layer.fields():
        contains_values = False
        for ft in temp_layer.getFeatures():
            if ft[fld.name()] != NULL:
                contains_values = True
                break
        if not contains_values:
            fld_ids.append(temp_layer.fields().lookupField(fld.name()))
    
    # Delete the null fields and update the temp layer fileds
    temp_layer.dataProvider().deleteAttributes(fld_ids)
    temp_layer.updateFields()
    
    # Save the temp layer with input layer name appended with '_no_null'
    # to the specified output folder in the specified format
    save_params = {'INPUT':temp_layer,
                    'OUTPUT':os.path.join(output_dir, f'{layer.name()}_no_null{file_format}')}
    processing.run("native:savefeatures", save_params)
    # If you want to load all the output layer, replace the line above with:
    # processing.runAndLoadResults("native:savefeatures", save_params)

# Define path to your output folder location
destination_folder = '/path/to/output/folder'# ******************EDIT THIS LINE

# Specify the desired output format. If you want Geopackage change to '.gpkg'
output_format = '.shp'

# Iterate over loaded layers, skipping any non-vector layers
# and call the removeNullColumns() function for each one
for lyr in QgsProject.instance().mapLayers().values():
    if not isinstance(lyr, QgsVectorLayer):
        continue
    removeNullColumns(lyr, destination_folder, output_format)
print('Done')
Related Question