forked from AG_QGIS/Plugin_SN_Basis
Tests überarbeitet, Mocks und coverage eingefügt
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
#Modul zur Prüfung und zum Exception Handling für Dateieingaben
|
||||
#Dateipruefer.py
|
||||
|
||||
import os
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
1
modules/Datenbankpruefer.py
Normal file
1
modules/Datenbankpruefer.py
Normal file
@@ -0,0 +1 @@
|
||||
#Datenbankpruefer.py
|
||||
@@ -1,5 +1,6 @@
|
||||
from PyQt5.QtWidgets import QMessageBox, QFileDialog
|
||||
from Dateipruefer import DateiEntscheidung
|
||||
#Pruefmanager.py
|
||||
from modules.qt_compat import QMessageBox, QFileDialog, YES, NO, CANCEL, ICON_QUESTION, exec_dialog
|
||||
from modules.Dateipruefer import DateiEntscheidung
|
||||
|
||||
class PruefManager:
|
||||
|
||||
@@ -8,40 +9,40 @@ class PruefManager:
|
||||
self.plugin_pfad = plugin_pfad
|
||||
|
||||
def frage_datei_ersetzen_oder_anhaengen(self, pfad: str) -> DateiEntscheidung:
|
||||
"""Fragt den Nutzer, ob die vorhandene Datei ersetzt, angehängt oder abgebrochen werden soll."""
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Question)
|
||||
msg.setIcon(ICON_QUESTION)
|
||||
msg.setWindowTitle("Datei existiert")
|
||||
msg.setText(f"Die Datei '{pfad}' existiert bereits.\nWas möchtest du tun?")
|
||||
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
|
||||
msg.setDefaultButton(QMessageBox.Yes)
|
||||
msg.button(QMessageBox.Yes).setText("Ersetzen")
|
||||
msg.button(QMessageBox.No).setText("Anhängen")
|
||||
msg.button(QMessageBox.Cancel).setText("Abbrechen")
|
||||
|
||||
result = msg.exec_()
|
||||
msg.setStandardButtons(YES | NO | CANCEL)
|
||||
msg.setDefaultButton(YES)
|
||||
|
||||
if result == QMessageBox.Yes:
|
||||
msg.button(YES).setText("Ersetzen")
|
||||
msg.button(NO).setText("Anhängen")
|
||||
msg.button(CANCEL).setText("Abbrechen")
|
||||
|
||||
result = exec_dialog(msg)
|
||||
|
||||
if result == YES:
|
||||
return DateiEntscheidung.ERSETZEN
|
||||
elif result == QMessageBox.No:
|
||||
elif result == NO:
|
||||
return DateiEntscheidung.ANHAENGEN
|
||||
else:
|
||||
return DateiEntscheidung.ABBRECHEN
|
||||
|
||||
def frage_temporär_verwenden(self) -> bool:
|
||||
"""Fragt den Nutzer, ob mit temporären Layern gearbeitet werden soll."""
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Question)
|
||||
msg.setIcon(ICON_QUESTION)
|
||||
msg.setWindowTitle("Temporäre Layer")
|
||||
msg.setText("Kein Speicherpfad wurde angegeben.\nMit temporären Layern fortfahren?")
|
||||
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
||||
msg.setDefaultButton(QMessageBox.Yes)
|
||||
|
||||
result = msg.exec_()
|
||||
return result == QMessageBox.Yes
|
||||
msg.setStandardButtons(YES | NO)
|
||||
msg.setDefaultButton(YES)
|
||||
|
||||
result = exec_dialog(msg)
|
||||
return result == YES
|
||||
|
||||
def waehle_dateipfad(self, titel="Speicherort wählen", filter="GeoPackage (*.gpkg)") -> str:
|
||||
"""Öffnet einen QFileDialog zur Dateiauswahl."""
|
||||
pfad, _ = QFileDialog.getSaveFileName(
|
||||
parent=None,
|
||||
caption=titel,
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
# Importiert den Event-Loop und URL-Objekte aus der PyQt-Bibliothek von QGIS
|
||||
from qgis.PyQt.QtCore import QEventLoop, QUrl
|
||||
# Importiert den NetworkAccessManager aus dem QGIS Core-Modul
|
||||
from qgis.core import QgsNetworkAccessManager
|
||||
# Importiert das QNetworkRequest-Objekt für HTTP-Anfragen
|
||||
from qgis.PyQt.QtNetwork import QNetworkRequest
|
||||
# Importiert die Klasse für das Ergebnisobjekt der Prüfung
|
||||
from pruef_ergebnis import PruefErgebnis
|
||||
# Linkpruefer.py – Qt5/Qt6-kompatibel über qt_compat
|
||||
|
||||
from modules.qt_compat import (
|
||||
QEventLoop,
|
||||
QUrl,
|
||||
QNetworkRequest,
|
||||
QNetworkReply
|
||||
)
|
||||
|
||||
from qgis.core import QgsNetworkAccessManager
|
||||
from modules.pruef_ergebnis import PruefErgebnis
|
||||
|
||||
|
||||
# Definiert die Klasse zum Prüfen von Links
|
||||
class Linkpruefer:
|
||||
"""Prüft den Link mit QgsNetworkAccessManager und klassifiziert Anbieter nach Attribut."""
|
||||
|
||||
# Statische Zuordnung möglicher Anbietertypen als Konstanten
|
||||
ANBIETER_TYPEN: dict[str, str] = {
|
||||
"REST": "REST",
|
||||
"WFS": "WFS",
|
||||
@@ -19,76 +21,57 @@ class Linkpruefer:
|
||||
"OGR": "OGR"
|
||||
}
|
||||
|
||||
# Konstruktor zum Initialisieren der Instanz
|
||||
def __init__(self, link: str, anbieter: str):
|
||||
# Speichert den übergebenen Link als Instanzvariable
|
||||
self.link = link
|
||||
|
||||
# Speichert den Anbietertyp, bereinigt und in Großbuchstaben (auch wenn leer oder None)
|
||||
self.anbieter = anbieter.upper().strip() if anbieter else ""
|
||||
# Erstellt einen neuen NetworkAccessManager für Netzwerkverbindungen
|
||||
self.network_manager = QgsNetworkAccessManager()
|
||||
|
||||
# Methode zur Klassifizierung des Anbieters und der Quelle
|
||||
def klassifiziere_anbieter(self):
|
||||
# Bestimmt den Typ auf Basis der vorgegebenen Konstante oder nimmt den Rohwert
|
||||
typ = self.ANBIETER_TYPEN.get(self.anbieter, self.anbieter)
|
||||
# Unterscheidet zwischen "remote" (http/https) oder "local" (Dateipfad)
|
||||
quelle = "remote" if self.link.startswith(("http://", "https://")) else "local"
|
||||
# Gibt Typ und Quelle als Dictionary zurück
|
||||
return {
|
||||
"typ": typ,
|
||||
"quelle": quelle
|
||||
}
|
||||
return {"typ": typ, "quelle": quelle}
|
||||
|
||||
|
||||
# Prüft die Erreichbarkeit und Plausibilität des Links
|
||||
def pruefe_link(self):
|
||||
# Initialisiert Listen für Fehler und Warnungen
|
||||
fehler = []
|
||||
warnungen = []
|
||||
|
||||
# Prüft, ob ein Link übergeben wurde
|
||||
if not self.link:
|
||||
fehler.append("Link fehlt.")
|
||||
return PruefErgebnis(False, fehler=fehler, warnungen=warnungen)
|
||||
|
||||
# Prüft, ob ein Anbieter angegeben ist
|
||||
|
||||
if not self.anbieter or not self.anbieter.strip():
|
||||
fehler.append("Anbieter muss gesetzt werden und darf nicht leer sein.")
|
||||
|
||||
# Prüfung für Remote-Links (http/https)
|
||||
# Remote-Links prüfen
|
||||
if self.link.startswith(("http://", "https://")):
|
||||
# Erstellt eine HTTP-Anfrage mit dem Link
|
||||
request = QNetworkRequest(QUrl(self.link))
|
||||
# Startet eine HEAD-Anfrage über den NetworkManager
|
||||
reply = self.network_manager.head(request)
|
||||
|
||||
# Wartet synchron auf die Netzwerkanwort (Event Loop)
|
||||
loop = QEventLoop()
|
||||
reply.finished.connect(loop.quit)
|
||||
loop.exec_()
|
||||
loop.exec() # Qt5/Qt6-kompatibel über qt_compat
|
||||
|
||||
# Prüft auf Netzwerkfehler
|
||||
if reply.error():
|
||||
# Fehlerprüfung Qt5/Qt6-kompatibel
|
||||
if reply.error() != QNetworkReply.NetworkError.NoError:
|
||||
fehler.append(f"Verbindungsfehler: {reply.errorString()}")
|
||||
else:
|
||||
# Holt den HTTP-Statuscode aus der Antwort
|
||||
status = reply.attribute(reply.HttpStatusCodeAttribute)
|
||||
# Prüft, ob der Status außerhalb des Erfolgsbereichs liegt
|
||||
status = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status is None or status < 200 or status >= 400:
|
||||
fehler.append(f"Link nicht erreichbar: HTTP {status}")
|
||||
# Räumt die Antwort auf (Vermeidung von Speicherlecks)
|
||||
|
||||
reply.deleteLater()
|
||||
|
||||
else:
|
||||
# Plausibilitäts-Check für lokale Links (Dateien), prüft auf Dateiendung
|
||||
# Lokale Pfade: Plausibilitätscheck
|
||||
if "." not in self.link.split("/")[-1]:
|
||||
warnungen.append("Der lokale Link sieht ungewöhnlich aus.")
|
||||
|
||||
# Gibt das Ergebnisobjekt mit allen gesammelten Informationen zurück
|
||||
return PruefErgebnis(len(fehler) == 0, daten=self.klassifiziere_anbieter(), fehler=fehler, warnungen=warnungen)
|
||||
return PruefErgebnis(
|
||||
len(fehler) == 0,
|
||||
daten=self.klassifiziere_anbieter(),
|
||||
fehler=fehler,
|
||||
warnungen=warnungen
|
||||
)
|
||||
|
||||
# Führt die Linkprüfung als externe Methode aus
|
||||
def ausfuehren(self):
|
||||
# Gibt das Ergebnis der Prüf-Methode zurück
|
||||
return self.pruefe_link()
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Klasse zur Definition eines Pruefergebnis-Objekts, das in allen Prüfern verwendet werden kann
|
||||
class PruefErgebnis:
|
||||
def __init__(self, erfolgreich: bool, daten=None, fehler=None, warnungen=None):
|
||||
self.erfolgreich = erfolgreich
|
||||
self.daten = daten or {}
|
||||
self.fehler = fehler or []
|
||||
self.warnungen = warnungen or []
|
||||
|
||||
def __repr__(self):
|
||||
return (f"PruefErgebnis(erfolgreich={self.erfolgreich}, "
|
||||
f"daten={self.daten}, fehler={self.fehler}, warnungen={self.warnungen})")
|
||||
@@ -1,3 +1,4 @@
|
||||
#pruef_ergebnis.py
|
||||
# Klasse zur Definition eines Pruefergebnis-Objekts, das in allen Prüfern verwendet werden kann
|
||||
class PruefErgebnis:
|
||||
def __init__(self, erfolgreich: bool, daten=None, fehler=None, warnungen=None):
|
||||
|
||||
111
modules/qt_compat.py
Normal file
111
modules/qt_compat.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
qt_compat.py – Einheitliche Qt-Kompatibilitätsschicht für QGIS-Plugins.
|
||||
|
||||
Ziele:
|
||||
- PyQt6 bevorzugt
|
||||
- Fallback auf PyQt5
|
||||
- Mock-Modus, wenn kein Qt verfügbar ist (z. B. in Unittests)
|
||||
- OR-fähige Fake-Enums im Mock-Modus
|
||||
"""
|
||||
|
||||
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Versuch: PyQt6 importieren
|
||||
# ---------------------------------------------------------
|
||||
try:
|
||||
from PyQt6.QtWidgets import QMessageBox, QFileDialog
|
||||
from PyQt6.QtCore import Qt, QEventLoop, QUrl
|
||||
from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
|
||||
YES = QMessageBox.StandardButton.Yes
|
||||
NO = QMessageBox.StandardButton.No
|
||||
CANCEL = QMessageBox.StandardButton.Cancel
|
||||
ICON_QUESTION = QMessageBox.Icon.Question
|
||||
|
||||
QT_VERSION = 6
|
||||
|
||||
def exec_dialog(dialog):
|
||||
"""Einheitliche Ausführung eines Dialogs."""
|
||||
return dialog.exec()
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Versuch: PyQt5 importieren
|
||||
# ---------------------------------------------------------
|
||||
except Exception:
|
||||
try:
|
||||
from PyQt5.QtWidgets import QMessageBox, QFileDialog
|
||||
from PyQt5.QtCore import Qt, QEventLoop, QUrl
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
|
||||
YES = QMessageBox.Yes
|
||||
NO = QMessageBox.No
|
||||
CANCEL = QMessageBox.Cancel
|
||||
ICON_QUESTION = QMessageBox.Question
|
||||
|
||||
QT_VERSION = 5
|
||||
|
||||
def exec_dialog(dialog):
|
||||
return dialog.exec_()
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Mock-Modus (kein Qt verfügbar)
|
||||
# ---------------------------------------------------------
|
||||
except Exception:
|
||||
QT_VERSION = 0
|
||||
|
||||
class FakeEnum(int):
|
||||
"""Ein OR-fähiger Enum-Ersatz für den Mock-Modus."""
|
||||
def __or__(self, other):
|
||||
return FakeEnum(int(self) | int(other))
|
||||
|
||||
class QMessageBox:
|
||||
Yes = FakeEnum(1)
|
||||
No = FakeEnum(2)
|
||||
Cancel = FakeEnum(4)
|
||||
Question = FakeEnum(8)
|
||||
|
||||
class QFileDialog:
|
||||
"""Minimaler Mock für QFileDialog."""
|
||||
@staticmethod
|
||||
def getOpenFileName(*args, **kwargs):
|
||||
return ("", "") # kein Dateipfad
|
||||
|
||||
YES = QMessageBox.Yes
|
||||
NO = QMessageBox.No
|
||||
CANCEL = QMessageBox.Cancel
|
||||
ICON_QUESTION = QMessageBox.Question
|
||||
|
||||
def exec_dialog(dialog):
|
||||
"""Mock-Ausführung: gibt YES zurück, außer Tests patchen es."""
|
||||
return YES
|
||||
# -------------------------
|
||||
# Mock Netzwerk-Klassen
|
||||
# -------------------------
|
||||
class QEventLoop:
|
||||
def exec(self):
|
||||
return 0
|
||||
|
||||
def quit(self):
|
||||
pass
|
||||
|
||||
class QUrl(str):
|
||||
pass
|
||||
|
||||
class QNetworkRequest:
|
||||
def __init__(self, url):
|
||||
self.url = url
|
||||
|
||||
class QNetworkReply:
|
||||
def __init__(self):
|
||||
self._data = b""
|
||||
|
||||
def readAll(self):
|
||||
return self._data
|
||||
|
||||
def error(self):
|
||||
return 0
|
||||
|
||||
def exec_dialog(dialog):
|
||||
return YES
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#stilpruefer.py
|
||||
import os
|
||||
from pruef_ergebnis import PruefErgebnis
|
||||
from modules.pruef_ergebnis import PruefErgebnis
|
||||
|
||||
|
||||
class Stilpruefer:
|
||||
|
||||
Reference in New Issue
Block a user