mirror of
https://github.com/qgis/QGIS.git
synced 2025-02-23 00:02:38 -05:00
287 lines
10 KiB
Python
Executable File
287 lines
10 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
|
|
import sys
|
|
|
|
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
|
|
|
|
try:
|
|
import qgis.core as qgis_core
|
|
import qgis.gui as qgis_gui
|
|
import qgis.analysis as qgis_analysis
|
|
import qgis._3d as qgis_3d
|
|
except ImportError:
|
|
qgis_core = None
|
|
qgis_gui = None
|
|
qgis_analysis = None
|
|
qgis_3d = None
|
|
print('QGIS classes not available for introspection, only a partial upgrade will be performed')
|
|
|
|
# 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",
|
|
}
|
|
|
|
deprecated_renamed_enums = {
|
|
('Qt', 'MidButton'): ('MouseButton', 'MiddleButton')
|
|
}
|
|
|
|
# dict of function name to known enum class name
|
|
disambiguated_enums = {
|
|
'pushMessage': ('Qgis', 'MessageLevel')
|
|
}
|
|
|
|
# { (class, enum_value) : enum_name }
|
|
qt_enums = {}
|
|
ambiguous_enums = defaultdict(set)
|
|
|
|
|
|
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 = {} # Unscoping of enums
|
|
rename_qt_enums = [] # Renaming deprecated removed enums
|
|
|
|
tree = ast.parse(contents, filename=filename)
|
|
for parent in ast.walk(tree):
|
|
|
|
for node in ast.iter_child_nodes(parent):
|
|
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 ambiguous_enums):
|
|
disambiguated = False
|
|
if isinstance(parent, ast.Call) and isinstance(parent.func, ast.Attribute):
|
|
function_name = parent.func.attr
|
|
if function_name in disambiguated_enums:
|
|
disambiguated = True
|
|
fix_qt_enums[Offset(node.lineno, node.col_offset)] = (
|
|
disambiguated_enums)[function_name]
|
|
|
|
if not disambiguated:
|
|
possible_values = [f'{node.value.id}.{e}.{node.attr}' for e
|
|
in ambiguous_enums[
|
|
(node.value.id, node.attr)]]
|
|
sys.stderr.write(f'{filename}:{node.lineno}:{node.col_offset} WARNING: ambiguous enum, cannot fix: {node.value.id}.{node.attr}. Could be: {", ".join(possible_values)}\n')
|
|
elif (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))
|
|
fix_qt_enums[Offset(node.lineno, node.col_offset)] = (node.value.id, qt_enums[(node.value.id, node.attr)])
|
|
|
|
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 == "."
|
|
_class, enum_name = fix_qt_enums[token.offset] # = 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 ekey, evalue in value.__dict__.items():
|
|
if isinstance(evalue, value):
|
|
if (item.__name__, ekey) in ambiguous_enums:
|
|
if value.__name__ not in ambiguous_enums[(item.__name__, ekey)]:
|
|
ambiguous_enums[(item.__name__, ekey)].add(value.__name__)
|
|
continue
|
|
|
|
existing_entry = qt_enums.get((item.__name__, ekey))
|
|
if existing_entry != value.__name__ and existing_entry:
|
|
ambiguous_enums[(item.__name__, ekey)].add(existing_entry)
|
|
ambiguous_enums[(item.__name__, ekey)].add(
|
|
value.__name__)
|
|
del qt_enums[(item.__name__, ekey)]
|
|
else:
|
|
qt_enums[(item.__name__, ekey)] = 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('--qgis3-incompatible-changes', action='store_true',
|
|
help='Apply modifications that would break behavior on QGIS 3, hence code may not work on QGIS 3')
|
|
args = parser.parse_args(argv)
|
|
|
|
# get all scope for all qt enum
|
|
enum_modules = [QtCore,
|
|
QtGui,
|
|
QtWidgets,
|
|
QtTest,
|
|
QtSql,
|
|
QtSvg,
|
|
QtXml,
|
|
QtNetwork,
|
|
QtPrintSupport,
|
|
Qsci]
|
|
if qgis_core is not None:
|
|
enum_modules.extend([
|
|
qgis_core,
|
|
qgis_gui,
|
|
qgis_analysis,
|
|
qgis_3d
|
|
]
|
|
)
|
|
for module in enum_modules:
|
|
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):
|
|
# print(f'Processing {filename}')
|
|
if 'auto_additions' in filename:
|
|
continue
|
|
|
|
ret |= fix_file(filename, not args.qgis3_incompatible_changes)
|
|
return ret
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|