QGIS/scripts/pyqt5_to_pyqt6/pyqt5_to_pyqt6.py
2024-01-19 19:44:48 +10:00

223 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""
***************************************************************************
3to4.py
---------------------
Date : 2023 December
Copyright : (C) 2023 by Julien Cabieces
Email : julien dot cabieces at oslandia dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************
"""
"""
Migrate a folder containing python files from QGIS3/Qt5 to QGIS4/Qt6
Highly inspired from this video https://www.youtube.com/watch?v=G1omxo5pphw
needed tools:
pip install astpretty tokenize-rt
Few useful commands:
- display python file ast
astpretty --no-show-offsets myfile.py
- display file tokens
tokenize-rt myfile.py
"""
__author__ = 'Julien Cabieces'
__date__ = '2023 December'
__copyright__ = '(C) 2023, Julien Cabieces'
import argparse
import ast
import glob
import os
import inspect
from collections import defaultdict
from tokenize_rt import Offset, src_to_tokens, tokens_to_src, reversed_enumerate, Token
from typing import Sequence
from PyQt6 import QtCore, QtGui, QtWidgets, QtTest, QtSql, QtSvg, QtXml, QtNetwork, QtPrintSupport, Qsci
# qmetatype which have been renamed
qmetatype_mapping = {
"Invalid": "UnknownType",
"BitArray": "QBitArray",
"Bitmap": "QBitmap",
"Brush": "QBrush",
"ByteArray": "QByteArray",
"Char": "QChar",
"Color": "QColor",
"Cursor": "QCursor",
"Date": "QDate",
"DateTime": "QDateTime",
"EasingCurve": "QEasingCurve",
"Uuid": "QUuid",
"ModelIndex": "QModelIndex",
"PersistentModelIndex": "QPersistentModelIndex",
"Font": "QFont",
"Hash": "QVariantHash",
"Icon": "QIcon",
"Image": "QImage",
"KeySequence": "QKeySequence",
"Line": "QLine",
"LineF": "QLineF",
"List": "QVariantList",
"Locale": "QLocale",
"Map": "QVariantMap",
"Transform": "QTransform",
"Matrix4x4": "QMatrix4x4",
"Palette": "QPalette",
"Pen": "QPen",
"Pixmap": "QPixmap",
"Point": "QPoint",
"PointF": "QPointF",
"Polygon": "QPolygon",
"PolygonF": "QPolygonF",
"Quaternion": "QQuaternion",
"Rect": "QRect",
"RectF": "QRectF",
"RegularExpression": "QRegularExpression",
"Region": "QRegion",
"Size": "QSize",
"SizeF": "QSizeF",
"SizePolicy": "QSizePolicy",
"String": "QString",
"StringList": "QStringList",
"TextFormat": "QTextFormat",
"TextLength": "QTextLength",
"Time": "QTime",
"Url": "QUrl",
"Vector2D": "QVector2D",
"Vector3D": "QVector3D",
"Vector4D": "QVector4D",
"UserType": "User",
}
extra_members = {
('Qt', 'FocusPolicy'): ('StrongFocus', 'WheelFocus', 'NoFocus')
}
deprecated_renamed_enums = {
('Qt', 'MidButton'): ('MouseButton', 'MiddleButton')
}
# { (class, enum_value) : enum_name }
qt_enums = {}
def fix_file(filename: str, qgis3_compat: bool) -> int:
with open(filename, encoding='UTF-8') as f:
contents = f.read()
fix_qvariant_type = [] # QVariant.Int, QVariant.Double ...
fix_pyqt_import = [] # from PyQt5.QtXXX
fix_qt_enums = [] # Unscopping of enums
rename_qt_enums = [] # Renaming deprecated removed enums
tree = ast.parse(contents, filename=filename)
for node in ast.walk(tree):
if (not qgis3_compat and isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name)
and node.value.id == "QVariant"):
fix_qvariant_type.append(Offset(node.lineno, node.col_offset))
if (isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name)
and (node.value.id, node.attr) in qt_enums):
fix_qt_enums.append(Offset(node.lineno, node.col_offset))
if (isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name)
and (node.value.id, node.attr) in deprecated_renamed_enums):
rename_qt_enums.append(Offset(node.lineno, node.col_offset))
elif (isinstance(node, ast.ImportFrom) and node.module and node.module.startswith("PyQt5.")):
fix_pyqt_import.append(Offset(node.lineno, node.col_offset))
if not fix_qvariant_type and not fix_pyqt_import and not fix_qt_enums and not rename_qt_enums:
return 0
tokens = src_to_tokens(contents)
for i, token in reversed_enumerate(tokens):
if token.offset in fix_qvariant_type:
assert tokens[i].src == "QVariant"
assert tokens[i + 1].src == "."
tokens[i] = tokens[i]._replace(src="QMetaType.Type")
attr = tokens[i + 2].src
if attr in qmetatype_mapping:
tokens[i + 2] = tokens[i + 2]._replace(src=qmetatype_mapping[attr])
if token.offset in fix_pyqt_import:
assert tokens[i + 2].src == "PyQt5"
tokens[i + 2] = tokens[i + 2]._replace(src="qgis.PyQt")
if token.offset in fix_qt_enums:
assert tokens[i + 1].src == "."
enum_name = qt_enums[(tokens[i].src, tokens[i + 2].src)]
assert enum_name
tokens[i + 2] = tokens[i + 2]._replace(src=f"{enum_name}.{tokens[i + 2].src}")
if token.offset in rename_qt_enums:
assert tokens[i + 1].src == "."
enum_name = deprecated_renamed_enums[(tokens[i].src, tokens[i + 2].src)]
assert enum_name
tokens[i + 2] = tokens[i + 2]._replace(src=f"{enum_name[0]}.{enum_name[1]}")
new_contents = tokens_to_src(tokens)
with open(filename, 'w') as f:
f.write(new_contents)
return new_contents != contents
def get_class_enums(item):
if not inspect.isclass(item):
return
for key, value in item.__dict__.items():
if inspect.isclass(value) and type(value).__name__ == 'EnumType':
for e in value:
qt_enums[(item.__name__, e.name)] = f"{value.__name__}"
for _extra_value in extra_members.get((item.__name__, value.__name__), []):
qt_enums[(item.__name__, _extra_value)] = f"{value.__name__}"
elif inspect.isclass(value):
get_class_enums(value)
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('directory')
parser.add_argument('--only-qgis3-compatible-changes', action='store_true',
help='Apply only modifications that would not break behavior on QGIS 3, hence code may not work on QGIS 4')
args = parser.parse_args(argv)
# get all scope for all qt enum
for module in (QtCore, QtGui, QtWidgets, QtTest, QtSql, QtSvg, QtXml, QtNetwork, QtPrintSupport, Qsci):
for key, value in module.__dict__.items():
get_class_enums(value)
ret = 0
for filename in glob.glob(os.path.join(args.directory, "**/*.py"), recursive=True):
ret |= fix_file(filename, args.only_qgis3_compatible_changes)
return ret
if __name__ == '__main__':
raise SystemExit(main())