Using Plugin in standalone PyQGIS script

Fist of all, for using a plugin (NNJoin here) in standalone script, you need to examine that plugin's code, decide which part you can call (I don't think this is always possible but I'm not sure 100%) and what changes/addings you need in your standalone script.

How works NNJoin plugin?: It opens a QDialog window, you select options, you click OK button and it processes.

So, you have two solutions here:

1. You can open NNJoin dialog window by script, you select options manually and click OK to run.

2. Or you can set options by script without opening NNJoin window and trigger the OK button.


SOLUTION 1:

I have two files: main.py (minimal pyqgis standalone script) and MapViewer.py (includes all GUI)

main.py:

import os
import sys
from qgis.core import QgsApplication

QgsApplication.setPrefixPath(os.environ['QGIS_PREFIX_PATH'], True)
qgs = QgsApplication([], True)
qgs.initQgis()

from MapViewer import MapViewer
main_window = MapViewer()
main_window.show()
qgs.exec_()
qgs.exitQgis()

MapViewer.py: It includes a layers panel, a map canvas, a status bar with a button. Please review the script. I added all explanation to the script as comment (PART 1, PART 2, ...).

import sys
from qgis.core import *
from qgis.gui import *
from qgis.PyQt.QtWidgets import *

######## PART 1: Add QGIS plugin path to the system path ########
import sys
sys.path.append(r"C:\Users\user\AppData\Roaming\QGIS\QGIS3\profiles\ks3.12\python\plugins")
########

######## PART 2: Import plugin dialog ########
from NNJoin.NNJoin_gui import NNJoinDialog
########

class MapViewer(QMainWindow):
    def __init__(self):
        QMainWindow.__init__(self, None)

        self._canvas = QgsMapCanvas()
        self._root = QgsProject.instance().layerTreeRoot()

        self.bridge = QgsLayerTreeMapCanvasBridge(self._root, self._canvas)
        self.model = QgsLayerTreeModel(self._root)
        self.model.setFlag(0x25043)
        self.model.setFlag(QgsLayerTreeModel.ShowLegend)
        self.layer_treeview = QgsLayerTreeView()
        self.layer_treeview.setModel(self.model)

        self.layer_tree_dock = QDockWidget("Layers")
        self.layer_tree_dock.setObjectName("layers")
        self.layer_tree_dock.setFeatures(QDockWidget.NoDockWidgetFeatures)
        self.layer_tree_dock.setWidget(self.layer_treeview)

        self.splitter = QSplitter()
        self.splitter.addWidget(self.layer_tree_dock)
        self.splitter.addWidget(self._canvas)
        self.splitter.setCollapsible(0, False)
        self.splitter.setStretchFactor(1, 1)

        self.layout = QHBoxLayout()
        self.layout.addWidget(self.splitter)
        self.contents = QWidget()
        self.contents.setLayout(self.layout)
        self.setCentralWidget(self.contents)

        ######## PART 3: Add messagebar to main window ########
        # We need this, because parent widget of NNJoin dialog
        # calls this messagebar.
        self.bar = QgsMessageBar()
        self.statusBar().addWidget(self.bar)
        self.messageBar = lambda: self.bar
        ########

        self.button = QPushButton("NNJoin")
        self.button.setMaximumHeight(30)
        self.button.setMaximumWidth(80)
        self.statusBar().addPermanentWidget(self.button)
        ######## PART 4: Connect click event of button to method ########
        self.button.clicked.connect(self.open_nnjoin_window)
        self.load_layers()

        ######## PART 5: Create an instance of plugin dioalg ########
        # 'self' is used here instead of 'self.iface' in plugin
        self.dlg = NNJoinDialog(self)
        ########

    def load_layers(self):
        # Adding two sample layers
        self.layer1 = QgsVectorLayer("file1.shp", 'Layer1', 'ogr')
        self.layer2 = QgsVectorLayer("file2.shp", 'Layer2', 'ogr')

        layers = [self.layer1, self.layer2]
        QgsProject.instance().addMapLayers(layers)
        self._canvas.setExtent(self.layer1.extent())
        self._canvas.setLayers(layers)

    ######## PART 6: Initialising NNJoin dialog ########
    # this method runs by clicking the button
    # this part was copied from NNJoin source code and modified
    def open_nnjoin_window(self):
        self.dlg.progressBar.setValue(0.0)
        self.dlg.outputDataset.setText('Result')

        layers = QgsProject.instance().mapLayers()
        layerslist = []
        for id in layers.keys():
            if layers[id].type() == QgsMapLayer.VectorLayer:
                if not layers[id].isValid():
                    QMessageBox.information(None, 'Information',
                        'Layer ' + layers[id].name() + ' is not valid')
                if layers[id].wkbType() != QgsWkbTypes.NoGeometry:
                    layerslist.append((layers[id].name(), id))

        self.dlg.inputVectorLayer.clear()
        for layerdescription in layerslist:
            self.dlg.inputVectorLayer.addItem(layerdescription[0],
                                        layerdescription[1])
        self.dlg.joinVectorLayer.clear()

        for layerdescription in layerslist:
            self.dlg.joinVectorLayer.addItem(layerdescription[0],
                                        layerdescription[1])

        self.dlg.show()
        self.dlg.exec_()
    ########

Note that NNJoin plugin doesn't return any layer reference, but just add a layer to QgsProject instance. So you should get the returned layer using QgsProject.instance().mapLayersByName("LAYER_NAME").


There's a few posts like this one which mentions that it may be difficult to call plugins from a standalone script especially if they're designed around an interface (iface).


You should be able to call the Join Attributes by Nearest tool but note that this was released in QGIS 3.8 so you may need to update your version.