12

I am in the process of migrating a QGIS python plugin from QGIS 2 to QGIS 3, and browsing various resources.

It's not clear if it's possible to have the plugin compatible with both versions or if it's necessary two handle to plugin versions.

The problem I hit so far is how to manage PyQt import (PyQt4/PyQt5) ?

18

Documentation

Here you can find what is new and what is break under the PyQGIS API.
To get details about how to port Python2 to Python3 go there

You can find some detail about testing from QGIS2 to QGIS3 on this question :Writing automated tests for QGIS plugins?

And you will find an interesting OpenGis.ch's paper here about the migrations tools.

What will change into my code

In fact, you need to change code of plugin that are not prepared to pass throught a new version.

You get qgis.utils.QGis.QGIS_VERSION_INT function which is made to check the QGIS version. This is usefull when a function is deprecabled . For exemple setSelectedFeatures since 2.16.

By exemple with the use of if statement :

if qgis.utils.QGis.QGIS_VERSION_INT < 21600 :
            joinLayer.setSelectedFeatures( [ f.id() for f in request ] )
        else:
            joinLayer.selectByIds(  [ f.id() for f in request ] )

It's the same about PyQt object you import under your module. If you need compatibility, the price is to wrote more code line (the code with QGIS2 function and the code with QGIS3 functions AND also the code for checking the version and the capabilities to import new libraries ).

About PyQt libraries

The PyQt5 is not backward compatible with PyQt4; there are several significant changes in PyQt5. However, it is not very difficult to adjust older code to the new library. The differences are, among others, the following:

  • Python modules have been reorganized. Some modules have been dropped (QtScript), others have been split into submodules (QtGui, QtWebKit).

  • New modules have been introduced, including QtBluetooth, QtPositioning, or Enginio.

  • PyQt5 supports only the new-style signal and slots handlig. The calls to SIGNAL() or SLOT() are no longer supported. PyQt5 does not support any parts of the Qt API that are marked as deprecated or obsolete in Qt v5.0.

source : (http://zetcode.com/gui/pyqt5/introduction/)

Here is some exemples of changes into your from/import statement:

Remember with PyQt4 you had to look on the API's doc:
for exemple
PyQT4 QtCore module
PyQT4 QtGui module

from PyQt4.QtCore import QSettings, QTranslator, qVersion, QCoreApplication, Qt, QObject, SIGNAL

from PyQt4.QtGui import QAction, QIcon, QDialog, QFormLayout

And with PyQt5 you have now to look on those API's doc :
PyQt5 QtCore module
PyQt5 QtGui module

so that become :

from PyQt5.QtCore import QSettings, QTranslator, QVersionNumber, QCoreApplication, Qt, QObject, pyqtSignal 
from PyQt5.QtGui import QIcon 
from PyQt5.QtWidgets import QAction, QDialog, QFormLayout

Note that :

QtGui module has been split into submodules. The QtGui module contains classes for windowing system integration, event handling, 2D graphics, basic imaging, fonts and text. It also containes a complete set of OpenGL and OpenGL ES bindings (see Support for OpenGL). Application developers would normally use this with higher level APIs such as those contained in the QtWidgets module.

And PyQt5 supports only the new-style signal and slots handlig! have a look to this page to understand how to use pyqtSignal, connect and e event object instead of use SIGNAL.

Make it compatible

So with compatibility between PyQt4/PyQt5 (and QGIS2 / QGIS3 as well) you need to try/except the import before use the pyQt5 librarie.

try:
    from PyQt5.QtCore import QSettings, QTranslator, QVersionNumber, QCoreApplication, Qt, QObject, pyqtSignal 
    from PyQt5.QtGui import QIcon 
    from PyQt5.QtWidgets import QAction, QDialog, QFormLayout

except:
    from PyQt4.QtCore import QSettings, QTranslator, qVersion, QCoreApplication, Qt, QObject, SIGNAL
    from PyQt4.QtGui import QAction, QIcon, QDialog, QFormLayout

And don't forget that you need to change also some specific function under your code by adding try/except or if statement.

  • 2
    Great answer, something that will greatly help is to first replace any from PyQt4.QtCore import * with from PyQt4.QtCore import QSomething, QWhatever, QElse, this will make the migration script do the last step properly(including required adjustments where modules changed), so no try-except imports are needed. – Matthias Kuhn Oct 31 '17 at 10:12
  • you are right I used * to keep it simple, but I will change that, thanks for your feedback – Hugo Roussaffa - GeoDatup Oct 31 '17 at 10:16
  • this topic is the perfect place to tell people to not use *-imports, because here it actually does make a difference – Matthias Kuhn Oct 31 '17 at 10:26
  • @Hugo : Very detailed answer indeed, it helped a lot to get started. I will add the qgis2compat plugin to the numerous usefull resources already cited. – sigeal Nov 5 '17 at 10:36
  • That's great idea. You can edit the answer as you want. Thanks for the feedback – Hugo Roussaffa - GeoDatup Nov 5 '17 at 11:47
2

Try something like that:

try:
    # action for QGIS 3/PyQt5
except:
    # action for QGIS 2/PyQt4
  • This might work for some isolated things but will often not work as a generic solution. – Matthias Kuhn Oct 31 '17 at 10:14
1

I have just finished porting a QGIS Python plugin so that it now supports both 2.x and 3.x QGIS versions. Here is my experience:

Mostly I tried to rely on QGIS version. But even the class holding the version was a bit renamed. So I first did

try:
    from qgis.utils import Qgis  # for QGIS 3
except ImportError:
    from qgis.utils import QGis as Qgis  #  for QGIS 2

and then make checks

if Qgis.QGIS_VERSION >= '3.0':
    # something for QGIS 3
else:
    # something for QGIS 2

After deploying a final version I noticed that a file resources.py which is created automatically by pyrcc5 also needs to be ported. Otherwise plugin will keep breaking down in 2.x. So I changed its line

from PyQt5 import QtCore

to

try:
    from PyQt5 import QtCore
except:
    from PyQt4 import QtCore

It looked like it worked. I made an official release and thought that is it. Only then I discovered this sequence:

Install my plugin in QGIS 2.18, close QGIS, open QGIS agan and then open Python Console inside QGIS -> The entire QGIS will instantly crash!

After some testing I found out the reason was this small change in resources.py written above. I am no expert in QGIS Python libraries but my explanation is the following:

When I open QGIS my plugin gets initialized. Attempt of doing from PyQt5 import QtCore causes some changes in the QGIS "workflow" before wrong PyQt version raises an exception (it was a RuntimeError). When I start Python Console those changes cause QGIS to crash.

At the end I decided for a different solution. Because QGIS 2 uses Python 2.7 and QGIS 3 uses Python 3 I simply always make checks for Python version.

from sys import version_info

if version_info[0] >= 3:
    # something for QGIS 3
else:
    # something for QGIS 2

This avoids all potentially harmful import attempts. My plugin now works on both QGIS versions without any problems.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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