Enums von PyQt5 nach PyQt6 konvertieren
Da die Portierung eines Projekts von PyQt5 nach PyQt6 aufgrund der geänderten Enums jede Menge Schreibarbeit benötigt, habe ich ein kleines Skript geschrieben, um das ganze zu erleichtern. Das Skript durchsucht PyQt6 nach Enums und konvertiert sie automatisch mithilfe von einfachen Textersetzen. Da das ganze innerhalb von wenigen Stunden geschrieben wurde, funktioniert es natürlich nicht perfekt. Auch as Importe werden nicht unterstützt. An der ein oder anderen Stelle ist auch ein wenig Handarbeit notwendig. Bei einem großen Projekt von mir (5.000+ Zeilen) hat das Skript in Sachen Enums 95% der Arbeit für mich erledigt. Vor der Benutzung aber unbedingt eine Sicherheitskopie machen. Das ganze ist auch über pip installierbar.
Statt das übergebene Wörterbuch zu ändern, erzeuge doch einfach nur das innere Wörterbuch und füge die Wörterbücher in der aufrufenden Funktion zusammen.
Statt auf die vielen __-Attribute zuzugreifen, benutze z.B. inspect.getmembers
In `parse_pyqt_object` ist das `obj` eigentlich ein Modul. `i` ist ein besonders schlechter Variablenname für einen (Klassen-)Namen.
Benutze keine nakten except, sondern fange nur die Exceptions ab, die Du auch erwartest. So wie das Programm jetzt geschrieben ist, ist es unmöglich Programmierfehler zu finden.
`i` heißen bei Dir sehr viele verschieden Dinge, auch Dateinamen. Statt os.listdir und ähnlichem, benutzt man heutzutage pathlib.Path.
Statt eines Skripts, das nur ein Paket importiert, das nur ein Modul enthält, könnte man auch gleich nur eine Skript-Datei verwenden:
Statt auf die vielen __-Attribute zuzugreifen, benutze z.B. inspect.getmembers
In `parse_pyqt_object` ist das `obj` eigentlich ein Modul. `i` ist ein besonders schlechter Variablenname für einen (Klassen-)Namen.
Benutze keine nakten except, sondern fange nur die Exceptions ab, die Du auch erwartest. So wie das Programm jetzt geschrieben ist, ist es unmöglich Programmierfehler zu finden.
`i` heißen bei Dir sehr viele verschieden Dinge, auch Dateinamen. Statt os.listdir und ähnlichem, benutzt man heutzutage pathlib.Path.
Statt eines Skripts, das nur ein Paket importiert, das nur ein Modul enthält, könnte man auch gleich nur eine Skript-Datei verwenden:
Code: Alles auswählen
import enum
import sys
from pathlib import Path
from inspect import getmembers
from PyQt6 import QtWidgets, QtCore, QtGui
def get_enums_of_object(obj):
result = {}
for name, member in getmembers(obj):
if isinstance(member, enum.EnumMeta):
result[name] = [attribute.name for attribute in member]
def parse_pyqt_object(module):
result = {}
for name, obj in getmembers(module):
if not name.startswith('_'):
try:
result[name] = get_enums_of_object(obj)
except RuntimeError:
pass
return result
def replace_in_file(path, data):
try:
content = path.read_text(encoding="utf8")
except IOError:
print(f"Can't read {path}", file=sys.stderr)
return
for module, enums in data.items():
for enum, attributes in enums.items():
for name in attributes:
content = content.replace(f"{module}.{name}", f"{module}.{enum}.{name}")
path.write_text(content, encoding="utf8")
def loop_directory(path, data):
for filename in path.rglob('*.py'):
if filename.is_file():
replace_in_file(filename, data)
def main():
if len(sys.argv) < 2:
print("Usage: PyQtEnumConverter <file/directory>", file=sys.stderr)
sys.exit(1)
target = Path(sys.argv[1])
if not target.exists():
print(f"{target} does not exists", file=sys.stderr)
sys.exit(1)
data = {}
data.update(parse_pyqt_object(QtWidgets))
data.update(parse_pyqt_object(QtCore))
data.update(parse_pyqt_object(QtGui))
try:
from PyQt6 import Qsci
except:
pass
else:
data.update(parse_pyqt_object(Qsci))
if target.is_dir():
loop_directory(target, data)
else:
replace_in_file(target, data)
if __name__ == "__main__":
main()
- __blackjack__
- User
- Beiträge: 13163
- Registriert: Samstag 2. Juni 2018, 10:21
- Wohnort: 127.0.0.1
- Kontaktdaten:
In `get_enums_of_object()` fehlt das ``return``, und man kann das als „comprehension“ formulieren.
Nur weil's zu selten verwendet wird habe ich mal `contextlib.suppress()` an passender Stelle eingebaut.
Ich würde den Import von `Qsci` auch an den Anfang verschieben. Oder alternativ die Importe nicht mit ``import`` machen, sondern mit `importlib` dynamisch. Jedenfalls alle an einem Ort und nicht zwischen fast ganz oben und fast ganz unten im Code aufgeteilt.
`loop_directory()` macht nicht so wirklich viel ”eigenes”, das würde ich in die Hauptfunktion integrieren und den ”eine Datei Fall” so ändern, dass der Code auch dafür geht.
Zwischenstand (ungetestet):
Ich würde da noch zwei Ansätze verfolgen/ausprobieten:
a) schauen ob/wie man es hinbekommt das gesamte `PyQt6`-Package rekursiv dynamisch zu importieren, statt manuell Modulnamen in den Code schreiben zu müssen.
b) die Enums versuchen ”von der anderen Seite” her zu ermitteln. Also nicht alle Module und Objekte abklappern, sondern nach dem Importieren von `enum.Enum.__subclasses__()` aus alles zu suchen, dessen voll qualifizierter Name mit `PyQt6` anfängt. Und da dann mal Ergebnisse und Laufzeiten vergleichen. Rein aus interesse.
Nur weil's zu selten verwendet wird habe ich mal `contextlib.suppress()` an passender Stelle eingebaut.
Ich würde den Import von `Qsci` auch an den Anfang verschieben. Oder alternativ die Importe nicht mit ``import`` machen, sondern mit `importlib` dynamisch. Jedenfalls alle an einem Ort und nicht zwischen fast ganz oben und fast ganz unten im Code aufgeteilt.
`loop_directory()` macht nicht so wirklich viel ”eigenes”, das würde ich in die Hauptfunktion integrieren und den ”eine Datei Fall” so ändern, dass der Code auch dafür geht.
Zwischenstand (ungetestet):
Code: Alles auswählen
#!/usr/bin/env python3
import enum
import sys
from contextlib import suppress
from inspect import getmembers
from pathlib import Path
from PyQt6 import QtCore, QtGui, QtWidgets
try:
from PyQt6 import Qsci
except ImportError:
Qsci = object()
def get_enums_of_object(obj):
return {
name: [attribute.name for attribute in member]
for name, member in getmembers(obj)
if isinstance(member, enum.EnumMeta)
}
def parse_pyqt_object(module):
result = {}
for name, obj in getmembers(module):
if not name.startswith("_"):
with suppress(RuntimeError):
result[name] = get_enums_of_object(obj)
return result
def replace_in_file(path, data):
try:
content = path.read_text(encoding="utf8")
except IOError:
print(f"Can't read {path!a}", file=sys.stderr)
return
for module, enums in data.items():
for enum, attributes in enums.items():
for name in attributes:
content = content.replace(
f"{module}.{name}", f"{module}.{enum}.{name}"
)
path.write_text(content, encoding="utf8")
def main():
if len(sys.argv) < 2:
print("Usage: PyQtEnumConverter <file/directory>", file=sys.stderr)
sys.exit(1)
target = Path(sys.argv[1])
if not target.exists():
print(f"{target!a} does not exists", file=sys.stderr)
sys.exit(1)
data = {}
for module in [QtWidgets, QtCore, QtGui, Qsci]:
data.update(parse_pyqt_object(module))
for filename in target.rglob("*.py") if target.is_dir() else [target]:
if filename.is_file():
replace_in_file(filename, data)
if __name__ == "__main__":
main()
a) schauen ob/wie man es hinbekommt das gesamte `PyQt6`-Package rekursiv dynamisch zu importieren, statt manuell Modulnamen in den Code schreiben zu müssen.
b) die Enums versuchen ”von der anderen Seite” her zu ermitteln. Also nicht alle Module und Objekte abklappern, sondern nach dem Importieren von `enum.Enum.__subclasses__()` aus alles zu suchen, dessen voll qualifizierter Name mit `PyQt6` anfängt. Und da dann mal Ergebnisse und Laufzeiten vergleichen. Rein aus interesse.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis