Files
Projektwissen/scripts/link_qgis_profile.py
T
AndreasTharangandClaude Sonnet 5 ab416fb34c Projektwissen: Git-Vorgaben, Ticketzugriff, Tests und Einstieg
Erster Stand des Repositories AG_QGIS/Projektwissen (bisher Arbeitstitel
"Workspace", ADR 0006). Der lokale Ordner heisst weiter AG_QGIS, weil darin
auch die Plugin-Klone liegen.

Git verbindlich (ADR 0007), aus dem bisherigen Wiki-Konzept entwickelt:
- docs/leitfaden/05_git_arbeitsweise.md mit Kanal-Kette, Branch-Namen und
  Gitea-Branch-Schutz.
- Der Einstiegspunkt eines Fixes richtet sich nach dem Kanal, in dem der
  Fehler sitzt; ein Fix an testing oder stable ist erst mit der
  Rueckfuehrung nach unten abgeschlossen. Das fehlte bisher.
- Der stabile Kanal heisst stable, nicht main: der Release-Workflow in
  Repository:hidden/workflows fuehrt git checkout "$CHANNEL" aus, der
  Branchname muss also dem Kanalnamen entsprechen. Er existiert in keinem
  Plugin-Repo und ist vom Maintainer anzulegen.
- Der Mensch drueckt ab: .claude/settings.json sperrt push, merge, tag.
- scripts/hallo.py und /hallo holen den Stand aller Klone, per
  Fast-Forward wo gefahrlos; nie mergen, stashen oder verwerfen.

Ticketzugriff:
- scripts/gitea.py liest Issues, Kommentare und PRs ueber die oeffentliche
  API, ohne Token, nur GET. Schreibendes nur als Entwurf in entwuerfe/.
- Skill sn-ticket.

Tests (ADR 0009):
- docs/leitfaden/06_tests.md: Testebenen, Testpflicht, und wie eine
  Fachperson beurteilt, ob Tests fachlich sinnvoll sind.
- scripts/testkatalog.py erzeugt docs/tests/testkatalog.md per ast-Parsing,
  ohne Import und ohne QGIS. Befund: nur 100 von 332 Tests haben einen
  Docstring, sn_basis keinen einzigen von 87.
- Vorlage und erste Testkonzept-Seite mit Mermaid. Dabei gefunden und gegen
  origin geprueft: _ermittle_gebietstyp_aus_fbschl hat keinen Test, obwohl
  PR #44/#45 genau diese Funktion tolerant gemacht hat.
- docs/konzept/07_qgis_tests_plan.md: Stufenplan fuer Tests gegen echtes
  QGIS. Dabei aufgefallen: QGIS 3.40 ist seit Januar 2026 End of Life,
  qgis_min steht aber noch darauf.

Werkzeuge (ADR 0008):
- 04_werkzeuge_einrichten.md nur noch Claude Code als CLI und in VS Code.
- .vscode/ entfernt, brauchbare Einstellungen als persoenliche Vorlage.

Ausserdem: README als Einstieg neu aufgebaut, Mermaid-Diagramm in
schnittstellen.md repariert (unmaskierte Klammern, rendert seither nicht).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 11:57:26 +02:00

159 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Verknüpft die Plugin-Klone des Workspace unter ihren Paketnamen im Plugin-Ordner eines QGIS-Profils.
QGIS lädt die Plugins dann direkt aus der Arbeitskopie – ohne Release, ohne Kopieren. Eine Änderung im Klon
ist nach „Plugin neu laden“ (Plugin „Plugin Reloader“) oder einem QGIS-Neustart aktiv.
Aufrufe (aus dem Workspace-Root):
python scripts/link_qgis_profile.py --profil dev # alle Plugins (plattform, fachplugin, geruest)
python scripts/link_qgis_profile.py --profil dev sn_basis sn_plan41 # nur bestimmte Pakete
python scripts/link_qgis_profile.py --pfad "D:/QGIS/profiles/dev/python/plugins" # Plugin-Ordner direkt angeben
python scripts/link_qgis_profile.py --profil dev --status # nur anzeigen
python scripts/link_qgis_profile.py --profil dev --entfernen # Verknüpfungen wieder entfernen
Empfehlung: eigenes QGIS-Profil (z. B. „dev“) verwenden, in dem die Plugins NICHT aus dem Release-Feed installiert sind.
Unter Windows werden Junctions angelegt (keine Administratorrechte nötig), sonst Symlinks.
"""
from __future__ import annotations
import argparse
import os
import sys
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ROLLEN_MIT_PLUGIN = {"plattform", "fachplugin", "geruest"}
def profil_plugin_ordner(profil: str) -> Path:
if sys.platform.startswith("win"):
basis = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
elif sys.platform == "darwin":
basis = Path.home() / "Library" / "Application Support"
else:
basis = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share"))
return basis / "QGIS" / "QGIS3" / "profiles" / profil / "python" / "plugins"
def ist_link(p: Path) -> bool:
if p.is_symlink():
return True
if os.name == "nt" and p.is_dir():
try:
return bool(os.stat(p, follow_symlinks=False).st_file_attributes & 0x400) # REPARSE_POINT (Junction)
except (AttributeError, OSError):
return False
return False
def link_anlegen(ziel: Path, quelle: Path) -> str:
try:
os.symlink(quelle, ziel, target_is_directory=True)
return "Symlink"
except (OSError, NotImplementedError):
if os.name == "nt":
import _winapi # type: ignore[attr-defined]
_winapi.CreateJunction(str(quelle), str(ziel))
return "Junction"
raise
def link_entfernen(p: Path) -> None:
if p.is_symlink():
p.unlink()
else:
os.rmdir(p) # Junction: rmdir entfernt nur den Verweis
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("pakete", nargs="*", help="Paketnamen (Standard: alle Plugins laut workspace.toml)")
ap.add_argument("--profil", default=None, help="Name des QGIS-Profils, z. B. dev")
ap.add_argument("--pfad", type=Path, default=None, help="Plugin-Ordner direkt (überschreibt --profil)")
ap.add_argument("--status", action="store_true", help="nur anzeigen, nichts ändern")
ap.add_argument("--entfernen", action="store_true", help="Verknüpfungen entfernen")
ap.add_argument("--ersetzen", action="store_true",
help="vorhandenen echten Plugin-Ordner (z. B. Release-Installation) nach <name>.vor_link umbenennen und verknüpfen")
args = ap.parse_args()
if args.pfad is None and args.profil is None:
ap.error("--profil <name> oder --pfad <ordner> angeben")
plugin_ordner = args.pfad or profil_plugin_ordner(args.profil)
with (ROOT / "workspace.toml").open("rb") as f:
manifest = tomllib.load(f)
repos = {r["paket"]: r for r in manifest["repos"] if r.get("paket") and r.get("rolle") in ROLLEN_MIT_PLUGIN}
gewuenscht = args.pakete or list(repos)
unbekannt = [p for p in gewuenscht if p not in repos]
if unbekannt:
print(f"Unbekannte oder nicht verknüpfbare Pakete: {unbekannt}. Möglich: {sorted(repos)}")
return 2
print(f"QGIS-Plugin-Ordner: {plugin_ordner}")
if not args.status and not args.entfernen:
plugin_ordner.mkdir(parents=True, exist_ok=True)
elif not plugin_ordner.exists():
print(" (existiert noch nicht – QGIS mit diesem Profil einmal starten oder --pfad prüfen)")
rc = 0
for paket in gewuenscht:
quelle = ROOT / repos[paket]["name"]
ziel = plugin_ordner / paket
if not (quelle / "__init__.py").is_file():
print(f"[{paket}] übersprungen: {quelle.name} enthält kein Plugin (__init__.py fehlt)")
continue
if args.status:
if ist_link(ziel):
print(f"[{paket}] verknüpft -> {os.path.realpath(ziel)}")
elif ziel.exists():
print(f"[{paket}] echter Ordner vorhanden (Release-Installation?) – nicht verknüpft")
else:
print(f"[{paket}] nicht vorhanden")
continue
if args.entfernen:
if ist_link(ziel):
link_entfernen(ziel)
print(f"[{paket}] Verknüpfung entfernt")
elif ziel.exists():
print(f"[{paket}] ist ein echter Ordner – nicht entfernt")
else:
print(f"[{paket}] nichts zu tun")
continue
if ist_link(ziel):
if os.path.realpath(ziel) == os.path.realpath(quelle):
print(f"[{paket}] bereits verknüpft")
continue
link_entfernen(ziel)
elif ziel.exists():
if not args.ersetzen:
print(f"[{paket}] echter Ordner vorhanden: {ziel}\n"
f" -> eigenes Profil verwenden oder mit --ersetzen nach {paket}.vor_link verschieben")
rc = 1
continue
sicherung = ziel.with_name(f"{paket}.vor_link")
if sicherung.exists():
print(f"[{paket}] {sicherung.name} existiert bereits – bitte manuell aufräumen")
rc = 1
continue
ziel.rename(sicherung)
print(f"[{paket}] vorhandenen Ordner nach {sicherung.name} verschoben")
try:
art = link_anlegen(ziel, quelle)
print(f"[{paket}] {art} angelegt -> {quelle}")
except OSError as e:
print(f"[{paket}] FEHLER: {e}")
rc = 1
if not args.status and not args.entfernen and rc == 0:
print("\nIn QGIS: Erweiterungen -> Verwalten -> Einstellungen: „Auch experimentelle Erweiterungen anzeigen“ aktivieren,\n"
"dann zuerst „LNO Sachsen | Plugin Basisfunktionen“ (sn_basis) einschalten, danach die Fachplugins.\n"
"Nach Codeänderungen: Plugin „Plugin Reloader“ (offizielles Repository) oder QGIS neu starten.")
return rc
if __name__ == "__main__":
raise SystemExit(main())