Enums von PyQt5 nach PyQt6 konvertieren

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Benutzeravatar
JakobDev
User
Beiträge: 63
Registriert: Mittwoch 17. Juli 2019, 17:20

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.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

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:

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()
Benutzeravatar
__blackjack__
User
Beiträge: 13004
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):

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()
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.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Antworten