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.")