Wrappe modular aufgebaut, Tests erfolgreich, Menüleiste und Werzeugleiste werden eingetragen (QT6 und QT5)- (Es fehlen noch Fachplugins, um zu prüfen, ob es auch wirklich in QGIS geht)

This commit is contained in:
2025-12-19 14:29:52 +01:00
parent e8fea163b5
commit f88b5da51f
37 changed files with 1886 additions and 1679 deletions

View File

@@ -0,0 +1,43 @@
from .ly_existence_wrapper import layer_exists
from .ly_geometry_wrapper import (
get_layer_geometry_type,
get_layer_feature_count,
)
from .ly_visibility_wrapper import (
is_layer_visible,
set_layer_visible,
)
from .ly_metadata_wrapper import (
get_layer_type,
get_layer_crs,
get_layer_fields,
get_layer_source,
is_layer_editable,
)
from .ly_style_wrapper import apply_style
from .dialog_wrapper import ask_yes_no
from .message_wrapper import (
_get_message_bar,
push_message,
error,
warning,
info,
success,
)
from .os_wrapper import *
from .qgiscore_wrapper import *
from .qt_wrapper import *
from .settings_logic import *
from .sys_wrapper import *
from .variable_wrapper import *
from .qgisui_wrapper import (
get_main_window,
add_dock_widget,
remove_dock_widget,
find_dock_widgets,
add_menu,
remove_menu,
add_toolbar,
remove_toolbar)

View File

@@ -0,0 +1,41 @@
"""
sn_basis/functions/dialog_wrapper.py Benutzer-Dialoge
"""
from typing import Any
from sn_basis.functions.qt_wrapper import (
QMessageBox,
YES,
NO,
)
# ---------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------
def ask_yes_no(
title: str,
message: str,
default: bool = False,
parent: Any = None,
) -> bool:
"""
Fragt den Benutzer eine Ja/Nein-Frage.
- In Qt: zeigt einen QMessageBox-Dialog
- Im Mock-Modus: gibt den Default-Wert zurück
"""
try:
buttons = QMessageBox.Yes | QMessageBox.No
result = QMessageBox.question(
parent,
title,
message,
buttons,
YES if default else NO,
)
return result == YES
except Exception:
return default

View File

@@ -0,0 +1,20 @@
# sn_basis/functions/ly_existence_wrapper.py
def layer_exists(layer) -> bool:
if layer is None:
return False
is_valid_flag = getattr(layer, "is_valid", None)
if is_valid_flag is not None:
try:
return bool(is_valid_flag)
except Exception:
return False
try:
is_valid = getattr(layer, "isValid", None)
if callable(is_valid):
return bool(is_valid())
return True
except Exception:
return False

View File

@@ -0,0 +1,57 @@
# sn_basis/functions/ly_geometry_wrapper.py
def get_layer_geometry_type(layer) -> str:
if layer is None:
return "None"
geometry_type = getattr(layer, "geometry_type", None)
if geometry_type is not None:
return str(geometry_type)
try:
if callable(getattr(layer, "isSpatial", None)) and not layer.isSpatial():
return "None"
gtype = getattr(layer, "geometryType", None)
if callable(gtype):
value = gtype()
if not isinstance(value, int):
return "None"
return {
0: "Point",
1: "LineString",
2: "Polygon",
}.get(value, "None")
except Exception:
pass
return "None"
def get_layer_feature_count(layer) -> int:
if layer is None:
return 0
count = getattr(layer, "feature_count", None)
if count is not None:
if isinstance(count, int):
return count
return 0
try:
if callable(getattr(layer, "isSpatial", None)) and not layer.isSpatial():
return 0
fc = getattr(layer, "featureCount", None)
if callable(fc):
value = fc()
if isinstance(value, int):
return value
except Exception:
pass
return 0

View File

@@ -0,0 +1,90 @@
# layer/metadata.py
def get_layer_type(layer) -> str:
if layer is None:
return "unknown"
layer_type = getattr(layer, "layer_type", None)
if layer_type is not None:
return str(layer_type)
try:
if callable(getattr(layer, "isSpatial", None)):
return "vector" if layer.isSpatial() else "table"
except Exception:
pass
return "unknown"
def get_layer_crs(layer) -> str:
if layer is None:
return "None"
crs = getattr(layer, "crs", None)
if crs is not None and not callable(crs):
if isinstance(crs, str):
return crs
return "None"
try:
crs_obj = layer.crs()
authid = getattr(crs_obj, "authid", None)
if callable(authid):
value = authid()
if isinstance(value, str):
return value
except Exception:
pass
return "None"
def get_layer_fields(layer) -> list[str]:
if layer is None:
return []
fields = getattr(layer, "fields", None)
if fields is not None and not callable(fields):
return list(fields)
try:
f = layer.fields()
if callable(getattr(f, "names", None)):
return list(f.names())
return list(f)
except Exception:
return []
def get_layer_source(layer) -> str:
if layer is None:
return "None"
source = getattr(layer, "source", None)
if source is not None and not callable(source):
return str(source)
try:
return layer.source() or "None"
except Exception:
return "None"
def is_layer_editable(layer) -> bool:
if layer is None:
return False
editable = getattr(layer, "editable", None)
if editable is not None:
return bool(editable)
try:
is_editable = getattr(layer, "isEditable", None)
if callable(is_editable):
return bool(is_editable())
except Exception:
pass
return False

View File

@@ -0,0 +1,27 @@
# layer/style.py
from sn_basis.functions.ly_existence_wrapper import layer_exists
from sn_basis.functions.sys_wrapper import (
get_plugin_root,
join_path,
file_exists,
)
def apply_style(layer, style_name: str) -> bool:
if not layer_exists(layer):
return False
style_path = join_path(get_plugin_root(), "styles", style_name)
if not file_exists(style_path):
return False
try:
ok, _ = layer.loadNamedStyle(style_path)
if ok:
getattr(layer, "triggerRepaint", lambda: None)()
return True
except Exception:
pass
return False

View File

@@ -0,0 +1,40 @@
# sn_basis/functions/ly_visibility_wrapper.py
def is_layer_visible(layer) -> bool:
if layer is None:
return False
visible = getattr(layer, "visible", None)
if visible is not None:
return bool(visible)
try:
is_visible = getattr(layer, "isVisible", None)
if callable(is_visible):
return bool(is_visible())
except Exception:
pass
return False
def set_layer_visible(layer, visible: bool) -> bool:
if layer is None:
return False
try:
if hasattr(layer, "visible"):
layer.visible = bool(visible)
return True
except Exception:
pass
try:
node = getattr(layer, "treeLayer", lambda: None)()
if node and callable(getattr(node, "setItemVisibilityChecked", None)):
node.setItemVisibilityChecked(bool(visible))
return True
except Exception:
pass
return False

View File

@@ -0,0 +1,84 @@
"""
sn_basis/functions/message_wrapper.py zentrale MessageBar-Abstraktion
"""
from typing import Any
from sn_basis.functions.qgisui_wrapper import iface
from sn_basis.functions.qgiscore_wrapper import Qgis
# ---------------------------------------------------------
# Interne Hilfsfunktion
# ---------------------------------------------------------
def _get_message_bar():
"""
Liefert eine MessageBar-Instanz (QGIS oder Mock).
"""
try:
bar = iface.messageBar()
if bar is not None:
return bar
except Exception:
pass
class _MockMessageBar:
def pushMessage(self, title, text, level=0, duration=5):
return {
"title": title,
"text": text,
"level": level,
"duration": duration,
}
return _MockMessageBar()
# ---------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------
def push_message(
level: int,
title: str,
text: str,
duration: int = 5,
parent: Any = None,
):
"""
Zeigt eine Message in der QGIS-MessageBar an.
Im Mock-Modus wird ein strukturierter Dict zurückgegeben.
"""
bar = _get_message_bar()
try:
return bar.pushMessage(
title,
text,
level=level,
duration=duration,
)
except Exception:
return None
def info(title: str, text: str, duration: int = 5):
level = Qgis.MessageLevel.Info
return push_message(level, title, text, duration)
def warning(title: str, text: str, duration: int = 5):
level = Qgis.MessageLevel.Warning
return push_message(level, title, text, duration)
def error(title: str, text: str, duration: int = 5):
level = Qgis.MessageLevel.Critical
return push_message(level, title, text, duration)
def success(title: str, text: str, duration: int = 5):
level = Qgis.MessageLevel.Success
return push_message(level, title, text, duration)

77
functions/os_wrapper.py Normal file
View File

@@ -0,0 +1,77 @@
"""
sn_basis/functions/os_wrapper.py Betriebssystem-Abstraktion
"""
from pathlib import Path
import platform
from typing import Union
# ---------------------------------------------------------
# OS-Erkennung
# ---------------------------------------------------------
_SYSTEM = platform.system().lower()
if _SYSTEM.startswith("win"):
OS_NAME = "windows"
elif _SYSTEM.startswith("darwin"):
OS_NAME = "macos"
else:
OS_NAME = "linux"
IS_WINDOWS = OS_NAME == "windows"
IS_LINUX = OS_NAME == "linux"
IS_MACOS = OS_NAME == "macos"
# ---------------------------------------------------------
# OS-Eigenschaften
# ---------------------------------------------------------
PATH_SEPARATOR = "\\" if IS_WINDOWS else "/"
LINE_SEPARATOR = "\r\n" if IS_WINDOWS else "\n"
# ---------------------------------------------------------
# Pfad-Utilities
# ---------------------------------------------------------
_PathLike = Union[str, Path]
def normalize_path(path: _PathLike) -> Path:
"""
Normalisiert einen Pfad OS-unabhängig.
"""
try:
return Path(path).expanduser().resolve()
except Exception:
return Path(path)
def get_home_dir() -> Path:
"""
Liefert das Home-Verzeichnis des aktuellen Users.
"""
return Path.home()
# ---------------------------------------------------------
# Dateisystem-Eigenschaften
# ---------------------------------------------------------
def is_case_sensitive_fs() -> bool:
"""
Gibt zurück, ob das Dateisystem case-sensitiv ist.
"""
# Windows ist immer case-insensitive
if IS_WINDOWS:
return False
# macOS meist case-insensitive, aber nicht garantiert
if IS_MACOS:
return False
# Linux praktisch immer case-sensitiv
return True

View File

@@ -0,0 +1,139 @@
"""
sn_basis/functions/qgiscore_wrapper.py zentrale QGIS-Core-Abstraktion
"""
from typing import Type, Any
from sn_basis.functions.qt_wrapper import (
QUrl,
QEventLoop,
QNetworkRequest,
)
# ---------------------------------------------------------
# QGIS-Symbole (werden dynamisch gesetzt)
# ---------------------------------------------------------
QgsProject: Type[Any]
QgsVectorLayer: Type[Any]
QgsNetworkAccessManager: Type[Any]
Qgis: Type[Any]
QGIS_AVAILABLE = False
# ---------------------------------------------------------
# Versuch: QGIS-Core importieren
# ---------------------------------------------------------
try:
from qgis.core import (
QgsProject as _QgsProject,
QgsVectorLayer as _QgsVectorLayer,
QgsNetworkAccessManager as _QgsNetworkAccessManager,
Qgis as _Qgis,
)
QgsProject = _QgsProject
QgsVectorLayer = _QgsVectorLayer
QgsNetworkAccessManager = _QgsNetworkAccessManager
Qgis = _Qgis
QGIS_AVAILABLE = True
# ---------------------------------------------------------
# Mock-Modus
# ---------------------------------------------------------
except Exception:
QGIS_AVAILABLE = False
class _MockQgsProject:
def __init__(self):
self._variables = {}
@staticmethod
def instance() -> "_MockQgsProject":
return _MockQgsProject()
def read(self) -> bool:
return True
QgsProject = _MockQgsProject
class _MockQgsVectorLayer:
def __init__(self, *args, **kwargs):
self._valid = True
def isValid(self) -> bool:
return self._valid
def loadNamedStyle(self, path: str):
return True, ""
def triggerRepaint(self) -> None:
pass
QgsVectorLayer = _MockQgsVectorLayer
class _MockQgsNetworkAccessManager:
@staticmethod
def instance():
return _MockQgsNetworkAccessManager()
def head(self, request: Any):
return None
QgsNetworkAccessManager = _MockQgsNetworkAccessManager
class _MockQgis:
class MessageLevel:
Success = 0
Info = 1
Warning = 2
Critical = 3
Qgis = _MockQgis
# ---------------------------------------------------------
# Netzwerk
# ---------------------------------------------------------
class NetworkReply:
"""
Minimaler Wrapper für Netzwerkantworten.
"""
def __init__(self, error: int):
self.error = error
def network_head(url: str) -> NetworkReply | None:
"""
Führt einen HTTP-HEAD-Request aus.
Rückgabe:
- NetworkReply(error=0) → erreichbar
- NetworkReply(error!=0) → nicht erreichbar
- None → Netzwerk nicht verfügbar / Fehler beim Request
"""
if not QGIS_AVAILABLE:
return None
if QUrl is None or QNetworkRequest is None:
return None
try:
manager = QgsNetworkAccessManager.instance()
request = QNetworkRequest(QUrl(url))
reply = manager.head(request)
# synchron warten (kurz)
if QEventLoop is not None:
loop = QEventLoop()
reply.finished.connect(loop.quit)
loop.exec()
return NetworkReply(error=reply.error())
except Exception:
return None

View File

@@ -1,880 +0,0 @@
"""
sn_basis/functions/qgisqt_wrapper.py zentrale QGIS/Qt-Abstraktion
"""
from typing import Optional, Type, Any
# ---------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------
def getattr_safe(obj: Any, name: str, default: Any = None) -> Any:
"""
Sichere getattr-Variante:
- fängt Exceptions beim Attributzugriff ab
- liefert default zurück, wenn Attribut fehlt oder fehlschlägt
"""
try:
return getattr(obj, name)
except Exception:
return default
# ---------------------------------------------------------
# QtSymbole (werden später dynamisch importiert)
# ---------------------------------------------------------
QMessageBox: Optional[Type[Any]] = None
QFileDialog: Optional[Type[Any]] = None
QEventLoop: Optional[Type[Any]] = None
QUrl: Optional[Type[Any]] = None
QNetworkRequest: Optional[Type[Any]] = None
QNetworkReply: Optional[Type[Any]] = None
QCoreApplication: Optional[Type[Any]] = None
QWidget: Type[Any]
QGridLayout: Type[Any]
QLabel: Type[Any]
QLineEdit: Type[Any]
QGroupBox: Type[Any]
QVBoxLayout: Type[Any]
QPushButton: Type[Any]
YES: Optional[Any] = None
NO: Optional[Any] = None
CANCEL: Optional[Any] = None
ICON_QUESTION: Optional[Any] = None
def exec_dialog(dialog: Any) -> Any:
raise NotImplementedError
# ---------------------------------------------------------
# QGISSymbole (werden später dynamisch importiert)
# ---------------------------------------------------------
QgsProject: Optional[Type[Any]] = None
QgsVectorLayer: Optional[Type[Any]] = None
QgsNetworkAccessManager: Optional[Type[Any]] = None
Qgis: Optional[Type[Any]] = None
iface: Optional[Any] = None
# ---------------------------------------------------------
# QtVersionserkennung
# ---------------------------------------------------------
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
# ---------------------------------------------------------
# Versuch: PyQt6 importieren
# ---------------------------------------------------------
try:
from PyQt6.QtWidgets import ( #type: ignore
QMessageBox as _QMessageBox,
QFileDialog as _QFileDialog,
QWidget as _QWidget,
QGridLayout as _QGridLayout,
QLabel as _QLabel,
QLineEdit as _QLineEdit,
QGroupBox as _QGroupBox,
QVBoxLayout as _QVBoxLayout,
QPushButton as _QPushButton,
)
from PyQt6.QtCore import ( #type: ignore
Qt,
QEventLoop as _QEventLoop,
QUrl as _QUrl,
QCoreApplication as _QCoreApplication,
)
from PyQt6.QtNetwork import ( #type: ignore
QNetworkRequest as _QNetworkRequest,
QNetworkReply as _QNetworkReply,
)
QMessageBox = _QMessageBox
QFileDialog = _QFileDialog
QEventLoop = _QEventLoop
QUrl = _QUrl
QNetworkRequest = _QNetworkRequest
QNetworkReply = _QNetworkReply
QCoreApplication = _QCoreApplication
QWidget = _QWidget
QGridLayout = _QGridLayout
QLabel = _QLabel
QLineEdit = _QLineEdit
QGroupBox = _QGroupBox
QVBoxLayout = _QVBoxLayout
QPushButton = _QPushButton
if QMessageBox is not None:
YES = QMessageBox.StandardButton.Yes
NO = QMessageBox.StandardButton.No
CANCEL = QMessageBox.StandardButton.Cancel
ICON_QUESTION = QMessageBox.Icon.Question
QT_VERSION = 6
def exec_dialog(dialog: Any) -> Any:
return dialog.exec()
# ---------------------------------------------------------
# Versuch: PyQt5 importieren
# ---------------------------------------------------------
except Exception:
try:
from PyQt5.QtWidgets import (
QMessageBox as _QMessageBox,
QFileDialog as _QFileDialog,
QWidget as _QWidget,
QGridLayout as _QGridLayout,
QLabel as _QLabel,
QLineEdit as _QLineEdit,
QGroupBox as _QGroupBox,
QVBoxLayout as _QVBoxLayout,
QPushButton as _QPushButton,
)
from PyQt5.QtCore import (
Qt,
QEventLoop as _QEventLoop,
QUrl as _QUrl,
QCoreApplication as _QCoreApplication,
)
from PyQt5.QtNetwork import (
QNetworkRequest as _QNetworkRequest,
QNetworkReply as _QNetworkReply,
)
QMessageBox = _QMessageBox
QFileDialog = _QFileDialog
QEventLoop = _QEventLoop
QUrl = _QUrl
QNetworkRequest = _QNetworkRequest
QNetworkReply = _QNetworkReply
QCoreApplication = _QCoreApplication
QWidget = _QWidget
QGridLayout = _QGridLayout
QLabel = _QLabel
QLineEdit = _QLineEdit
QGroupBox = _QGroupBox
QVBoxLayout = _QVBoxLayout
QPushButton = _QPushButton
if QMessageBox is not None:
YES = QMessageBox.Yes
NO = QMessageBox.No
CANCEL = QMessageBox.Cancel
ICON_QUESTION = QMessageBox.Question
QT_VERSION = 5
def exec_dialog(dialog: Any) -> Any:
return dialog.exec_()
# ---------------------------------------------------------
# MockModus (kein Qt verfügbar)
# ---------------------------------------------------------
except Exception:
QT_VERSION = 0
class FakeEnum(int):
"""ORfähiger EnumErsatz für MockModus."""
def __new__(cls, value: int):
return int.__new__(cls, value)
def __or__(self, other: "FakeEnum") -> "FakeEnum":
return FakeEnum(int(self) | int(other))
class _MockQMessageBox:
Yes = FakeEnum(1)
No = FakeEnum(2)
Cancel = FakeEnum(4)
Question = FakeEnum(8)
QMessageBox = _MockQMessageBox
class _MockQFileDialog:
@staticmethod
def getOpenFileName(*args, **kwargs):
return ("", "")
@staticmethod
def getSaveFileName(*args, **kwargs):
return ("", "")
QFileDialog = _MockQFileDialog
class _MockQEventLoop:
def exec(self) -> int:
return 0
def quit(self) -> None:
pass
QEventLoop = _MockQEventLoop
class _MockQUrl(str):
def isValid(self) -> bool:
return True
QUrl = _MockQUrl
class _MockQNetworkRequest:
def __init__(self, url: Any):
self.url = url
QNetworkRequest = _MockQNetworkRequest
class _MockQNetworkReply:
class NetworkError:
NoError = 0
def __init__(self):
self._data = b""
def error(self) -> int:
return 0
def errorString(self) -> str:
return ""
def attribute(self, *args, **kwargs) -> Any:
return 200
def readAll(self) -> bytes:
return self._data
def deleteLater(self) -> None:
pass
QNetworkReply = _MockQNetworkReply
YES = FakeEnum(1)
NO = FakeEnum(2)
CANCEL = FakeEnum(4)
ICON_QUESTION = FakeEnum(8)
def exec_dialog(dialog: Any) -> Any:
return YES
class _MockWidget:
def __init__(self, *args, **kwargs):
pass
class _MockLayout:
def __init__(self, *args, **kwargs):
pass
def addWidget(self, *args, **kwargs):
pass
def addLayout(self, *args, **kwargs):
pass
def addStretch(self, *args, **kwargs):
pass
def setLayout(self, *args, **kwargs):
pass
class _MockLabel:
def __init__(self, text: str = ""):
self._text = text
class _MockLineEdit:
def __init__(self, *args, **kwargs):
self._text = ""
def text(self) -> str:
return self._text
def setText(self, value: str) -> None:
self._text = value
class _MockButton:
def __init__(self, *args, **kwargs):
# einfache Attr für Kompatibilität mit Qt-Signal-Syntax
self.clicked = lambda *a, **k: None
def connect(self, *args, **kwargs):
pass
QWidget = _MockWidget
QGridLayout = _MockLayout
QLabel = _MockLabel
QLineEdit = _MockLineEdit
QGroupBox = _MockWidget
QVBoxLayout = _MockLayout
QPushButton = _MockButton
# Kein echtes QCoreApplication im Mock
QCoreApplication = None
# ---------------------------------------------------------
# QGISImports
# ---------------------------------------------------------
try:
from qgis.core import (
QgsProject as _QgsProject,
QgsVectorLayer as _QgsVectorLayer,
QgsNetworkAccessManager as _QgsNetworkAccessManager,
Qgis as _Qgis,
)
from qgis.utils import iface as _iface
QgsProject = _QgsProject
QgsVectorLayer = _QgsVectorLayer
QgsNetworkAccessManager = _QgsNetworkAccessManager
Qgis = _Qgis
iface = _iface
QGIS_AVAILABLE = True
except Exception:
QGIS_AVAILABLE = False
class _MockQgsProject:
@staticmethod
def instance() -> "_MockQgsProject":
return _MockQgsProject()
def __init__(self):
self._variables = {}
def read(self) -> bool:
return True
QgsProject = _MockQgsProject
class _MockQgsVectorLayer:
def __init__(self, *args, **kwargs):
self._valid = True
def isValid(self) -> bool:
return self._valid
def loadNamedStyle(self, path: str):
return True, ""
def triggerRepaint(self) -> None:
pass
QgsVectorLayer = _MockQgsVectorLayer
class _MockQgsNetworkAccessManager:
def head(self, request: Any) -> _MockQNetworkReply:
return _MockQNetworkReply()
QgsNetworkAccessManager = _MockQgsNetworkAccessManager
class _MockQgis:
class MessageLevel:
Success = 0
Info = 1
Warning = 2
Critical = 3
Qgis = _MockQgis
class FakeIface:
class FakeMessageBar:
def pushMessage(self, title, text, level=0, duration=5):
return {"title": title, "text": text, "level": level, "duration": duration}
def messageBar(self):
return self.FakeMessageBar()
def mainWindow(self):
return None
iface = FakeIface()
# ---------------------------------------------------------
# MessageFunktionen
# ---------------------------------------------------------
def _get_message_bar():
if iface is not None:
bar_attr = getattr_safe(iface, "messageBar")
if callable(bar_attr):
try:
return bar_attr()
except Exception:
pass
class _MockMessageBar:
def pushMessage(self, title, text, level=0, duration=5):
return {
"title": title,
"text": text,
"level": level,
"duration": duration,
}
return _MockMessageBar()
def push_message(level, title, text, duration=5, parent=None):
bar = _get_message_bar()
push = getattr_safe(bar, "pushMessage")
if callable(push):
return push(title, text, level=level, duration=duration)
return None
def info(title, text, duration=5):
level = Qgis.MessageLevel.Info if Qgis is not None else 1
return push_message(level, title, text, duration)
def warning(title, text, duration=5):
level = Qgis.MessageLevel.Warning if Qgis is not None else 2
return push_message(level, title, text, duration)
def error(title, text, duration=5):
level = Qgis.MessageLevel.Critical if Qgis is not None else 3
return push_message(level, title, text, duration)
def success(title, text, duration=5):
level = Qgis.MessageLevel.Success if Qgis is not None else 0
return push_message(level, title, text, duration)
# ---------------------------------------------------------
# DialogInteraktionen
# ---------------------------------------------------------
def ask_yes_no(
title: str,
message: str,
default: bool = False,
parent: Any = None,
) -> bool:
"""
Fragt den Benutzer eine Ja/NeinFrage.
- In QGIS/Qt: zeigt einen QMessageBoxDialog
- Im Mock/TestModus: gibt default zurück
"""
if QMessageBox is None:
return default
try:
buttons = YES | NO
result = QMessageBox.question(
parent,
title,
message,
buttons,
YES if default else NO,
)
return result == YES
except Exception:
return default
# ---------------------------------------------------------
# VariablenWrapper
# ---------------------------------------------------------
try:
from qgis.core import QgsExpressionContextUtils
_HAS_QGIS_VARIABLES = True
except Exception:
_HAS_QGIS_VARIABLES = False
class _MockVariableStore:
global_vars: dict[str, str] = {}
project_vars: dict[str, str] = {}
class QgsExpressionContextUtils:
@staticmethod
def setGlobalVariable(name: str, value: str) -> None:
_MockVariableStore.global_vars[name] = value
@staticmethod
def globalScope():
class _Scope:
def variable(self, name: str) -> str:
return _MockVariableStore.global_vars.get(name, "")
return _Scope()
@staticmethod
def setProjectVariable(project: Any, name: str, value: str) -> None:
_MockVariableStore.project_vars[name] = value
@staticmethod
def projectScope(project: Any):
class _Scope:
def variable(self, name: str) -> str:
return _MockVariableStore.project_vars.get(name, "")
return _Scope()
def get_variable(key: str, scope: str = "project") -> str:
var_name = f"sn_{key}"
if scope == "project":
if QgsProject is not None:
projekt = QgsProject.instance()
else:
projekt = None # type: ignore[assignment]
return QgsExpressionContextUtils.projectScope(projekt).variable(var_name) or ""
if scope == "global":
return QgsExpressionContextUtils.globalScope().variable(var_name) or ""
raise ValueError("Scope muss 'project' oder 'global' sein.")
def set_variable(key: str, value: str, scope: str = "project") -> None:
var_name = f"sn_{key}"
if scope == "project":
if QgsProject is not None:
projekt = QgsProject.instance()
else:
projekt = None # type: ignore[assignment]
QgsExpressionContextUtils.setProjectVariable(projekt, var_name, value)
return
if scope == "global":
QgsExpressionContextUtils.setGlobalVariable(var_name, value)
return
raise ValueError("Scope muss 'project' oder 'global' sein.")
# ---------------------------------------------------------
# syswrapper LazyImport
# ---------------------------------------------------------
def _sys():
from sn_basis.functions import syswrapper
return syswrapper
# ---------------------------------------------------------
# StyleFunktion
# ---------------------------------------------------------
def apply_style(layer, style_name: str) -> bool:
if layer is None:
return False
is_valid_attr = getattr_safe(layer, "isValid")
if not callable(is_valid_attr) or not is_valid_attr():
return False
sys = _sys()
base_dir = sys.get_plugin_root()
style_path = sys.join_path(base_dir, "styles", style_name)
if not sys.file_exists(style_path):
return False
try:
ok, error_msg = layer.loadNamedStyle(style_path)
except Exception:
return False
if not ok:
return False
try:
trigger = getattr_safe(layer, "triggerRepaint")
if callable(trigger):
trigger()
except Exception:
pass
return True
# ---------------------------------------------------------
# LayerWrapper
# ---------------------------------------------------------
def layer_exists(layer) -> bool:
if layer is None:
return False
# Mock/Wrapper-Attribut
is_valid_flag = getattr_safe(layer, "is_valid")
if is_valid_flag is not None:
try:
return bool(is_valid_flag)
except Exception:
return False
try:
is_valid_attr = getattr_safe(layer, "isValid")
if callable(is_valid_attr):
return bool(is_valid_attr())
return True
except Exception:
return False
def get_layer_geometry_type(layer) -> str:
if layer is None:
return "None"
geometry_type_attr = getattr_safe(layer, "geometry_type")
if geometry_type_attr is not None:
return str(geometry_type_attr)
try:
is_spatial_attr = getattr_safe(layer, "isSpatial")
if callable(is_spatial_attr) and not is_spatial_attr():
return "None"
geometry_type_qgis = getattr_safe(layer, "geometryType")
if callable(geometry_type_qgis):
gtype = geometry_type_qgis()
if gtype == 0:
return "Point"
if gtype == 1:
return "LineString"
if gtype == 2:
return "Polygon"
return "None"
return "None"
except Exception:
return "None"
def get_layer_feature_count(layer) -> int:
if layer is None:
return 0
feature_count_attr = getattr_safe(layer, "feature_count")
if feature_count_attr is not None:
try:
return int(feature_count_attr)
except Exception:
return 0
try:
is_spatial_attr = getattr_safe(layer, "isSpatial")
if callable(is_spatial_attr) and not is_spatial_attr():
return 0
feature_count_qgis = getattr_safe(layer, "featureCount")
if callable(feature_count_qgis):
return int(feature_count_qgis())
return 0
except Exception:
return 0
def is_layer_visible(layer) -> bool:
if layer is None:
return False
visible_attr = getattr_safe(layer, "visible")
if visible_attr is not None:
try:
return bool(visible_attr)
except Exception:
return False
try:
is_visible_attr = getattr_safe(layer, "isVisible")
if callable(is_visible_attr):
return bool(is_visible_attr())
tree_layer_attr = getattr_safe(layer, "treeLayer")
if callable(tree_layer_attr):
node = tree_layer_attr()
else:
node = tree_layer_attr
if node is not None:
node_visible_attr = getattr_safe(node, "isVisible")
if callable(node_visible_attr):
return bool(node_visible_attr())
return False
except Exception:
return False
def set_layer_visible(layer, visible: bool) -> bool:
"""
Setzt die Sichtbarkeit eines Layers.
Unterstützt:
- Mock-/Wrapper-Attribute (layer.visible)
- QGIS-LayerTreeNode (treeLayer().setItemVisibilityChecked)
- Fallbacks ohne Exception-Wurf
Gibt True zurück, wenn die Sichtbarkeit gesetzt werden konnte.
"""
if layer is None:
return False
# 1⃣ Mock / Wrapper-Attribut
try:
if hasattr(layer, "visible"):
layer.visible = bool(visible)
return True
except Exception:
pass
# 2⃣ QGIS: LayerTreeNode
try:
tree_layer_attr = getattr_safe(layer, "treeLayer")
node = tree_layer_attr() if callable(tree_layer_attr) else tree_layer_attr
if node is not None:
set_visible = getattr_safe(node, "setItemVisibilityChecked")
if callable(set_visible):
set_visible(bool(visible))
return True
except Exception:
pass
# 3⃣ QGIS-Fallback: setVisible (selten, aber vorhanden)
try:
set_visible_attr = getattr_safe(layer, "setVisible")
if callable(set_visible_attr):
set_visible_attr(bool(visible))
return True
except Exception:
pass
return False
def get_layer_type(layer) -> str:
if layer is None:
return "unknown"
layer_type_attr = getattr_safe(layer, "layer_type")
if layer_type_attr is not None:
return str(layer_type_attr)
try:
is_spatial_attr = getattr_safe(layer, "isSpatial")
if callable(is_spatial_attr):
return "vector" if is_spatial_attr() else "table"
data_provider_attr = getattr_safe(layer, "dataProvider")
raster_type_attr = getattr_safe(layer, "rasterType")
if data_provider_attr is not None and raster_type_attr is not None:
return "raster"
return "unknown"
except Exception:
return "unknown"
def get_layer_crs(layer) -> str:
if layer is None:
return "None"
crs_attr_direct = getattr_safe(layer, "crs")
if crs_attr_direct is not None and not callable(crs_attr_direct):
# direkter Attributzugriff (z. B. im Mock)
return str(crs_attr_direct)
try:
crs_callable = getattr_safe(layer, "crs")
if callable(crs_callable):
crs = crs_callable()
authid_attr = getattr_safe(crs, "authid")
if callable(authid_attr):
return authid_attr() or "None"
return "None"
except Exception:
return "None"
def get_layer_fields(layer) -> list[str]:
if layer is None:
return []
# direkter Attributzugriff (Mock / Wrapper)
fields_attr_direct = getattr_safe(layer, "fields")
if fields_attr_direct is not None and not callable(fields_attr_direct):
try:
# direkter Iterable oder Mapping von Namen
if hasattr(fields_attr_direct, "__iter__") and not isinstance(
fields_attr_direct, (str, bytes)
):
return list(fields_attr_direct)
except Exception:
return []
try:
fields_callable = getattr_safe(layer, "fields")
if callable(fields_callable):
fields = fields_callable()
# QGIS: QgsFields.names()
names_attr = getattr_safe(fields, "names")
if callable(names_attr):
return list(names_attr())
# Fallback: iterierbar?
if hasattr(fields, "__iter__") and not isinstance(fields, (str, bytes)):
return list(fields)
return []
except Exception:
return []
def get_layer_source(layer) -> str:
if layer is None:
return "None"
source_attr_direct = getattr_safe(layer, "source")
if source_attr_direct is not None and not callable(source_attr_direct):
return str(source_attr_direct)
try:
source_callable = getattr_safe(layer, "source")
if callable(source_callable):
return source_callable() or "None"
return "None"
except Exception:
return "None"
def is_layer_editable(layer) -> bool:
if layer is None:
return False
editable_attr = getattr_safe(layer, "editable")
if editable_attr is not None:
try:
return bool(editable_attr)
except Exception:
return False
try:
editable_callable = getattr_safe(layer, "isEditable")
if callable(editable_callable):
return bool(editable_callable())
return False
except Exception:
return False

140
functions/qgisui_wrapper.py Normal file
View File

@@ -0,0 +1,140 @@
"""
sn_basis/functions/qgisui_wrapper.py zentrale QGIS-UI-Abstraktion
"""
from typing import Any, List
from sn_basis.functions.qt_wrapper import QDockWidget
iface: Any
QGIS_UI_AVAILABLE = False
# ---------------------------------------------------------
# iface initialisieren (QGIS oder Mock)
# ---------------------------------------------------------
try:
from qgis.utils import iface as _iface
iface = _iface
QGIS_UI_AVAILABLE = True
except Exception:
class _MockMessageBar:
def pushMessage(self, title, text, level=0, duration=5):
return {
"title": title,
"text": text,
"level": level,
"duration": duration,
}
class _MockIface:
def messageBar(self):
return _MockMessageBar()
def mainWindow(self):
return None
def addDockWidget(self, *args, **kwargs):
pass
def removeDockWidget(self, *args, **kwargs):
pass
def addToolBar(self, *args, **kwargs):
pass
def removeToolBar(self, *args, **kwargs):
pass
iface = _MockIface()
# ---------------------------------------------------------
# Main Window
# ---------------------------------------------------------
def get_main_window():
try:
return iface.mainWindow()
except Exception:
return None
# ---------------------------------------------------------
# Dock-Handling
# ---------------------------------------------------------
def add_dock_widget(area, dock: Any) -> None:
try:
iface.addDockWidget(area, dock)
except Exception:
pass
def remove_dock_widget(dock: Any) -> None:
try:
iface.removeDockWidget(dock)
except Exception:
pass
def find_dock_widgets() -> List[Any]:
main_window = get_main_window()
if not main_window:
return []
try:
return main_window.findChildren(QDockWidget)
except Exception:
return []
# ---------------------------------------------------------
# Menü-Handling
# ---------------------------------------------------------
def add_menu(menu):
main_window = iface.mainWindow()
if not main_window:
return
# Nur echte Qt-Menüs an Qt übergeben
if hasattr(menu, "menuAction"):
main_window.menuBar().addMenu(menu)
def remove_menu(menu):
main_window = iface.mainWindow()
if not main_window:
return
if hasattr(menu, "menuAction"):
main_window.menuBar().removeAction(menu.menuAction())
# ---------------------------------------------------------
# Toolbar-Handling
# ---------------------------------------------------------
def add_toolbar(toolbar: Any) -> None:
try:
iface.addToolBar(toolbar)
except Exception:
pass
def remove_toolbar(toolbar: Any) -> None:
try:
iface.removeToolBar(toolbar)
except Exception:
pass

393
functions/qt_wrapper.py Normal file
View File

@@ -0,0 +1,393 @@
"""
sn_basis/functions/qt_wrapper.py zentrale Qt-Abstraktion (PyQt5 / PyQt6 / Mock)
"""
from typing import Optional, Type, Any
# ---------------------------------------------------------
# Qt-Symbole (werden dynamisch gesetzt)
# ---------------------------------------------------------
QDockWidget: Type[Any]
QMessageBox: Type[Any]
QFileDialog: Type[Any]
QEventLoop: Type[Any]
QUrl: Type[Any]
QNetworkRequest: Type[Any]
QNetworkReply: Type[Any]
QCoreApplication: Type[Any]
QWidget: Type[Any]
QGridLayout: Type[Any]
QLabel: Type[Any]
QLineEdit: Type[Any]
QGroupBox: Type[Any]
QVBoxLayout: Type[Any]
QPushButton: Type[Any]
QAction: Type[Any]
QMenu: Type[Any]
QToolBar: Type[Any]
QActionGroup: Type[Any]
QTabWidget: type
YES: Optional[Any] = None
NO: Optional[Any] = None
CANCEL: Optional[Any] = None
ICON_QUESTION: Optional[Any] = None
QT_VERSION = 0 # 0 = Mock, 5 = PyQt5, 6 = PyQt6
def exec_dialog(dialog: Any) -> Any:
raise NotImplementedError
# ---------------------------------------------------------
# Versuch: PyQt6
# ---------------------------------------------------------
try:
from qgis.PyQt.QtWidgets import ( # type: ignore
QMessageBox as _QMessageBox,# type: ignore
QFileDialog as _QFileDialog,# type: ignore
QWidget as _QWidget,# type: ignore
QGridLayout as _QGridLayout,# type: ignore
QLabel as _QLabel,# type: ignore
QLineEdit as _QLineEdit,# type: ignore
QGroupBox as _QGroupBox,# type: ignore
QVBoxLayout as _QVBoxLayout,# type: ignore
QPushButton as _QPushButton,# type: ignore
QAction as _QAction,
QMenu as _QMenu,# type: ignore
QToolBar as _QToolBar,# type: ignore
QActionGroup as _QActionGroup,# type: ignore
QDockWidget as _QDockWidget,# type: ignore
QTabWidget as _QTabWidget,# type: ignore
)
from qgis.PyQt.QtCore import ( # type: ignore
QEventLoop as _QEventLoop,# type: ignore
QUrl as _QUrl,# type: ignore
QCoreApplication as _QCoreApplication,# type: ignore
)
from qgis.PyQt.QtNetwork import ( # type: ignore
QNetworkRequest as _QNetworkRequest,# type: ignore
QNetworkReply as _QNetworkReply,# type: ignore
)
QT_VERSION = 6
QMessageBox = _QMessageBox
QFileDialog = _QFileDialog
QEventLoop = _QEventLoop
QUrl = _QUrl
QNetworkRequest = _QNetworkRequest
QNetworkReply = _QNetworkReply
QCoreApplication = _QCoreApplication
QDockWidget = _QDockWidget
QWidget = _QWidget
QGridLayout = _QGridLayout
QLabel = _QLabel
QLineEdit = _QLineEdit
QGroupBox = _QGroupBox
QVBoxLayout = _QVBoxLayout
QPushButton = _QPushButton
QAction = _QAction
QMenu = _QMenu
QToolBar = _QToolBar
QActionGroup = _QActionGroup
QTabWidget = _QTabWidget
YES = QMessageBox.StandardButton.Yes
NO = QMessageBox.StandardButton.No
CANCEL = QMessageBox.StandardButton.Cancel
ICON_QUESTION = QMessageBox.Icon.Question
def exec_dialog(dialog: Any) -> Any:
return dialog.exec()
# ---------------------------------------------------------
# Versuch: PyQt5
# ---------------------------------------------------------
except Exception:
try:
from PyQt5.QtWidgets import (
QMessageBox as _QMessageBox,
QFileDialog as _QFileDialog,
QWidget as _QWidget,
QGridLayout as _QGridLayout,
QLabel as _QLabel,
QLineEdit as _QLineEdit,
QGroupBox as _QGroupBox,
QVBoxLayout as _QVBoxLayout,
QPushButton as _QPushButton,
QAction as _QAction,
QMenu as _QMenu,
QToolBar as _QToolBar,
QActionGroup as _QActionGroup,
QDockWidget as _QDockWidget,
QTabWidget as _QTabWidget,
)
from PyQt5.QtCore import (
QEventLoop as _QEventLoop,
QUrl as _QUrl,
QCoreApplication as _QCoreApplication,
)
from PyQt5.QtNetwork import (
QNetworkRequest as _QNetworkRequest,
QNetworkReply as _QNetworkReply,
)
QMessageBox = _QMessageBox
QFileDialog = _QFileDialog
QEventLoop = _QEventLoop
QUrl = _QUrl
QNetworkRequest = _QNetworkRequest
QNetworkReply = _QNetworkReply
QCoreApplication = _QCoreApplication
QDockWidget = _QDockWidget
QWidget = _QWidget
QGridLayout = _QGridLayout
QLabel = _QLabel
QLineEdit = _QLineEdit
QGroupBox = _QGroupBox
QVBoxLayout = _QVBoxLayout
QPushButton = _QPushButton
QAction = _QAction
QMenu = _QMenu
QToolBar = _QToolBar
QActionGroup = _QActionGroup
QTabWidget = _QTabWidget
YES = QMessageBox.Yes
NO = QMessageBox.No
CANCEL = QMessageBox.Cancel
ICON_QUESTION = QMessageBox.Question
QT_VERSION = 5
def exec_dialog(dialog: Any) -> Any:
return dialog.exec_()
# ---------------------------------------------------------
# Mock-Modus
# ---------------------------------------------------------
except Exception:
QT_VERSION = 0
class FakeEnum(int):
def __or__(self, other: "FakeEnum") -> "FakeEnum":
return FakeEnum(int(self) | int(other))
YES = FakeEnum(1)
NO = FakeEnum(2)
CANCEL = FakeEnum(4)
ICON_QUESTION = FakeEnum(8)
class _MockQMessageBox:
Yes = YES
No = NO
Cancel = CANCEL
Question = ICON_QUESTION
QMessageBox = _MockQMessageBox
class _MockQFileDialog:
@staticmethod
def getOpenFileName(*args, **kwargs):
return ("", "")
@staticmethod
def getSaveFileName(*args, **kwargs):
return ("", "")
QFileDialog = _MockQFileDialog
class _MockQEventLoop:
def exec(self) -> int:
return 0
def quit(self) -> None:
pass
QEventLoop = _MockQEventLoop
class _MockQUrl(str):
def isValid(self) -> bool:
return True
QUrl = _MockQUrl
class _MockQNetworkRequest:
def __init__(self, url: Any):
self.url = url
QNetworkRequest = _MockQNetworkRequest
class _MockQNetworkReply:
def error(self) -> int:
return 0
def errorString(self) -> str:
return ""
def readAll(self) -> bytes:
return b""
def deleteLater(self) -> None:
pass
QNetworkReply = _MockQNetworkReply
class _MockWidget:
def __init__(self, *args, **kwargs):
pass
class _MockLayout:
def addWidget(self, *args, **kwargs):
pass
def addLayout(self, *args, **kwargs):
pass
def addStretch(self, *args, **kwargs):
pass
class _MockLabel:
def __init__(self, text: str = ""):
self._text = text
class _MockLineEdit:
def __init__(self, *args, **kwargs):
self._text = ""
def text(self) -> str:
return self._text
def setText(self, value: str) -> None:
self._text = value
class _MockButton:
def __init__(self, *args, **kwargs):
self.clicked = lambda *a, **k: None
QWidget = _MockWidget
QGridLayout = _MockLayout
QLabel = _MockLabel
QLineEdit = _MockLineEdit
QGroupBox = _MockWidget
QVBoxLayout = _MockLayout
QPushButton = _MockButton
class _MockQCoreApplication:
pass
QCoreApplication = _MockQCoreApplication
class _MockQDockWidget(_MockWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._object_name = ""
def setObjectName(self, name: str) -> None:
self._object_name = name
def objectName(self) -> str:
return self._object_name
def show(self) -> None:
pass
def deleteLater(self) -> None:
pass
QDockWidget = _MockQDockWidget
class _MockAction:
def __init__(self, *args, **kwargs):
self._checked = False
self.triggered = lambda *a, **k: None
def setToolTip(self, text: str) -> None:
pass
def setCheckable(self, value: bool) -> None:
pass
def setChecked(self, value: bool) -> None:
self._checked = value
class _MockMenu:
def __init__(self, *args, **kwargs):
self._actions = []
def addAction(self, action):
self._actions.append(action)
def removeAction(self, action):
if action in self._actions:
self._actions.remove(action)
def clear(self):
self._actions.clear()
def menuAction(self):
return self
class _MockToolBar:
def __init__(self, *args, **kwargs):
self._actions = []
def setObjectName(self, name: str) -> None:
pass
def addAction(self, action):
self._actions.append(action)
def removeAction(self, action):
if action in self._actions:
self._actions.remove(action)
def clear(self):
self._actions.clear()
class _MockActionGroup:
def __init__(self, *args, **kwargs):
self._actions = []
def setExclusive(self, value: bool) -> None:
pass
def addAction(self, action):
self._actions.append(action)
QAction = _MockAction
QMenu = _MockMenu
QToolBar = _MockToolBar
QActionGroup = _MockActionGroup
def exec_dialog(dialog: Any) -> Any:
return YES
class _MockTabWidget:
def __init__(self, *args, **kwargs):
self._tabs = []
def addTab(self, widget, title: str):
self._tabs.append((widget, title))
QTabWidget = _MockTabWidget

View File

@@ -1,9 +1,9 @@
"""
sn_basis/funktions/settings_logic.py Logik zum Lesen und Schreiben der Plugin-Einstellungen
über den zentralen qgisqt_wrapper.
sn_basis/functions/settings_logic.py Logik zum Lesen und Schreiben der Plugin-Einstellungen
über den zentralen variable_wrapper.
"""
from sn_basis.functions.qgisqt_wrapper import (
from sn_basis.functions.variable_wrapper import (
get_variable,
set_variable,
)
@@ -27,17 +27,17 @@ class SettingsLogic:
"landkreise_proj",
]
def load(self) -> dict:
def load(self) -> dict[str, str]:
"""
Lädt alle Variablen aus dem Projekt.
Rückgabe: dict mit allen Werten (leere Strings, wenn nicht gesetzt).
"""
daten = {}
daten: dict[str, str] = {}
for key in self.VARIABLEN:
daten[key] = get_variable(key, scope="project")
return daten
def save(self, daten: dict):
def save(self, daten: dict[str, str]) -> None:
"""
Speichert alle übergebenen Variablen im Projekt.
daten: dict mit key → value

104
functions/sys_wrapper.py Normal file
View File

@@ -0,0 +1,104 @@
"""
sn_basis/functions/sys_wrapper.py System- und Pfad-Abstraktion
"""
from pathlib import Path
from typing import Union
import sys
_PathLike = Union[str, Path]
# ---------------------------------------------------------
# Plugin Root
# ---------------------------------------------------------
def get_plugin_root() -> Path:
"""
Liefert das Basisverzeichnis des Plugins.
"""
return Path(__file__).resolve().parents[2]
# ---------------------------------------------------------
# Pfad-Utilities
# ---------------------------------------------------------
def join_path(*parts: _PathLike) -> Path:
"""
Verbindet Pfadbestandteile OS-sicher.
"""
path = Path(parts[0])
for part in parts[1:]:
path /= part
return path
def file_exists(path: _PathLike) -> bool:
"""
Prüft, ob eine Datei existiert.
"""
try:
return Path(path).exists()
except Exception:
return False
def ensure_dir(path: _PathLike) -> Path:
"""
Stellt sicher, dass ein Verzeichnis existiert.
"""
p = Path(path)
p.mkdir(parents=True, exist_ok=True)
return p
# ---------------------------------------------------------
# Datei-IO
# ---------------------------------------------------------
def read_text(path: _PathLike, encoding: str = "utf-8") -> str:
"""
Liest eine Textdatei.
"""
try:
return Path(path).read_text(encoding=encoding)
except Exception:
return ""
def write_text(
path: _PathLike,
content: str,
encoding: str = "utf-8",
) -> bool:
"""
Schreibt eine Textdatei.
"""
try:
Path(path).write_text(content, encoding=encoding)
return True
except Exception:
return False
def add_to_sys_path(path: Union[str, Path]) -> None:
"""
Fügt einen Pfad zu sys.path hinzu, falls er noch nicht enthalten ist.
"""
p = str(path)
if p not in sys.path:
sys.path.insert(0, p)
def getattr_safe(obj, attr, default=None):
"""
Sicherer Zugriff auf ein Attribut.
Gibt das Attribut zurück, wenn es existiert,
ansonsten den Default-Wert (None, wenn nicht angegeben).
"""
try:
return getattr(obj, attr)
except Exception:
return default

View File

@@ -1,185 +0,0 @@
"""
snbasis/functions/syswrapper.py zentrale OS-/Dateisystem-Abstraktion
Robust, testfreundlich, mock-fähig.
"""
import os
import tempfile
import pathlib
import sys
# ---------------------------------------------------------
# DateisystemFunktionen
# ---------------------------------------------------------
def file_exists(path: str) -> bool:
"""Prüft, ob eine Datei existiert."""
try:
return os.path.exists(path)
except Exception:
return False
def is_file(path: str) -> bool:
"""Prüft, ob ein Pfad eine Datei ist."""
try:
return os.path.isfile(path)
except Exception:
return False
def is_dir(path: str) -> bool:
"""Prüft, ob ein Pfad ein Verzeichnis ist."""
try:
return os.path.isdir(path)
except Exception:
return False
def join_path(*parts) -> str:
"""Verbindet Pfadbestandteile OSunabhängig."""
try:
return os.path.join(*parts)
except Exception:
# Fallback: naive Verkettung
return "/".join(str(p) for p in parts)
# ---------------------------------------------------------
# Pfad und Systemfunktionen
# ---------------------------------------------------------
def get_temp_dir() -> str:
"""Gibt das temporäre Verzeichnis zurück."""
try:
return tempfile.gettempdir()
except Exception:
return "/tmp"
def get_plugin_root() -> str:
"""
Ermittelt den PluginRootPfad.
Annahme: syswrapper liegt in sn_basis/funktions/
→ also zwei Ebenen hoch.
"""
try:
here = pathlib.Path(__file__).resolve()
return str(here.parent.parent)
except Exception:
# Fallback: aktuelles Arbeitsverzeichnis
return os.getcwd()
# ---------------------------------------------------------
# DateiI/O (optional, aber nützlich)
# ---------------------------------------------------------
def read_file(path: str, mode="r"):
"""Liest eine Datei ein. Gibt None zurück, wenn Fehler auftreten."""
try:
with open(path, mode) as f:
return f.read()
except Exception:
return None
def write_file(path: str, data, mode="w"):
"""Schreibt Daten in eine Datei. Gibt True/False zurück."""
try:
with open(path, mode) as f:
f.write(data)
return True
except Exception:
return False
# ---------------------------------------------------------
# MockModus (optional erweiterbar)
# ---------------------------------------------------------
class FakeFileSystem:
"""
Minimaler MockDateisystemErsatz.
Wird nicht automatisch aktiviert, aber kann in Tests gepatcht werden.
"""
files = {}
@classmethod
def add_file(cls, path, content=""):
cls.files[path] = content
@classmethod
def exists(cls, path):
return path in cls.files
@classmethod
def read(cls, path):
return cls.files.get(path, None)
# ---------------------------------------------------------
# BetriebssystemErkennung
# ---------------------------------------------------------
import platform
def get_os() -> str:
"""
Gibt das Betriebssystem zurück:
- 'windows'
- 'linux'
- 'mac'
"""
system = platform.system().lower()
if "windows" in system:
return "windows"
if "darwin" in system:
return "mac"
if "linux" in system:
return "linux"
return "unknown"
def is_windows() -> bool:
return get_os() == "windows"
def is_linux() -> bool:
return get_os() == "linux"
def is_mac() -> bool:
return get_os() == "mac"
# ---------------------------------------------------------
# PfadNormalisierung
# ---------------------------------------------------------
def normalize_path(path: str) -> str:
"""
Normalisiert Pfade OSunabhängig:
- ersetzt Backslashes durch Slashes
- entfernt doppelte Slashes
- löst relative Pfade auf
"""
try:
p = pathlib.Path(path).resolve()
return str(p)
except Exception:
# Fallback: einfache Normalisierung
return path.replace("\\", "/").replace("//", "/")
def add_to_sys_path(path: str) -> None:
"""
Fügt einen Pfad sicher zum Python-Importpfad hinzu.
"""
try:
if path not in sys.path:
sys.path.insert(0, path)
except Exception:
pass

View File

@@ -0,0 +1,115 @@
"""
variable_wrapper.py QGIS-Variablen-Abstraktion
"""
from typing import Any
from sn_basis.functions.qgiscore_wrapper import QgsProject
# ---------------------------------------------------------
# Versuch: QgsExpressionContextUtils importieren
# ---------------------------------------------------------
try:
from qgis.core import QgsExpressionContextUtils
_HAS_QGIS_VARIABLES = True
# ---------------------------------------------------------
# Mock-Modus
# ---------------------------------------------------------
except Exception:
_HAS_QGIS_VARIABLES = False
class _MockVariableStore:
global_vars: dict[str, str] = {}
project_vars: dict[str, str] = {}
class QgsExpressionContextUtils:
@staticmethod
def setGlobalVariable(name: str, value: str) -> None:
_MockVariableStore.global_vars[name] = value
@staticmethod
def globalScope():
class _Scope:
def variable(self, name: str) -> str:
return _MockVariableStore.global_vars.get(name, "")
return _Scope()
@staticmethod
def setProjectVariable(project: Any, name: str, value: str) -> None:
_MockVariableStore.project_vars[name] = value
@staticmethod
def projectScope(project: Any):
class _Scope:
def variable(self, name: str) -> str:
return _MockVariableStore.project_vars.get(name, "")
return _Scope()
# ---------------------------------------------------------
# Öffentliche API
# ---------------------------------------------------------
def get_variable(key: str, scope: str = "project") -> str:
"""
Liest eine QGIS-Variable.
:param key: Variablenname ohne Prefix
:param scope: 'project' oder 'global'
"""
var_name = f"sn_{key}"
if scope == "project":
project = QgsProject.instance()
return (
QgsExpressionContextUtils
.projectScope(project)
.variable(var_name)
or ""
)
if scope == "global":
return (
QgsExpressionContextUtils
.globalScope()
.variable(var_name)
or ""
)
raise ValueError("Scope muss 'project' oder 'global' sein.")
def set_variable(key: str, value: str, scope: str = "project") -> None:
"""
Setzt eine QGIS-Variable.
:param key: Variablenname ohne Prefix
:param value: Wert
:param scope: 'project' oder 'global'
"""
var_name = f"sn_{key}"
if scope == "project":
project = QgsProject.instance()
QgsExpressionContextUtils.setProjectVariable(
project,
var_name,
value,
)
return
if scope == "global":
QgsExpressionContextUtils.setGlobalVariable(
var_name,
value,
)
return
raise ValueError("Scope muss 'project' oder 'global' sein.")

29
main.py
View File

@@ -1,13 +1,20 @@
# sn_basis/main.py
from sn_basis.functions.qgisqt_wrapper import QCoreApplication, getattr_safe
from qgis.utils import plugins
from sn_basis.functions.qt_wrapper import QCoreApplication
from sn_basis.functions.sys_wrapper import getattr_safe
from sn_basis.ui.navigation import Navigation
class BasisPlugin:
"""
Einstiegspunkt des sn_basis-Plugins.
Orchestriert UI und Fachmodule keine UI-Logik.
"""
def __init__(self, iface):
self.iface = iface
# iface wird von QGIS übergeben, aber nicht direkt verwendet
self.ui = None
# QCoreApplication kann im Mock-Modus None sein
@@ -21,10 +28,12 @@ class BasisPlugin:
connect(self.unload)
def initGui(self):
# Basis-Navigation neu aufbauen
self.ui = Navigation(self.iface)
# Alle Fachplugins mit "sn_" prüfen und neu initialisieren
"""
Initialisiert die Basis-Navigation und triggert initGui
aller abhängigen sn_-Plugins.
"""
self.ui = Navigation()
self.ui.init_ui()
for name, plugin in plugins.items():
if name.startswith("sn_") and name != "sn_basis":
try:
@@ -33,10 +42,12 @@ class BasisPlugin:
init_gui()
except Exception as e:
print(f"Fehler beim Neuinitialisieren von {name}: {e}")
self.ui.finalize_menu_and_toolbar()
def unload(self):
"""
Räumt UI-Komponenten sauber auf.
"""
if self.ui:
remove_all = getattr_safe(self.ui, "remove_all")
if callable(remove_all):
remove_all()
self.ui.remove_all()
self.ui = None

View File

@@ -1,12 +1,13 @@
"""
sn_basis/modulesdateipruefer.py Prüfung von Dateieingaben für das Plugin.
Verwendet syswrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
sn_basis/modules/Dateipruefer.py Prüfung von Dateieingaben für das Plugin.
Verwendet sys_wrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
"""
from sn_basis.functions.syswrapper import (
file_exists,
is_file,
from pathlib import Path
from sn_basis.functions import (
join_path,
file_exists,
)
from sn_basis.modules.Pruefmanager import pruef_ergebnis
@@ -32,13 +33,14 @@ class Dateipruefer:
self.standarddatei = standarddatei
self.temporaer_erlaubt = temporaer_erlaubt
# ---------------------------------------------------------
# Hilfsfunktion
# ---------------------------------------------------------
def _pfad(self, relativer_pfad: str) -> str:
"""Erzeugt einen OSunabhängigen Pfad relativ zum Basisverzeichnis."""
def _pfad(self, relativer_pfad: str) -> Path:
"""
Erzeugt einen OSunabhängigen Pfad relativ zum Basisverzeichnis.
"""
return join_path(self.basis_pfad, relativer_pfad)
# ---------------------------------------------------------
@@ -62,12 +64,12 @@ class Dateipruefer:
# -----------------------------------------------------
pfad = self._pfad(self.pfad)
if not file_exists(pfad) or not is_file(pfad):
if not file_exists(pfad):
return pruef_ergebnis(
ok=False,
meldung=f"Die Datei '{self.pfad}' wurde nicht gefunden.",
aktion="datei_nicht_gefunden",
pfad=pfad,
kontext=pfad,
)
# -----------------------------------------------------
@@ -77,7 +79,7 @@ class Dateipruefer:
ok=True,
meldung="Datei gefunden.",
aktion="ok",
pfad=pfad,
kontext=pfad,
)
# ---------------------------------------------------------
@@ -96,25 +98,31 @@ class Dateipruefer:
ok=False,
meldung="Das Dateifeld ist leer. Soll ohne Datei fortgefahren werden?",
aktion="leereingabe_erlaubt",
pfad=None,
kontext=None,
)
# 2. Standarddatei verfügbar → Nutzer fragen, ob sie verwendet werden soll
if self.standarddatei:
return pruef_ergebnis(
ok=False,
meldung=f"Es wurde keine Datei angegeben. Soll die Standarddatei '{self.standarddatei}' verwendet werden?",
meldung=(
f"Es wurde keine Datei angegeben. "
f"Soll die Standarddatei '{self.standarddatei}' verwendet werden?"
),
aktion="standarddatei_vorschlagen",
pfad=self._pfad(self.standarddatei),
kontext=self._pfad(self.standarddatei),
)
# 3. Temporäre Datei erlaubt → Nutzer fragen, ob temporär gearbeitet werden soll
if self.temporaer_erlaubt:
return pruef_ergebnis(
ok=False,
meldung="Es wurde keine Datei angegeben. Soll eine temporäre Datei erzeugt werden?",
meldung=(
"Es wurde keine Datei angegeben. "
"Soll eine temporäre Datei erzeugt werden?"
),
aktion="temporaer_erlaubt",
pfad=None,
kontext=None,
)
# 4. Leereingabe nicht erlaubt → Fehler
@@ -122,5 +130,5 @@ class Dateipruefer:
ok=False,
meldung="Es wurde keine Datei angegeben.",
aktion="leereingabe_nicht_erlaubt",
pfad=None,
kontext=None,
)

View File

@@ -1,14 +1,14 @@
"""
sn_basis/modules/pruefmanager.py zentrale Verarbeitung von pruef_ergebnis-Objekten.
Steuert die Nutzerinteraktion über qgisqt_wrapper.
sn_basis/modules/Pruefmanager.py zentrale Verarbeitung von pruef_ergebnis-Objekten.
Steuert die Nutzerinteraktion über Wrapper.
"""
from sn_basis.functions.qgisqt_wrapper import (
from sn_basis.functions import (
ask_yes_no,
info,
warning,
error,
set_layer_visible, # optional, falls implementiert
set_layer_visible,
)
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
@@ -36,6 +36,7 @@ class Pruefmanager:
return ergebnis
aktion = ergebnis.aktion
kontext = ergebnis.kontext
# -----------------------------------------------------
# Allgemeine Aktionen
@@ -47,7 +48,12 @@ class Pruefmanager:
if aktion == "leereingabe_erlaubt":
if ask_yes_no("Ohne Eingabe fortfahren", ergebnis.meldung):
return pruef_ergebnis(True, "Ohne Eingabe fortgefahren.", "ok", None)
return pruef_ergebnis(
ok=True,
meldung="Ohne Eingabe fortgefahren.",
aktion="ok",
kontext=None,
)
return ergebnis
if aktion == "leereingabe_nicht_erlaubt":
@@ -56,12 +62,22 @@ class Pruefmanager:
if aktion == "standarddatei_vorschlagen":
if ask_yes_no("Standarddatei verwenden", ergebnis.meldung):
return pruef_ergebnis(True, "Standarddatei wird verwendet.", "ok", ergebnis.pfad)
return pruef_ergebnis(
ok=True,
meldung="Standarddatei wird verwendet.",
aktion="ok",
kontext=kontext,
)
return ergebnis
if aktion == "temporaer_erlaubt":
if ask_yes_no("Temporäre Datei erzeugen", ergebnis.meldung):
return pruef_ergebnis(True, "Temporäre Datei soll erzeugt werden.", "temporaer_erzeugen", None)
return pruef_ergebnis(
ok=True,
meldung="Temporäre Datei soll erzeugt werden.",
aktion="temporaer_erzeugen",
kontext=None,
)
return ergebnis
if aktion == "datei_nicht_gefunden":
@@ -94,12 +110,18 @@ class Pruefmanager:
if aktion == "layer_unsichtbar":
if ask_yes_no("Layer einblenden", ergebnis.meldung):
# Falls set_layer_visible implementiert ist
try:
set_layer_visible(ergebnis.pfad, True)
except Exception:
pass
return pruef_ergebnis(True, "Layer wurde eingeblendet.", "ok", ergebnis.pfad)
if kontext is not None:
try:
set_layer_visible(kontext, True)
except Exception:
pass
return pruef_ergebnis(
ok=True,
meldung="Layer wurde eingeblendet.",
aktion="ok",
kontext=kontext,
)
return ergebnis
if aktion == "falscher_geotyp":

View File

@@ -1,9 +1,9 @@
"""
sn_basis/modules/layerpruefer.py Prüfung von QGIS-Layern.
Verwendet ausschließlich qgisqt_wrapper und gibt pruef_ergebnis zurück.
Verwendet ausschließlich Wrapper und gibt pruef_ergebnis zurück.
"""
from sn_basis.functions.qgisqt_wrapper import (
from sn_basis.functions import (
layer_exists,
get_layer_geometry_type,
get_layer_feature_count,
@@ -15,7 +15,7 @@ from sn_basis.functions.qgisqt_wrapper import (
is_layer_editable,
)
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
class Layerpruefer:
@@ -57,8 +57,8 @@ class Layerpruefer:
return pruef_ergebnis(
ok=False,
meldung="Der Layer existiert nicht oder wurde nicht geladen.",
aktion="layer_nicht_gefunden", # type: ignore
pfad=None,
aktion="layer_nicht_gefunden",
kontext=None,
)
# -----------------------------------------------------
@@ -69,8 +69,8 @@ class Layerpruefer:
return pruef_ergebnis(
ok=False,
meldung="Der Layer ist unsichtbar. Soll er eingeblendet werden?",
aktion="layer_unsichtbar", # type: ignore
pfad=self.layer, # Layerobjekt wird übergeben
aktion="layer_unsichtbar",
kontext=self.layer, # Layerobjekt als Kontext
)
# -----------------------------------------------------
@@ -80,9 +80,12 @@ class Layerpruefer:
if self.erwarteter_layertyp and layertyp != self.erwarteter_layertyp:
return pruef_ergebnis(
ok=False,
meldung=f"Der Layer hat den Typ '{layertyp}', erwartet wurde '{self.erwarteter_layertyp}'.",
meldung=(
f"Der Layer hat den Typ '{layertyp}', "
f"erwartet wurde '{self.erwarteter_layertyp}'."
),
aktion="falscher_layertyp",
pfad=None,
kontext=None,
)
# -----------------------------------------------------
@@ -92,9 +95,12 @@ class Layerpruefer:
if self.erwarteter_geotyp and geotyp != self.erwarteter_geotyp:
return pruef_ergebnis(
ok=False,
meldung=f"Der Layer hat den Geometrietyp '{geotyp}', erwartet wurde '{self.erwarteter_geotyp}'.",
meldung=(
f"Der Layer hat den Geometrietyp '{geotyp}', "
f"erwartet wurde '{self.erwarteter_geotyp}'."
),
aktion="falscher_geotyp",
pfad=None,
kontext=None,
)
# -----------------------------------------------------
@@ -106,7 +112,7 @@ class Layerpruefer:
ok=False,
meldung="Der Layer enthält keine Objekte.",
aktion="layer_leer",
pfad=None,
kontext=None,
)
# -----------------------------------------------------
@@ -116,9 +122,12 @@ class Layerpruefer:
if self.erwartetes_crs and crs != self.erwartetes_crs:
return pruef_ergebnis(
ok=False,
meldung=f"Der Layer hat das CRS '{crs}', erwartet wurde '{self.erwartetes_crs}'.",
meldung=(
f"Der Layer hat das CRS '{crs}', "
f"erwartet wurde '{self.erwartetes_crs}'."
),
aktion="falsches_crs",
pfad=None,
kontext=None,
)
# -----------------------------------------------------
@@ -130,9 +139,12 @@ class Layerpruefer:
if fehlende:
return pruef_ergebnis(
ok=False,
meldung=f"Der Layer enthält nicht alle erforderlichen Felder: {', '.join(fehlende)}",
meldung=(
"Der Layer enthält nicht alle erforderlichen Felder: "
+ ", ".join(fehlende)
),
aktion="felder_fehlen",
pfad=None,
kontext=None,
)
# -----------------------------------------------------
@@ -144,7 +156,7 @@ class Layerpruefer:
ok=False,
meldung=f"Die Datenquelle '{quelle}' ist nicht erlaubt.",
aktion="datenquelle_unerwartet",
pfad=None,
kontext=None,
)
# -----------------------------------------------------
@@ -156,7 +168,7 @@ class Layerpruefer:
ok=False,
meldung="Der Layer ist nicht editierbar.",
aktion="layer_nicht_editierbar",
pfad=None,
kontext=None,
)
# -----------------------------------------------------
@@ -166,5 +178,5 @@ class Layerpruefer:
ok=True,
meldung="Layerprüfung erfolgreich.",
aktion="ok",
pfad=None,
kontext=None,
)

View File

@@ -1,20 +1,17 @@
"""
sn_basis/modules/linkpruefer.py Prüfung von URLs und lokalen Links.
Verwendet syswrapper und qgisqt_wrapper.
Gibt pruef_ergebnis an den Pruefmanager zurück.
Verwendet Wrapper und gibt pruef_ergebnis an den Pruefmanager zurück.
"""
from sn_basis.functions.syswrapper import (
file_exists,
is_file,
join_path,
)
from pathlib import Path
from sn_basis.functions.qgisqt_wrapper import (
from sn_basis.functions import (
file_exists,
join_path,
network_head,
)
from sn_basis.modules.Pruefmanager import pruef_ergebnis
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis, PruefAktion
class Linkpruefer:
@@ -33,14 +30,18 @@ class Linkpruefer:
# Hilfsfunktionen
# ---------------------------------------------------------
def _pfad(self, relativer_pfad: str) -> str:
"""Erzeugt einen OS-unabhängigen Pfad relativ zum Basisverzeichnis."""
def _pfad(self, relativer_pfad: str) -> Path:
"""
Erzeugt einen OSunabhängigen Pfad relativ zum Basisverzeichnis.
"""
if not self.basis:
return relativer_pfad
return Path(relativer_pfad)
return join_path(self.basis, relativer_pfad)
def _ist_url(self, text: str) -> bool:
"""Einfache URL-Erkennung."""
"""
Einfache URL-Erkennung.
"""
return text.startswith("http://") or text.startswith("https://")
# ---------------------------------------------------------
@@ -58,7 +59,7 @@ class Linkpruefer:
ok=False,
meldung="Es wurde kein Link angegeben.",
aktion="leer",
pfad=None,
kontext=None,
)
# -----------------------------------------------------
@@ -88,7 +89,7 @@ class Linkpruefer:
ok=False,
meldung=f"Die URL '{url}' konnte nicht geprüft werden.",
aktion="netzwerkfehler",
pfad=url,
kontext=url,
)
if reply.error != 0:
@@ -96,14 +97,14 @@ class Linkpruefer:
ok=False,
meldung=f"Die URL '{url}' ist nicht erreichbar.",
aktion="url_nicht_erreichbar",
pfad=url,
kontext=url,
)
return pruef_ergebnis(
ok=True,
meldung="URL ist erreichbar.",
aktion="ok",
pfad=url,
kontext=url,
)
# ---------------------------------------------------------
@@ -122,20 +123,12 @@ class Linkpruefer:
ok=False,
meldung=f"Der Pfad '{eingabe}' wurde nicht gefunden.",
aktion="pfad_nicht_gefunden",
pfad=pfad,
)
if not is_file(pfad):
return pruef_ergebnis(
ok=False,
meldung=f"Der Pfad '{eingabe}' ist keine Datei.",
aktion="kein_dateipfad",
pfad=pfad,
kontext=pfad,
)
return pruef_ergebnis(
ok=True,
meldung="Dateipfad ist gültig.",
aktion="ok",
pfad=pfad,
kontext=pfad,
)

View File

@@ -1,10 +1,10 @@
"""
sn_basis/modules/pruef_ergebnis.py Ergebnisobjekt für alle Prüfer.
"""
from dataclasses import dataclass
from typing import Optional, Literal
from pathlib import Path
from typing import Any, Optional, Literal
# Alle möglichen Aktionen, die ein Prüfer auslösen kann.
@@ -31,28 +31,19 @@ PruefAktion = Literal[
"temporaer_erzeugen",
"stil_nicht_anwendbar",
"layer_unsichtbar",
"layer_nicht_gefunden",
"unbekannt",
"stil_anwendbar",
"falsche_endung",
]
@dataclass
@dataclass(slots=True)
class pruef_ergebnis:
"""
Reines Datenobjekt, das das Ergebnis einer Prüfung beschreibt.
ok: True → Prüfung erfolgreich
False → Nutzerinteraktion oder Fehler nötig
meldung: Text, der dem Nutzer angezeigt werden soll
aktion: Maschinenlesbarer Code, der dem Pruefmanager sagt,
wie er weiter verfahren soll
pfad: Optionaler Pfad oder URL, die geprüft wurde oder
verwendet werden soll
"""
ok: bool
meldung: str
aktion: PruefAktion
pfad: Optional[str] = None
kontext: Optional[Any] = None

View File

@@ -1,59 +1,75 @@
"""
sn_basis/modules/stilpruefer.py Prüfung und Anwendung von Layerstilen.
Verwendet ausschließlich qgisqt_wrapper und gibt pruef_ergebnis zurück.
sn_basis/modules/stilpruefer.py Prüfung von Layerstilen.
Prüft ausschließlich, ob ein Stilpfad gültig ist.
Die Anwendung erfolgt später über eine Aktion.
"""
from sn_basis.functions.qgisqt_wrapper import (
apply_style,
)
from pathlib import Path
from sn_basis.functions import file_exists
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
class Stilpruefer:
"""
Prüft, ob ein Stil auf einen Layer angewendet werden kann.
Die eigentliche Nutzerinteraktion übernimmt der Pruefmanager.
Prüft, ob ein Stilpfad gültig ist und angewendet werden kann.
Keine Seiteneffekte, keine QGIS-Aufrufe.
"""
def __init__(self, layer, stil_pfad: str):
"""
layer: QGIS-Layer oder Mock-Layer
stil_pfad: relativer oder absoluter Pfad zum .qml-Stil
"""
self.layer = layer
self.stil_pfad = stil_pfad
def __init__(self):
pass
# ---------------------------------------------------------
# Hauptfunktion
# ---------------------------------------------------------
def pruefe(self) -> pruef_ergebnis:
def pruefe(self, stil_pfad: str) -> pruef_ergebnis:
"""
Versucht, den Stil anzuwenden.
Prüft einen Stilpfad.
Rückgabe: pruef_ergebnis
"""
# Wrapper übernimmt:
# - Pfadberechnung
# - Existenzprüfung
# - loadNamedStyle
# - Fehlerbehandlung
# - Mock-Modus
erfolg, meldung = apply_style(self.layer, self.stil_pfad)
if erfolg:
# -----------------------------------------------------
# 1. Kein Stil angegeben → OK
# -----------------------------------------------------
if not stil_pfad:
return pruef_ergebnis(
ok=True,
meldung=f"Stil erfolgreich angewendet: {self.stil_pfad}",
meldung="Kein Stil angegeben.",
aktion="ok",
pfad=self.stil_pfad,
kontext=None,
)
# Fehlerfall → Nutzerinteraktion nötig
pfad = Path(stil_pfad)
# -----------------------------------------------------
# 2. Datei existiert nicht
# -----------------------------------------------------
if not file_exists(pfad):
return pruef_ergebnis(
ok=False,
meldung=f"Die Stil-Datei '{stil_pfad}' wurde nicht gefunden.",
aktion="datei_nicht_gefunden",
kontext=pfad,
)
# -----------------------------------------------------
# 3. Falsche Endung
# -----------------------------------------------------
if pfad.suffix.lower() != ".qml":
return pruef_ergebnis(
ok=False,
meldung="Die Stil-Datei muss die Endung '.qml' haben.",
aktion="falsche_endung",
kontext=pfad,
)
# -----------------------------------------------------
# 4. Stil ist gültig → Anwendung später
# -----------------------------------------------------
return pruef_ergebnis(
ok=False,
meldung=meldung,
aktion="stil_nicht_anwendbar",
pfad=self.stil_pfad,
ok=True,
meldung="Stil-Datei ist gültig.",
aktion="stil_anwendbar",
kontext=pfad,
)

View File

@@ -11,18 +11,22 @@ import inspect
import os
import sys
from pathlib import Path
# ---------------------------------------------------------
# Pre-Bootstrap: Plugin-Root in sys.path eintragen
# ---------------------------------------------------------
THIS_FILE = Path(__file__).resolve()
PLUGIN_ROOT = THIS_FILE.parents[2]
# Minimaler Bootstrap, um sn_basis importierbar zu machen
TEST_DIR = os.path.dirname(__file__)
PLUGIN_ROOT = os.path.abspath(os.path.join(TEST_DIR, "..", ".."))
if str(PLUGIN_ROOT) not in sys.path:
sys.path.insert(0, str(PLUGIN_ROOT))
if PLUGIN_ROOT not in sys.path:
sys.path.insert(0, PLUGIN_ROOT)
from sn_basis.functions import syswrapper
from sn_basis.functions import (
get_plugin_root,
add_to_sys_path,
)
# ---------------------------------------------------------
# Bootstrap: Plugin-Root in sys.path eintragen
@@ -33,13 +37,12 @@ def bootstrap():
Simuliert das QGIS-Plugin-Startverhalten:
stellt sicher, dass sn_basis importierbar ist.
"""
plugin_root = syswrapper.get_plugin_root()
syswrapper.add_to_sys_path(plugin_root)
plugin_root = get_plugin_root()
add_to_sys_path(plugin_root)
bootstrap()
# ---------------------------------------------------------
# Farben
# ---------------------------------------------------------
@@ -53,12 +56,14 @@ RESET = "\033[0m"
GLOBAL_TEST_COUNTER = 0
# ---------------------------------------------------------
# Farbige TestResult-Klasse
# ---------------------------------------------------------
class ColoredTestResult(unittest.TextTestResult):
_last_test_class: type | None = None
def startTest(self, test):
global GLOBAL_TEST_COUNTER
@@ -93,16 +98,19 @@ class ColoredTestResult(unittest.TextTestResult):
super().addSuccess(test)
self.stream.write(f"{GREEN}OK{RESET}\n")
# ---------------------------------------------------------
# Farbiger TestRunner
# ---------------------------------------------------------
class ColoredTestRunner(unittest.TextTestRunner):
resultclass = ColoredTestResult
def _makeResult(self):
result = super()._makeResult()
result = ColoredTestResult(
self.stream,
self.descriptions,
self.verbosity,
)
original_start_test = result.startTest
def patched_start_test(test):
@@ -127,7 +135,7 @@ def main():
f"{datetime.datetime.now():%Y-%m-%d %H:%M:%S}{RESET}"
)
print("=" * 70 + "\n")
loader = unittest.TestLoader()
suite = loader.discover(

View File

@@ -1,2 +1,2 @@
from sn_basis.functions import syswrapper
syswrapper.add_to_sys_path(syswrapper.get_plugin_root())
from sn_basis.functions import sys_wrapper
sys_wrapper.add_to_sys_path(sys_wrapper.get_plugin_root())

View File

@@ -1,12 +1,10 @@
# sn_basis/test/test_dateipruefer.py
import unittest
from pathlib import Path
from unittest.mock import patch
from sn_basis.modules.Dateipruefer import Dateipruefer
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
class TestDateipruefer(unittest.TestCase):
@@ -24,6 +22,7 @@ class TestDateipruefer(unittest.TestCase):
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "leereingabe_erlaubt")
self.assertIsNone(result.kontext)
# -----------------------------------------------------
# 2. Leere Eingabe nicht erlaubt
@@ -38,6 +37,7 @@ class TestDateipruefer(unittest.TestCase):
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "leereingabe_nicht_erlaubt")
self.assertIsNone(result.kontext)
# -----------------------------------------------------
# 3. Standarddatei vorschlagen
@@ -52,7 +52,7 @@ class TestDateipruefer(unittest.TestCase):
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "standarddatei_vorschlagen")
self.assertEqual(result.pfad, "/tmp/std.txt")
self.assertEqual(result.kontext, Path("/tmp/std.txt"))
# -----------------------------------------------------
# 4. Temporäre Datei erlaubt
@@ -67,11 +67,12 @@ class TestDateipruefer(unittest.TestCase):
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "temporaer_erlaubt")
self.assertIsNone(result.kontext)
# -----------------------------------------------------
# 5. Datei existiert nicht
# -----------------------------------------------------
@patch("sn_basis.functions.syswrapper.file_exists", return_value=False)
@patch("sn_basis.modules.Dateipruefer.file_exists", return_value=False)
def test_datei_nicht_gefunden(self, mock_exists):
pruefer = Dateipruefer(
pfad="/tmp/nichtvorhanden.txt"
@@ -81,13 +82,13 @@ class TestDateipruefer(unittest.TestCase):
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "datei_nicht_gefunden")
self.assertEqual(result.kontext, Path("/tmp/nichtvorhanden.txt"))
# -----------------------------------------------------
# 6. Datei existiert
# -----------------------------------------------------
@patch("sn_basis.functions.syswrapper.file_exists", return_value=True)
@patch("sn_basis.functions.syswrapper.is_file", return_value=True)
def test_datei_ok(self, mock_isfile, mock_exists):
@patch("sn_basis.modules.Dateipruefer.file_exists", return_value=True)
def test_datei_ok(self, mock_exists):
pruefer = Dateipruefer(
pfad="/tmp/test.txt"
)
@@ -96,7 +97,7 @@ class TestDateipruefer(unittest.TestCase):
self.assertTrue(result.ok)
self.assertEqual(result.aktion, "ok")
self.assertEqual(result.pfad, "/tmp/test.txt")
self.assertEqual(result.kontext, Path("/tmp/test.txt"))
if __name__ == "__main__":

View File

@@ -78,18 +78,19 @@ def mock_is_layer_editable(layer):
class TestLayerpruefer(unittest.TestCase):
def setUp(self):
# Monkeypatching der Wrapper-Funktionen
import sn_basis.functions.qgisqt_wrapper as wrapper
# Monkeypatching der im Layerpruefer verwendeten Wrapper-Funktionen
import sn_basis.modules.layerpruefer as module
module.layer_exists = mock_layer_exists
module.is_layer_visible = mock_is_layer_visible
module.get_layer_type = mock_get_layer_type
module.get_layer_geometry_type = mock_get_layer_geometry_type
module.get_layer_feature_count = mock_get_layer_feature_count
module.get_layer_crs = mock_get_layer_crs
module.get_layer_fields = mock_get_layer_fields
module.get_layer_source = mock_get_layer_source
module.is_layer_editable = mock_is_layer_editable
wrapper.layer_exists = mock_layer_exists
wrapper.is_layer_visible = mock_is_layer_visible
wrapper.get_layer_type = mock_get_layer_type
wrapper.get_layer_geometry_type = mock_get_layer_geometry_type
wrapper.get_layer_feature_count = mock_get_layer_feature_count
wrapper.get_layer_crs = mock_get_layer_crs
wrapper.get_layer_fields = mock_get_layer_fields
wrapper.get_layer_source = mock_get_layer_source
wrapper.is_layer_editable = mock_is_layer_editable
# -----------------------------------------------------
# Tests

View File

@@ -1,107 +1,78 @@
# sn_basis/test/test_linkpruefer.py
import unittest
from pathlib import Path
from unittest.mock import patch
from sn_basis.modules.linkpruefer import Linkpruefer
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
from sn_basis.functions.qgiscore_wrapper import NetworkReply
# ---------------------------------------------------------
# Mock-Ergebnisse für network_head()
# ---------------------------------------------------------
class MockResponseOK:
ok = True
status = 200
error = None
class MockResponseNotFound:
ok = False
status = 404
error = "Not Found"
class MockResponseConnectionError:
ok = False
status = None
error = "Connection refused"
# ---------------------------------------------------------
# Testklasse
# ---------------------------------------------------------
class TestLinkpruefer(unittest.TestCase):
# -----------------------------------------------------
# 1. Remote-Link erreichbar
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.network_head")
@patch("sn_basis.modules.linkpruefer.network_head")
def test_remote_link_ok(self, mock_head):
mock_head.return_value = MockResponseOK()
mock_head.return_value = NetworkReply(error=0)
lp = Linkpruefer("http://example.com", "REST")
result = lp.pruefe()
lp = Linkpruefer()
result = lp.pruefe("http://example.com")
self.assertTrue(result.ok)
self.assertEqual(result.aktion, "ok")
self.assertEqual(result.kontext, "http://example.com")
# -----------------------------------------------------
# 2. Remote-Link nicht erreichbar
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.network_head")
@patch("sn_basis.modules.linkpruefer.network_head")
def test_remote_link_error(self, mock_head):
mock_head.return_value = MockResponseConnectionError()
mock_head.return_value = NetworkReply(error=1)
lp = Linkpruefer("http://example.com", "REST")
result = lp.pruefe()
lp = Linkpruefer()
result = lp.pruefe("http://example.com")
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "url_nicht_erreichbar")
self.assertIn("Connection refused", result.meldung)
self.assertEqual(result.kontext, "http://example.com")
# -----------------------------------------------------
# 3. Remote-Link 404
# 3. Netzwerkfehler (None)
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.network_head")
def test_remote_link_404(self, mock_head):
mock_head.return_value = MockResponseNotFound()
lp = Linkpruefer("http://example.com/missing", "REST")
result = lp.pruefe()
@patch("sn_basis.modules.linkpruefer.network_head", return_value=None)
def test_remote_link_network_error(self, mock_head):
lp = Linkpruefer()
result = lp.pruefe("http://example.com")
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "url_nicht_erreichbar")
self.assertIn("404", result.meldung)
self.assertEqual(result.aktion, "netzwerkfehler")
self.assertEqual(result.kontext, "http://example.com")
# -----------------------------------------------------
# 4. Lokaler Pfad existiert nicht
# -----------------------------------------------------
@patch("sn_basis.functions.syswrapper.file_exists")
@patch("sn_basis.modules.linkpruefer.file_exists", return_value=False)
def test_local_link_not_found(self, mock_exists):
mock_exists.return_value = False
lp = Linkpruefer("/path/to/missing/file.shp", "OGR")
result = lp.pruefe()
lp = Linkpruefer()
result = lp.pruefe("/path/to/missing/file.shp")
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "pfad_nicht_gefunden")
self.assertEqual(result.kontext, Path("/path/to/missing/file.shp"))
# -----------------------------------------------------
# 5. Lokaler Pfad existiert, aber ungewöhnlich
# 5. Lokaler Pfad existiert
# -----------------------------------------------------
@patch("sn_basis.functions.syswrapper.file_exists")
def test_local_link_warning(self, mock_exists):
mock_exists.return_value = True
lp = Linkpruefer("/path/to/file_without_extension", "OGR")
result = lp.pruefe()
@patch("sn_basis.modules.linkpruefer.file_exists", return_value=True)
def test_local_link_ok(self, mock_exists):
lp = Linkpruefer()
result = lp.pruefe("/path/to/file.shp")
self.assertTrue(result.ok)
self.assertEqual(result.aktion, "ok")
self.assertIn("ungewöhnlich", result.meldung)
self.assertEqual(result.kontext, Path("/path/to/file.shp"))
if __name__ == "__main__":

View File

@@ -25,7 +25,7 @@ class TestPruefmanager(unittest.TestCase):
# -----------------------------------------------------
# 2. Leere Eingabe erlaubt → Nutzer sagt JA
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
def test_leereingabe_erlaubt_ja(self, mock_ask):
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
entscheidung = self.manager.verarbeite(ergebnis)
@@ -36,7 +36,7 @@ class TestPruefmanager(unittest.TestCase):
# -----------------------------------------------------
# 3. Leere Eingabe erlaubt → Nutzer sagt NEIN
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
def test_leereingabe_erlaubt_nein(self, mock_ask):
ergebnis = pruef_ergebnis(False, "Leer?", "leereingabe_erlaubt", None)
entscheidung = self.manager.verarbeite(ergebnis)
@@ -47,21 +47,33 @@ class TestPruefmanager(unittest.TestCase):
# -----------------------------------------------------
# 4. Standarddatei vorschlagen → Nutzer sagt JA
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
def test_standarddatei_vorschlagen_ja(self, mock_ask):
ergebnis = pruef_ergebnis(False, "Standarddatei verwenden?", "standarddatei_vorschlagen", "/tmp/std.txt")
ergebnis = pruef_ergebnis(
False,
"Standarddatei verwenden?",
"standarddatei_vorschlagen",
"/tmp/std.txt",
)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertTrue(entscheidung.ok)
self.assertEqual(entscheidung.aktion, "ok")
self.assertEqual(entscheidung.pfad, "/tmp/std.txt")
self.assertEqual(entscheidung.kontext, "/tmp/std.txt")
# -----------------------------------------------------
# 5. Standarddatei vorschlagen → Nutzer sagt NEIN
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
def test_standarddatei_vorschlagen_nein(self, mock_ask):
ergebnis = pruef_ergebnis(False, "Standarddatei verwenden?", "standarddatei_vorschlagen", "/tmp/std.txt")
ergebnis = pruef_ergebnis(
False,
"Standarddatei verwenden?",
"standarddatei_vorschlagen",
"/tmp/std.txt",
)
entscheidung = self.manager.verarbeite(ergebnis)
self.assertFalse(entscheidung.ok)
@@ -70,7 +82,7 @@ class TestPruefmanager(unittest.TestCase):
# -----------------------------------------------------
# 6. Temporäre Datei erzeugen → Nutzer sagt JA
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
def test_temporaer_erlaubt_ja(self, mock_ask):
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
entscheidung = self.manager.verarbeite(ergebnis)
@@ -81,7 +93,7 @@ class TestPruefmanager(unittest.TestCase):
# -----------------------------------------------------
# 7. Temporäre Datei erzeugen → Nutzer sagt NEIN
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
def test_temporaer_erlaubt_nein(self, mock_ask):
ergebnis = pruef_ergebnis(False, "Temporär?", "temporaer_erlaubt", None)
entscheidung = self.manager.verarbeite(ergebnis)
@@ -92,8 +104,8 @@ class TestPruefmanager(unittest.TestCase):
# -----------------------------------------------------
# 8. Layer unsichtbar → Nutzer sagt JA
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=True)
@patch("sn_basis.functions.qgisqt_wrapper.set_layer_visible")
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=True)
@patch("sn_basis.modules.Pruefmanager.set_layer_visible")
def test_layer_unsichtbar_ja(self, mock_set, mock_ask):
fake_layer = object()
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
@@ -107,7 +119,7 @@ class TestPruefmanager(unittest.TestCase):
# -----------------------------------------------------
# 9. Layer unsichtbar → Nutzer sagt NEIN
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.ask_yes_no", return_value=False)
@patch("sn_basis.modules.Pruefmanager.ask_yes_no", return_value=False)
def test_layer_unsichtbar_nein(self, mock_ask):
fake_layer = object()
ergebnis = pruef_ergebnis(False, "Layer unsichtbar", "layer_unsichtbar", fake_layer)
@@ -120,7 +132,7 @@ class TestPruefmanager(unittest.TestCase):
# -----------------------------------------------------
# 10. Fehlerhafte Aktion → Fallback
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.warning")
@patch("sn_basis.modules.Pruefmanager.warning")
def test_unbekannte_aktion(self, mock_warn):
ergebnis = pruef_ergebnis(False, "???", "unbekannt", None)
entscheidung = self.manager.verarbeite(ergebnis)

View File

@@ -11,7 +11,7 @@ class TestSettingsLogic(unittest.TestCase):
# -----------------------------------------------------
# Test: load() liest alle Variablen über get_variable()
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.get_variable")
@patch("sn_basis.functions.settings_logic.get_variable")
def test_load(self, mock_get):
# Mock-Rückgabe für jede Variable
mock_get.side_effect = lambda key, scope="project": f"wert_{key}"
@@ -30,7 +30,7 @@ class TestSettingsLogic(unittest.TestCase):
# -----------------------------------------------------
# Test: save() ruft set_variable() nur für bekannte Keys auf
# -----------------------------------------------------
@patch("sn_basis.functions.qgisqt_wrapper.set_variable")
@patch("sn_basis.functions.settings_logic.set_variable")
def test_save(self, mock_set):
logic = SettingsLogic()

View File

@@ -3,10 +3,10 @@
import unittest
import tempfile
import os
from pathlib import Path
from unittest.mock import patch
from sn_basis.modules.stilpruefer import Stilpruefer
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
class TestStilpruefer(unittest.TestCase):
@@ -23,13 +23,13 @@ class TestStilpruefer(unittest.TestCase):
self.assertTrue(result.ok)
self.assertEqual(result.aktion, "ok")
self.assertIn("Kein Stil angegeben", result.meldung)
self.assertIsNone(result.kontext)
# -----------------------------------------------------
# 2. Datei existiert und ist .qml
# -----------------------------------------------------
@patch("sn_basis.functions.syswrapper.file_exists", return_value=True)
@patch("sn_basis.functions.syswrapper.is_file", return_value=True)
def test_datei_existiert_mit_qml(self, mock_isfile, mock_exists):
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=True)
def test_datei_existiert_mit_qml(self, mock_exists):
with tempfile.NamedTemporaryFile(suffix=".qml", delete=False) as tmp:
tmp_path = tmp.name
@@ -37,8 +37,8 @@ class TestStilpruefer(unittest.TestCase):
result = self.pruefer.pruefe(tmp_path)
self.assertTrue(result.ok)
self.assertEqual(result.aktion, "ok")
self.assertEqual(result.pfad, tmp_path)
self.assertEqual(result.aktion, "stil_anwendbar")
self.assertEqual(result.kontext, Path(tmp_path))
finally:
os.remove(tmp_path)
@@ -46,9 +46,8 @@ class TestStilpruefer(unittest.TestCase):
# -----------------------------------------------------
# 3. Datei existiert, aber falsche Endung
# -----------------------------------------------------
@patch("sn_basis.functions.syswrapper.file_exists", return_value=True)
@patch("sn_basis.functions.syswrapper.is_file", return_value=True)
def test_datei_existiert_falsche_endung(self, mock_isfile, mock_exists):
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=True)
def test_datei_existiert_falsche_endung(self, mock_exists):
with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp:
tmp_path = tmp.name
@@ -58,6 +57,7 @@ class TestStilpruefer(unittest.TestCase):
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "falsche_endung")
self.assertIn(".qml", result.meldung)
self.assertEqual(result.kontext, Path(tmp_path))
finally:
os.remove(tmp_path)
@@ -65,7 +65,7 @@ class TestStilpruefer(unittest.TestCase):
# -----------------------------------------------------
# 4. Datei existiert nicht
# -----------------------------------------------------
@patch("sn_basis.functions.syswrapper.file_exists", return_value=False)
@patch("sn_basis.modules.stilpruefer.file_exists", return_value=False)
def test_datei_existiert_nicht(self, mock_exists):
fake_path = "/tmp/nichtvorhanden.qml"
@@ -74,6 +74,7 @@ class TestStilpruefer(unittest.TestCase):
self.assertFalse(result.ok)
self.assertEqual(result.aktion, "datei_nicht_gefunden")
self.assertIn("nicht gefunden", result.meldung)
self.assertEqual(result.kontext, Path(fake_path))
if __name__ == "__main__":

View File

@@ -1,164 +0,0 @@
# sn_basis/test/test_wrapper.py
import unittest
import os
import tempfile
# Wrapper importieren
import sn_basis.functions.syswrapper as syswrapper
import sn_basis.functions.qgisqt_wrapper as qgisqt
# ---------------------------------------------------------
# Mock-Layer für qgisqt_wrapper
# ---------------------------------------------------------
class MockLayer:
def __init__(
self,
exists=True,
visible=True,
layer_type="vector",
geometry_type="Polygon",
feature_count=10,
crs="EPSG:25833",
fields=None,
source="/tmp/test.shp",
editable=True,
):
self.exists = exists
self.visible = visible
self.layer_type = layer_type
self.geometry_type = geometry_type
self.feature_count = feature_count
self.crs = crs
self.fields = fields or []
self.source = source
self.editable = editable
# ---------------------------------------------------------
# Monkeypatching für qgisqt_wrapper
# ---------------------------------------------------------
def mock_layer_exists(layer):
return layer is not None and layer.exists
def mock_is_layer_visible(layer):
return layer.visible
def mock_get_layer_type(layer):
return layer.layer_type
def mock_get_layer_geometry_type(layer):
return layer.geometry_type
def mock_get_layer_feature_count(layer):
return layer.feature_count
def mock_get_layer_crs(layer):
return layer.crs
def mock_get_layer_fields(layer):
return layer.fields
def mock_get_layer_source(layer):
return layer.source
def mock_is_layer_editable(layer):
return layer.editable
# ---------------------------------------------------------
# Testklasse
# ---------------------------------------------------------
class TestWrapper(unittest.TestCase):
def setUp(self):
# qgisqt_wrapper monkeypatchen
qgisqt.layer_exists = mock_layer_exists
qgisqt.is_layer_visible = mock_is_layer_visible
qgisqt.get_layer_type = mock_get_layer_type
qgisqt.get_layer_geometry_type = mock_get_layer_geometry_type
qgisqt.get_layer_feature_count = mock_get_layer_feature_count
qgisqt.get_layer_crs = mock_get_layer_crs
qgisqt.get_layer_fields = mock_get_layer_fields
qgisqt.get_layer_source = mock_get_layer_source
qgisqt.is_layer_editable = mock_is_layer_editable
# -----------------------------------------------------
# syswrapper Tests
# -----------------------------------------------------
def test_syswrapper_file_exists(self):
with tempfile.NamedTemporaryFile(delete=True) as tmp:
self.assertTrue(syswrapper.file_exists(tmp.name))
self.assertFalse(syswrapper.file_exists("/path/does/not/exist"))
def test_syswrapper_is_file(self):
with tempfile.NamedTemporaryFile(delete=True) as tmp:
self.assertTrue(syswrapper.is_file(tmp.name))
self.assertFalse(syswrapper.is_file("/path/does/not/exist"))
def test_syswrapper_join_path(self):
result = syswrapper.join_path("/tmp", "test.txt")
self.assertEqual(result, "/tmp/test.txt")
# -----------------------------------------------------
# qgisqt_wrapper Tests (Mock-Modus)
# -----------------------------------------------------
def test_qgisqt_layer_exists(self):
layer = MockLayer(exists=True)
self.assertTrue(qgisqt.layer_exists(layer))
layer = MockLayer(exists=False)
self.assertFalse(qgisqt.layer_exists(layer))
def test_qgisqt_layer_visible(self):
layer = MockLayer(visible=True)
self.assertTrue(qgisqt.is_layer_visible(layer))
layer = MockLayer(visible=False)
self.assertFalse(qgisqt.is_layer_visible(layer))
def test_qgisqt_layer_type(self):
layer = MockLayer(layer_type="vector")
self.assertEqual(qgisqt.get_layer_type(layer), "vector")
def test_qgisqt_geometry_type(self):
layer = MockLayer(geometry_type="Polygon")
self.assertEqual(qgisqt.get_layer_geometry_type(layer), "Polygon")
def test_qgisqt_feature_count(self):
layer = MockLayer(feature_count=12)
self.assertEqual(qgisqt.get_layer_feature_count(layer), 12)
def test_qgisqt_crs(self):
layer = MockLayer(crs="EPSG:4326")
self.assertEqual(qgisqt.get_layer_crs(layer), "EPSG:4326")
def test_qgisqt_fields(self):
layer = MockLayer(fields=["id", "name"])
self.assertEqual(qgisqt.get_layer_fields(layer), ["id", "name"])
def test_qgisqt_source(self):
layer = MockLayer(source="/tmp/test.shp")
self.assertEqual(qgisqt.get_layer_source(layer), "/tmp/test.shp")
def test_qgisqt_editable(self):
layer = MockLayer(editable=True)
self.assertTrue(qgisqt.is_layer_editable(layer))
layer = MockLayer(editable=False)
self.assertFalse(qgisqt.is_layer_editable(layer))
if __name__ == "__main__":
unittest.main()

View File

@@ -1,12 +1,17 @@
# sn_basis/ui/base_dockwidget.py
"""
sn_basis/ui/base_dockwidget.py
from qgis.PyQt.QtWidgets import QDockWidget, QTabWidget
from sn_basis.functions.qgisqt_wrapper import warning, error
Basis-Dockwidget für alle LNO-Module.
"""
from sn_basis.functions.qt_wrapper import QDockWidget, QTabWidget
from sn_basis.functions.message_wrapper import warning, error
class BaseDockWidget(QDockWidget):
"""
Basis-Dockwidget für alle LNO-Module.
- Titel wird automatisch aus base_title + subtitle erzeugt
- Tabs werden dynamisch aus der Klassenvariable 'tabs' erzeugt
- Die zugehörige Toolbar-Action wird beim Schließen zurückgesetzt
@@ -23,19 +28,15 @@ class BaseDockWidget(QDockWidget):
# Titel setzen
# -----------------------------------------------------
try:
title = self.base_title if not subtitle else f"{self.base_title} | {subtitle}"
title = (
self.base_title
if not subtitle
else f"{self.base_title} | {subtitle}"
)
self.setWindowTitle(title)
except Exception as e:
warning("Titel konnte nicht gesetzt werden", str(e))
# -----------------------------------------------------
# Dock-Features
# -----------------------------------------------------
try:
self.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetClosable)
except Exception as e:
warning("Dock-Features konnten nicht gesetzt werden", str(e))
# -----------------------------------------------------
# Tabs erzeugen
# -----------------------------------------------------
@@ -45,15 +46,25 @@ class BaseDockWidget(QDockWidget):
for tab_class in self.tabs:
try:
tab_instance = tab_class()
tab_title = getattr(tab_class, "tab_title", tab_class.__name__)
tab_title = getattr(
tab_class,
"tab_title",
tab_class.__name__,
)
tab_widget.addTab(tab_instance, tab_title)
except Exception as e:
error("Tab konnte nicht geladen werden", f"{tab_class}: {e}")
error(
"Tab konnte nicht geladen werden",
f"{tab_class}: {e}",
)
self.setWidget(tab_widget)
except Exception as e:
error("Tab-Widget konnte nicht initialisiert werden", str(e))
error(
"Tab-Widget konnte nicht initialisiert werden",
str(e),
)
# ---------------------------------------------------------
# Dock schließen
@@ -68,6 +79,9 @@ class BaseDockWidget(QDockWidget):
if self.action:
self.action.setChecked(False)
except Exception as e:
warning("Toolbar-Status konnte nicht zurückgesetzt werden", str(e))
warning(
"Toolbar-Status konnte nicht zurückgesetzt werden",
str(e),
)
super().closeEvent(event)

View File

@@ -1,53 +1,69 @@
# sn_basis/ui/dockmanager.py
"""
sn_basis/ui/dockmanager.py
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtWidgets import QDockWidget
from qgis.utils import iface
Verwaltet das Anzeigen und Ersetzen von DockWidgets.
Stellt sicher, dass immer nur ein sn_basis-Dock gleichzeitig sichtbar ist.
"""
from sn_basis.functions.qgisqt_wrapper import warning, error
from typing import Any
from sn_basis.functions import (
add_dock_widget,
remove_dock_widget,
find_dock_widgets,
warning,
error,
)
class DockManager:
"""
Verwaltet das Anzeigen und Ersetzen von DockWidgets.
Stellt sicher, dass immer nur ein LNO-Dock gleichzeitig sichtbar ist.
"""
default_area = Qt.DockWidgetArea.RightDockWidgetArea
dock_prefix = "sn_dock_"
@classmethod
def show(cls, dock_widget, area=None):
def show(cls, dock_widget: Any, area=None) -> None:
"""
Zeigt ein DockWidget an und entfernt vorher alle anderen
LNO-Docks (erkennbar am Prefix 'sn_dock_').
sn_basis-Docks (erkennbar am Prefix 'sn_dock_').
"""
if dock_widget is None:
error("Dock konnte nicht angezeigt werden", "Dock-Widget ist None.")
return
try:
area = area or cls.default_area
# Prüfen, ob das Dock einen gültigen Namen hat
# Sicherstellen, dass das Dock einen Namen hat
if not dock_widget.objectName():
dock_widget.setObjectName(f"{cls.dock_prefix}{id(dock_widget)}")
# Bestehende Plugin-Docks schließen
# Vorhandene Plugin-Docks entfernen
try:
for widget in iface.mainWindow().findChildren(QDockWidget):
if widget is not dock_widget and widget.objectName().startswith(cls.dock_prefix):
iface.removeDockWidget(widget)
for widget in find_dock_widgets():
if (
widget is not dock_widget
and widget.objectName().startswith(cls.dock_prefix)
):
remove_dock_widget(widget)
widget.deleteLater()
except Exception as e:
warning("Vorherige Docks konnten nicht entfernt werden", str(e))
warning(
"Vorherige Docks konnten nicht entfernt werden",
str(e),
)
# Neues Dock anzeigen
try:
iface.addDockWidget(area, dock_widget)
add_dock_widget(area, dock_widget)
dock_widget.show()
except Exception as e:
error("Dock konnte nicht angezeigt werden", str(e))
error(
"Dock konnte nicht angezeigt werden",
str(e),
)
except Exception as e:
error("DockManager-Fehler", str(e))

View File

@@ -1,84 +1,115 @@
#sn_basis/ui/navigation.py
from qgis.PyQt.QtWidgets import QAction, QMenu, QToolBar, QActionGroup
"""
sn_basis/ui/navigation.py
Zentrale Navigation (Menü + Toolbar) für sn_basis.
"""
from typing import Any, List, Tuple
from sn_basis.functions.qt_wrapper import (
QAction,
QMenu,
QToolBar,
QActionGroup,
)
from sn_basis.functions import (
get_main_window,
add_toolbar,
remove_toolbar,
add_menu,
remove_menu,
)
class Navigation:
def __init__(self, iface):
self.iface = iface
def __init__(self):
self.actions = []
# Menü und Toolbar einmalig anlegen
self.menu = QMenu("LNO Sachsen", iface.mainWindow())
iface.mainWindow().menuBar().addMenu(self.menu)
self.menu = None
self.toolbar = None
self.plugin_group = None
self.toolbar = QToolBar("LNO Sachsen")
def init_ui(self):
print(">>> Navigation.init_ui() CALLED")
main_window = get_main_window()
if not main_window:
return
self.menu = QMenu("LNO Sachsen", main_window)
add_menu(self.menu)
self.toolbar = QToolBar("LNO Sachsen", main_window)
self.toolbar.setObjectName("LnoSachsenToolbar")
iface.addToolBar(self.toolbar)
add_toolbar(self.toolbar)
# Gruppe für exklusive Auswahl (nur ein Plugin aktiv)
self.plugin_group = QActionGroup(iface.mainWindow())
self.plugin_group.setExclusive(True)
test_action = QAction("TEST ACTION", main_window)
self.menu.addAction(test_action)
self.toolbar.addAction(test_action)
# -----------------------------------------------------
# Actions
# -----------------------------------------------------
def add_action(self, text, callback, tooltip="", priority=100):
action = QAction(text, self.iface.mainWindow())
if not self.plugin_group:
return None
action = QAction(text, get_main_window())
action.setToolTip(tooltip)
action.setCheckable(True) # Button kann aktiv sein
action.setCheckable(True)
action.triggered.connect(callback)
# Action in Gruppe aufnehmen
self.plugin_group.addAction(action)
# Action mit Priority speichern
self.actions.append((priority, action))
return action
def finalize_menu_and_toolbar(self):
# Sortieren nach Priority
if not self.menu or not self.toolbar:
return
self.actions.sort(key=lambda x: x[0])
# Menüeinträge
self.menu.clear()
self.toolbar.clear()
for _, action in self.actions:
self.menu.addAction(action)
# Toolbar-Einträge
self.toolbar.clear()
for _, action in self.actions:
self.toolbar.addAction(action)
def set_active_plugin(self, active_action):
# Alle zurücksetzen, dann aktives Plugin markieren
for _, action in self.actions:
action.setChecked(False)
if active_action:
active_action.setChecked(True)
def remove_all(self):
"""Alles entfernen beim Entladen des Basisplugins"""
# Menü entfernen
if self.menu:
self.iface.mainWindow().menuBar().removeAction(self.menu.menuAction())
self.menu = None
# Toolbar entfernen
if self.toolbar:
self.iface.mainWindow().removeToolBar(self.toolbar)
self.toolbar = None
# Actions zurücksetzen
self.actions.clear()
# Gruppe leeren
self.plugin_group = None
# -----------------------------------------------------
# Cleanup
# -----------------------------------------------------
def remove_action(self, action):
"""Entfernt eine einzelne Action aus Menü und Toolbar"""
if not action:
return
# Menüeintrag entfernen
if self.menu:
self.menu.removeAction(action)
# Toolbar-Eintrag entfernen
if self.toolbar:
self.toolbar.removeAction(action)
# Aus der internen Liste löschen
self.actions = [(p, a) for p, a in self.actions if a != action]
def remove_all(self):
if self.menu:
remove_menu(self.menu)
self.menu = None
if self.toolbar:
remove_toolbar(self.toolbar)
self.toolbar = None
self.actions.clear()
self.plugin_group = None

View File

@@ -1,129 +1,87 @@
# sn_basis/ui/tabs/settings_tab.py
"""
sn_basis/ui/base_dockwidget.py
from sn_basis.functions.qgisqt_wrapper import (
QWidget, QGridLayout, QLabel, QLineEdit,
QGroupBox, QVBoxLayout, QPushButton,
info, warning, error
)
Basis-Dockwidget für alle LNO-Module.
"""
from sn_basis.functions.settings_logic import SettingsLogic
from sn_basis.functions.qt_wrapper import QDockWidget, QTabWidget
from sn_basis.functions.message_wrapper import warning, error
class SettingsTab(QWidget):
class BaseDockWidget(QDockWidget):
"""
Tab für benutzer- und projektspezifische Einstellungen.
Nutzt SettingsLogic für das Laden/Speichern und den Wrapper für Meldungen.
Basis-Dockwidget für alle LNO-Module.
- Titel wird automatisch aus base_title + subtitle erzeugt
- Tabs werden dynamisch aus der Klassenvariable 'tabs' erzeugt
- Die zugehörige Toolbar-Action wird beim Schließen zurückgesetzt
"""
tab_title = "Projekteigenschaften"
base_title = "LNO Sachsen"
tabs = [] # Liste von Tab-Klassen
action = None # Referenz auf die Toolbar-Action
def __init__(self, parent=None):
def __init__(self, parent=None, subtitle=""):
super().__init__(parent)
self.logic = SettingsLogic()
main_layout = QVBoxLayout()
# -----------------------------------------------------
# Definition der Felder
# Titel setzen
# -----------------------------------------------------
self.user_fields = {
"amt": "Amt:",
"behoerde": "Behörde:",
"landkreis_user": "Landkreis:",
"sachgebiet": "Sachgebiet:"
}
self.project_fields = {
"bezeichnung": "Bezeichnung:",
"verfahrensnummer": "Verfahrensnummer:",
"gemeinden": "Gemeinde(n):",
"landkreise_proj": "Landkreis(e):"
}
# -----------------------------------------------------
# Benutzer-Felder
# -----------------------------------------------------
user_group = QGroupBox("Benutzerspezifische Festlegungen")
user_layout = QGridLayout()
self.user_inputs = {}
for row, (key, label) in enumerate(self.user_fields.items()):
line_edit = QLineEdit()
self.user_inputs[key] = line_edit
user_layout.addWidget(QLabel(label), row, 0)
user_layout.addWidget(line_edit, row, 1)
user_group.setLayout(user_layout)
# -----------------------------------------------------
# Projekt-Felder
# -----------------------------------------------------
project_group = QGroupBox("Projektspezifische Festlegungen")
project_layout = QGridLayout()
self.project_inputs = {}
for row, (key, label) in enumerate(self.project_fields.items()):
line_edit = QLineEdit()
self.project_inputs[key] = line_edit
project_layout.addWidget(QLabel(label), row, 0)
project_layout.addWidget(line_edit, row, 1)
project_group.setLayout(project_layout)
# -----------------------------------------------------
# Speichern-Button
# -----------------------------------------------------
save_button = QPushButton("Speichern")
save_button.clicked.connect(self.save_data)
# -----------------------------------------------------
# Layout zusammenfügen
# -----------------------------------------------------
main_layout.addWidget(user_group)
main_layout.addWidget(project_group)
main_layout.addStretch()
main_layout.addWidget(save_button)
self.setLayout(main_layout)
# Daten laden
self.load_data()
# ---------------------------------------------------------
# Speichern
# ---------------------------------------------------------
def save_data(self):
"""
Speichert alle Eingaben über SettingsLogic.
Fehler werden über den Wrapper gemeldet.
"""
try:
fields = {
key: widget.text()
for key, widget in {**self.user_inputs, **self.project_inputs}.items()
}
title = (
self.base_title
if not subtitle
else f"{self.base_title} | {subtitle}"
)
self.setWindowTitle(title)
except Exception as e:
warning("Titel konnte nicht gesetzt werden", str(e))
self.logic.save(fields)
info("Gespeichert", "Die Einstellungen wurden erfolgreich gespeichert.")
# -----------------------------------------------------
# Tabs erzeugen
# -----------------------------------------------------
try:
tab_widget = QTabWidget()
for tab_class in self.tabs:
try:
tab_instance = tab_class()
tab_title = getattr(
tab_class,
"tab_title",
tab_class.__name__,
)
tab_widget.addTab(tab_instance, tab_title)
except Exception as e:
error(
"Tab konnte nicht geladen werden",
f"{tab_class}: {e}",
)
self.setWidget(tab_widget)
except Exception as e:
error("Fehler beim Speichern", str(e))
error(
"Tab-Widget konnte nicht initialisiert werden",
str(e),
)
# ---------------------------------------------------------
# Laden
# Dock schließen
# ---------------------------------------------------------
def load_data(self):
def closeEvent(self, event):
"""
Lädt gespeicherte Einstellungen und füllt die Felder.
Fehler werden über den Wrapper gemeldet.
Wird aufgerufen, wenn das Dock geschlossen wird.
Setzt die zugehörige Toolbar-Action zurück.
"""
try:
data = self.logic.load()
for key, widget in {**self.user_inputs, **self.project_inputs}.items():
widget.setText(data.get(key, ""))
if self.action:
self.action.setChecked(False)
except Exception as e:
warning("Einstellungen konnten nicht geladen werden", str(e))
warning(
"Toolbar-Status konnte nicht zurückgesetzt werden",
str(e),
)
super().closeEvent(event)