[PyQt6] Add new file to migrate from 3 to 4

This commit is contained in:
Julien Cabieces 2023-12-14 10:05:00 +01:00 committed by Nyall Dawson
parent d0d7465244
commit 84f334bead

175
scripts/3to4/3to4.py Normal file
View File

@ -0,0 +1,175 @@
#!/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 usefull 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
from collections import defaultdict
from tokenize_rt import Offset, src_to_tokens, tokens_to_src, reversed_enumerate, Token
from typing import Sequence
qmetatype_mapping = {"String": "QString",
"Invalid": "UnknownType",
"Date": "QDate",
"Time": "QTime",
"DateTime": "QDateTime",
"ByteArray": "QByteArray",
"StringList": "QStringList"
}
# key: class that has moved from one module to another
# value: (qt5 module, qt6 module)
moved_classes = {"QAction": ("QtWidgets", "QtGui")}
NEW_LINE = Token('NEWLINE', '\n')
SPACE = Token('UNIMPORTANT_WS', ' ')
COMMA = Token('OP', ',')
POINT = Token('OP', '.')
def import_tokens(module, classes):
classes_str = ",".join(classes)
return src_to_tokens(f"from qgis.PyQt.{module} import {classes_str}")
def fix_file(filename: str, args: argparse.Namespace) -> int:
if (filename != "../python/plugins/processing/gui/BatchInputSelectionPanel.py"):
return 0
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_moved_classes = [] # classes changing from modules
new_import_qt5 = defaultdict(list)
new_import_qt6 = defaultdict(list)
last_import = None
tree = ast.parse(contents, filename=filename)
for node in ast.walk(tree):
if (isinstance(node, ast.ImportFrom)):
last_import = Offset(node.end_lineno, node.end_col_offset)
if (isinstance(node, ast.ImportFrom) and node.module in ["PyQt5.QtWidgets", "qgis.PyQt.QtWidgets", "PyQt5.QtGui", "qgis.PyQt.QtGui"]):
for name in node.names:
if name.name in moved_classes:
fix_moved_classes.append(Offset(name.lineno, name.col_offset))
new_import_qt5[moved_classes[name.name][0]].append(name.name)
new_import_qt6[moved_classes[name.name][1]].append(name.name)
if (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))
elif (isinstance(node, ast.ImportFrom) 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_moved_classes:
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_moved_classes:
# TODO We need to deal with case where we have removed all imported classes
tokens[i] = tokens[i]._replace(src="")
j = i + 1
while tokens[j].src == "," or tokens[j].name == 'UNIMPORTANT_WS' or tokens[j].name == 'NEWLINE':
tokens[j] = tokens[j]._replace(src="")
j += 1
if token.offset == last_import:
new_tokens = [NEW_LINE] * 2 + import_tokens("QtCore", ["QT_VERSION"])
new_tokens += [NEW_LINE] + src_to_tokens("if (QT_VERSION >= 0x060000):") + [NEW_LINE]
for module, classes in new_import_qt6.items():
new_tokens += [Token('INDENT', ' ')] + import_tokens(module, classes)
new_tokens += [NEW_LINE] + src_to_tokens("else:") + [NEW_LINE]
for module, classes in new_import_qt5.items():
new_tokens += [Token('INDENT', ' ')] + import_tokens(module, classes)
for tok in reversed(new_tokens):
tokens.insert(i, tok)
new_contents = tokens_to_src(tokens)
with (open(filename, 'w') as f):
f.write(new_contents)
return new_contents != contents
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('directory')
# parser.add_argument('--exit-zero-even-if-changed', action='store_true')
# parser.add_argument('--py35-plus', action='store_true')
# parser.add_argument('--py36-plus', action='store_true')
args = parser.parse_args(argv)
print(os.path.join(args.directory, "*.py"))
ret = 0
for filename in glob.glob(os.path.join(args.directory, "*.py")):
ret |= fix_file(filename, args)
return ret
if __name__ == '__main__':
raise SystemExit(main())