Categories
Programming

Debugging QGIS Python Plugins

1. Introduction

Recently, I started working with QGIS, and found it a very powerful open source GIS platform, with the ability to create Python (or C++) based plugin extensions.

However, as in any programming language, you’ll create errors, and there will be bugs to be fixed. Working with a debugger, that allows you to step through your code to check your code is doing what it should do can be a real time saver.

Debugging your Python scripts starts with selecting an IDE such as PyCharm, Eclipse or Visual Studio Code. Given the wide popularity and the ongoing development of VS Code I chose the latter, but setting it up for debugging is not that straightforward.

First it is worth noting that there are two ways to install QGIS on Windows:

  1. As part of an OSGEO installation, that by default has its own folder at the root of the C-drive (C:\OSGeo4W)
  2. As a standalone installation under the C:\Program Files folder (which is a ‘protected’ folder, requiring admin write access)

In my case, the latest standalone version of QGIS has been installed under C:\Program Files\QGIS 3.26.0.

The steps I took are listed to make it all work, are listed here :

2. QGIS Plugin ‘debugvs’

In QGIS install the plugin debugvs, where ‘vs’ stands for Visual Studio.

debugvs selected in the Plugin Manager

3. Debugvs dependencies

The debugvs plugin needs the python module ptvsd to function. This module is not installed by default.

In principle you just pip install ptvsd using the python interpreter used by QGIS, but you need to pay attention here, in order to update information under the Program Files folder, you need to run it from the OSGeo4W Shell ("C:\Program Files\QGIS 3.26.0\OSGeo4W.bat") in elevated mode. You can do this by right clicking the OSGeo4W Shell from the QGIS 3.26.0 program group, and then select Run as administrator in the context menu.

Then type pip install ptvsd and installation should proceed, with files stored under the Program Files folder in the appropriate directory. Upon installation, you should get a confirmation that the installation went okay, and you might get a message asking you to update pip to the latest release.

According to the ptvsd documentation this package is deprecated, and one should use debugpy instead. That package is available here. I have not yet tested the difference. For now, ptvsd does the job.

4. Python extension for Visual Studio Code

After installing VS Code, you’ll need to install the Python extension, in order to develop Python code.

This will automatically install a Python interpreter as well. But that interpreter may not be 100% compatible with the one in QGIS. See next paragraph.

Python extension for VS Code

The Python extension will also install the Pylance and Jupyter extensions to give you the best experience when working with Python files and Jupyter notebooks.

Pylance is an optional dependency, meaning the Python extension will remain fully functional if it fails to be installed. You can also uninstall it at the expense of some features if you’re using a different language server.

5. Setting the interpreter

The Python interpreter can be set to C:\Program Files\QGIS 3.26.0\bin\python3.exe by selecting an interpreter in the right part of the status bar.

6. Prepare for debugging

The next step in VS Code is to prepare for Python debugging, for which we need to prepare a launch.json file in the the .vscode directory. This can simply be done by hitting F5 to start debugging, when you’ll be asked to create this file. In our case, we want to attach the debugger to an existing QGIS process, and the launch.json file then looks like :

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Remote Attach",
            "type": "python",
            "request": "attach",
            "connect": {
                "host": "localhost",
                "port": 5678
            },
            "pathMappings": [
                {
                    "localRoot": "${workspaceFolder}",
                    "remoteRoot": "${workspaceFolder}"
                }
            ],
            "justMyCode": true
        }
    ]
}

See this link for more information.

7. Overall Workflow

The overall workflow now is as follows:

  1. In VS Code, set one or more breakpoints in your Python code, from where you want to step through your code
  2. From the QGIS Plugin toolbar, or the Plugins menu select:
    Enable Debug for Visual Studio .
    You should see a message like:
    DebugVS : Remote Debug for Visual Studio is running(“request”: “attach”, “Port”: 5678, “host”: “localhost”)
  3. In VS Code, enable debugging by pressing F5
  4. Start your Plugin from the Plugin toolbar, or menu
  5. Step through your code
  6. Solve your bug(s) …
  7. Reload your plugin reload
  8. Test updated functionality, and if required:
  9. Adjust your breakpoints, and go back to step 4.

8. Pylance fix

For some reason I got niggling warnings that I did not understand; Pylance was not able to find some essential QGIS packages, which resulted in the following warnings:

  1. Import “qgis.PyQt.QtCore” could not be resolved
  2. Import “qgis.PyQt.QtGui” could not be resolved
  3. Import “qgis.PyQt.QtWidgets” could not be resolved
  4. Import “qgis.core” could not be resolved

This was rather strange, as debugging & code execution worked fine. According to the Pylance Common Questions and Issues, unresolved imports are often related to lack of a suitable relative path. I did extensive search on the internet, where – among other things – you come across Pylance bug reports that have in the mean time been closed out, so not very helpful.

In the end, the following Note helped find me the solution:

To find the required folders, you need to open the QGIS Python Console (Ctrl+Alt+P) and run the following command to see a list of default directories : QStandardPaths.standardLocations(QStandardPaths.AppDataLocation) . In my case, this command returned the following:

['C:/Users/Bart/AppData/Roaming/QGIS/QGIS3', 'C:/ProgramData/QGIS/QGIS3', 'C:/Program Files/QGIS 3.26.0/bin', 'C:/Program Files/QGIS 3.26.0/bin/data', 'C:/Program Files/QGIS 3.26.0/bin/data/QGIS/QGIS3']

This path has been added in a settings.json file under the .vscode folder.

Also the path for the default Python interpreter can be added in the settings file. The Python interpreter used by QGIS can also be found from the Python Console using the following command : QgsApplication.prefixPath(). In my case this returned: 'C:/PROGRA~1/QGIS32~2.0/apps/qgis'.

This is the ‘old’ 8.3 character representation of a folder path. By replacing the forward slashes by backward slashes and typing this path in the explorer window, it evaluates to : C:\Program Files\QGIS 3.26.0\apps\qgis.

This leads to the following setting.json file

{
    "python.defaultInterpreterPath": "C:\Program Files\QGIS 3.26.0\apps\qgis",
    
    "python.autoComplete.extraPaths": [
        "C:\\Users\\Bart\\ppData\\Roaming\\QGIS\\QGIS3", 
        "C:\\ProgramData\\QGIS\\QGIS3", 
        "C:\\Program Files\\QGIS 3.26.0\\bin", 
        "C:\\Program Files\\QGIS 3.26.0\\bin\\data", 
        "C:\\Program Files\\QGIS 3.26.0\\bin\\data\\QGIS\\QGIS3"
    ],
​
    "python.analysis.extraPaths": [
        "C:\\Users\\Bart\\ppData\\Roaming\\QGIS\\QGIS3", 
        "C:\\ProgramData\\QGIS\\QGIS3", 
        "C:\\Program Files\\QGIS 3.26.0\\bin", 
        "C:\\Program Files\\QGIS 3.26.0\\bin\\data", 
        "C:\\Program Files\\QGIS 3.26.0\\bin\\data\\QGIS\\QGIS3"
    ]
}

Things seem to work okay now. For instance my code contains:

crs = QgsProject.instance().crs()
units = crs.mapUnits()

By right-clicking on units and selecting go to type definition, the correct Python Interface Definition file (_core.pyi) is opened and it shows:

   class DistanceUnit(int):
        DistanceMeters = ... # type: QgsUnitTypes.DistanceUnit
        DistanceKilometers = ... # type: QgsUnitTypes.DistanceUnit
        DistanceFeet = ... # type: QgsUnitTypes.DistanceUnit
        DistanceNauticalMiles = ... # type: QgsUnitTypes.DistanceUnit
        DistanceYards = ... # type: QgsUnitTypes.DistanceUnit
        DistanceMiles = ... # type: QgsUnitTypes.DistanceUnit
        DistanceDegrees = ... # type: QgsUnitTypes.DistanceUnit
        DistanceCentimeters = ... # type: QgsUnitTypes.DistanceUnit
        DistanceMillimeters = ... # type: QgsUnitTypes.DistanceUnit
        DistanceUnknownUnit = ... # type: QgsUnitTypes.DistanceUnit

As the units variable returned ‘0’ it confirms that the units being used are meters. So it seems that Pylance can now complete code (Intellisense) and find parameter type definitions.

For this all to work, no environment file (.env) is required.