Migration from PyQT4 to PyQT5 at GNS3

The current GNS3 GUI interface use PyQT4. In order to support the retina display we choose to move to PyQT5.

The move was not too complicated with the help of a small script.

You can see the complete list of PyQT4 / PyQT5 differences here: http://pyqt.sourceforge.net/Docs/PyQt5/pyqt4_differences.html

Actually most of recent distributions support PyQT5:

  • Debian Jessie
  • Ubuntu 14.04 and later
  • Arch linux
  • Gentoo
  • Fedora 21 and later

In all our code we already not directly include the PyQT4 module but our own qt module responsible of all the imports this save us the need to patch every files for changing the module.

An annoying point was the move from QtGui to QtWidgets of a lot of element. That’s why we wrote a script finding all the call to QtGui and checking if the module as not move to QtWidgets. The clean syntax of Python allow us to write a naive script.

It’s very easy with this apply to each line of the file:

if "QtGui." in line:
    for name, obj in inspect.getmembers(QtWidgets):
        line = line.replace("QtGui." + name, "QtWidgets." + name)

After that you need to import also QtWidgets. All our imports was like:

from .qt import QtGui

It’s easy to patch it by code:

if line.startswith("from ") and "import" in self._line and "QtGui" in line and not "QtWidgets" in line:
    line = line.strip() + ", QtWidgets\n"

Thanks to the previous move to Python 3 fixing the call to parent classe was easy because we can replace:

QtGui.QWidget.__init__(self)

By:

super().__init__()

Our script detect the name of parent classes and replace it by super.

Thanks to the script most of our code was working after this. We just need to check the script behavior by using git diff.

Also when using the app we have detected some issue. You need to know that method deprecated in QT5 are totaly removed in PyQT5. For us it was only a few calls.

Our patch script it’s specific for our code but can be a source of inspiration, if you want to use it you need to replace gns3 by your module. It’s also check if you haven’t a double init call:

import os
import sys
import re
import inspect
from PyQt5 import QtWidgets
 
class FilePatcher:
    def __init__(self, path):
        self._path = path
        self._line_number = 1
        self._previous_line = None
        self._line = None
        self._parent_classes = []
        self._qtwidgets_imported = False
 
    def error(self, msg):
        print("ERROR: {}:{} {}".format(self._path,self._line_number, msg))
 
    def info(self, msg , *args):
        msg = msg + " ".join(args)
        print("INFO: {}:{} {}".format(self._path,self._line_number, msg))
 
    def patch(self):
        out = ""
        with open(self._path) as f:
            previous_line = None
            for self._line in f.readlines():
                self.fix_object_inheritance()
                self.get_parent_class()
                if self._previous_line:
                    self.error_double_init()
                self.fix_qt_widgets()
                self.fix_init_with_self()
                self.fix_super_with_argument()
                self.fix_call_without_super()
                self.error_import_missing()
                self._line_number += 1
                self._previous_line = self._line
                out += self._line
 
        with open(self._path, 'w+') as f:
            f.write(out)
 
    def get_parent_class(self):
        """Extract parent class to self._parent_classes var"""
        if self._line.startswith("class "):
            l = re.match(r".*\((.*)\):", self._line)
            if l:
                self._parent_classes = []
                for cls in l.group(1).split(","):
                    self._parent_classes.append(cls.strip())
 
    def fix_object_inheritance(self):
        """In Python3 you no longer need to inherit from object"""
 
        if "(object):" in self._line:
            self._line = re.sub(r"\(object\)", "", self._line)
 
    def error_double_init(self):
        """
        Detect double call to init and raise an error
        """
 
        if ".__init__" in self._previous_line and ".__init__" in self._line:
            self.error("Double init is no longer allowed")
 
    def fix_super_with_argument(self):
        """
        Thanks to Python 3 now we can call super() instead of super(MyClassParent)
        """
        if re.match(r".*super\([^)].*", self._line):
            self._line = re.sub(r"super\([^)]+\)", "super()", self._line)
            self.info("Fix ", self._line.strip())
 
    def fix_init_with_self(self):
        """
        In some part of the code we have MyClassParent.__init__(self

        Now we can use super().__init__
        """
        if ".__init__(self" in self._line:
            self._line = re.sub(r"([A-Z\.a-z0-9_-]+)\.__init__\(self,? ?", "super().__init__(", self._line)
 
    def fix_call_without_super(self):
        """
        We need to call parent with super instead of class name in order to avoid issues
        """
 
        for cls in self._parent_classes:
            m = re.match(r"(.*)({})\.([a-zA-Z]+)\((self, ? ?)(.*)".format(cls), self._line)
            if m:
                fixed = m.group(1) + "super()." + m.group(3) + "(" + m.group(5) + "\n"
                self._line = fixed
 
    def fix_qt_widgets(self):
        """
        Replace QtGui by QtWidgets when require
        """
 
        if self._line.startswith("from ") and "import" in self._line and "QtGui" in self._line and not "QtWidgets" in self._line:
            self._line = self._line.strip() + ", QtWidgets\n"
 
        if "QtGui." in self._line:
            for name, obj in inspect.getmembers(QtWidgets):
                self._line = self._line.replace("QtGui." + name, "QtWidgets." + name)
 
    def error_import_missing(self):
        """
        Check if we use QtWidgets without QtGui
        """
        if "QtWidgets" in self._line:
            if " import " in self._line:
                self._qtwidgets_imported = True
            else:
                if self._qtwidgets_imported is False:
                    self.error("QtWidgets import missing")
 
for root, dirs, files in os.walk('gns3'):
  for file in files:
    if file.endswith('.py'):
        FilePatcher(os.path.join(root, file)).patch()