Make QGIS python plugin for both versions 2.x and 3.x?

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.


Try something like that:

try:
    # action for QGIS 3/PyQt5
except:
    # action for QGIS 2/PyQt4