Merge pull request 'Bugfix/42 umlaute sonderzeichen-Doku Prüfskript' (#12) from bugfix/42-umlaute-sonderzeichen into main

Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
2026-09-15 06:08:28 +00:00
4 changed files with 190 additions and 6 deletions
+1
View File
@@ -37,6 +37,7 @@ Legende: ⚠ = über 600 Zeilen, nie komplett laden, per `grep -n "def "` navigi
| `functions/sys_wrapper.py` | 106 | `get_plugin_root`, `join_path`, `file_exists`, `read_text`, `add_to_sys_path`, `getattr_safe` | | `functions/sys_wrapper.py` | 106 | `get_plugin_root`, `join_path`, `file_exists`, `read_text`, `add_to_sys_path`, `getattr_safe` |
| `functions/landkreise.py` | 48 | Sächsische Landkreise aus `data/landkreise.json` (`LANDKREISE`, `ALLE_LANDKREISE`) | | `functions/landkreise.py` | 48 | Sächsische Landkreise aus `data/landkreise.json` (`LANDKREISE`, `ALLE_LANDKREISE`) |
| `functions/laenderkennung.py` | 52 | ALKIS-Länderkennung je Aufrufer-Präfix | | `functions/laenderkennung.py` | 52 | ALKIS-Länderkennung je Aufrufer-Präfix |
| `functions/text_wrapper.py` | 25 | `sanitize_bezeichner(text, ersatz="_")`: Freitext → Layer-/Dateiname, Umlaute/ß/_/- bleiben erhalten |
### modules/ – Prüfen, Laden, Schreiben, Drucken ### modules/ – Prüfen, Laden, Schreiben, Drucken
+24 -6
View File
@@ -9,17 +9,17 @@ selbst (Docstrings) und aus der Testbaseline.
Stand der Baseline: `docs/status/testbaseline.json` (SHA-256 `aeabc946a7f3`) Stand der Baseline: `docs/status/testbaseline.json` (SHA-256 `aeabc946a7f3`)
Gesamt: 3 Pakete, 30 Testdateien, 332 Testfälle – 287 grün, 37 rot (Baseline), 8 übersprungen. Gesamt: 3 Pakete, 31 Testdateien, 339 Testfälle – 294 grün, 37 rot (Baseline), 8 übersprungen.
Mit Beschreibung (Docstring): 100 von 332 (30 %). Mit Beschreibung (Docstring): 107 von 339 (32 %).
## Zusammenfassung ## Zusammenfassung
| Paket | Testdateien | Testfälle | grün | rot (Baseline) | übersprungen | mit Beschreibung | | Paket | Testdateien | Testfälle | grün | rot (Baseline) | übersprungen | mit Beschreibung |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | | --- | ---: | ---: | ---: | ---: | ---: | ---: |
| `sn_basis` | 13 | 87 | 87 | 0 | 0 | 0 von 87 (0 %) | | `sn_basis` | 14 | 94 | 94 | 0 | 0 | 7 von 94 (7 %) |
| `sn_verfahrensgebiet` | 12 | 201 | 157 | 36 | 8 | 99 von 201 (49 %) | | `sn_verfahrensgebiet` | 12 | 201 | 157 | 36 | 8 | 99 von 201 (49 %) |
| `sn_plan41` | 5 | 44 | 43 | 1 | 0 | 1 von 44 (2 %) | | `sn_plan41` | 5 | 44 | 43 | 1 | 0 | 1 von 44 (2 %) |
| **Summe** | **30** | **332** | **287** | **37** | **8** | **100 von 332 (30 %)** | | **Summe** | **31** | **339** | **294** | **37** | **8** | **107 von 339 (32 %)** |
### Legende ### Legende
@@ -31,7 +31,7 @@ Mit Beschreibung (Docstring): 100 von 332 (30 %).
## Paket `sn_basis` ## Paket `sn_basis`
Plattform-Plugin, Ordner `Plugin_SN_Basis` – 13 Testdateien mit 87 Testfällen (87 grün, 0 rot, 0 übersprungen). Plattform-Plugin, Ordner `Plugin_SN_Basis` – 14 Testdateien mit 94 Testfällen (94 grün, 0 rot, 0 übersprungen).
### `Plugin_SN_Basis/tests/test_bootstrap.py` ### `Plugin_SN_Basis/tests/test_bootstrap.py`
@@ -225,6 +225,24 @@ Testgruppen:
| `TestStilpruefer.test_datei_existiert_falsche_endung` | — (Docstring fehlt) | grün | | `TestStilpruefer.test_datei_existiert_falsche_endung` | — (Docstring fehlt) | grün |
| `TestStilpruefer.test_datei_existiert_nicht` | — (Docstring fehlt) | grün | | `TestStilpruefer.test_datei_existiert_nicht` | — (Docstring fehlt) | grün |
### `Plugin_SN_Basis/tests/test_text_wrapper.py`
Worum es in der Datei geht: Unit-Tests für `sn_basis.functions.text_wrapper`.
Testgruppen:
- `TestSanitizeBezeichner` (7 Testfälle): — (Docstring fehlt)
| Testfall | prüft | Status |
| --- | --- | --- |
| `TestSanitizeBezeichner.test_umlaute_bleiben_erhalten` | Prüft, dass Umlaute und ß nicht durch den Ersatz entfernt werden. | grün |
| `TestSanitizeBezeichner.test_bindestrich_bleibt_erhalten` | Prüft, dass ein Bindestrich im Bezeichner erhalten bleibt (Issue #42). | grün |
| `TestSanitizeBezeichner.test_unterstrich_bleibt_erhalten` | Prüft, dass ein Unterstrich im Bezeichner erhalten bleibt. | grün |
| `TestSanitizeBezeichner.test_leerzeichen_wird_durch_ersatz_zusammengefasst` | Prüft, dass Leerzeichen (kein zulässiges Zeichen) zu einem Ersatzzeichen werden. | grün |
| `TestSanitizeBezeichner.test_fuehrender_und_folgender_ersatz_wird_entfernt` | Prüft, dass ein Ersatzzeichen am Anfang oder Ende des Ergebnisses entfernt wird. | grün |
| `TestSanitizeBezeichner.test_eigenes_ersatzzeichen_wird_verwendet` | Prüft, dass ein abweichendes Ersatzzeichen anstelle von Unterstrich genutzt wird. | grün |
| `TestSanitizeBezeichner.test_nur_unzulaessige_zeichen_ergibt_leeren_string` | Prüft, dass ein Bezeichner ganz aus unzulässigen Zeichen zu einem leeren String wird. | grün |
### `Plugin_SN_Basis/tests/test_variable_wrapper.py` ### `Plugin_SN_Basis/tests/test_variable_wrapper.py`
Worum es in der Datei geht: — (Docstring fehlt) Worum es in der Datei geht: — (Docstring fehlt)
@@ -736,7 +754,7 @@ Arbeitsliste: Diese Testfälle haben keinen Docstring; was sie fachlich prüfen,
aus dem Quelltext erschließen. Jeder Eintrag braucht eine einzeilige Beschreibung im Testfall, aus dem Quelltext erschließen. Jeder Eintrag braucht eine einzeilige Beschreibung im Testfall,
dann verschwindet er hier von selbst. dann verschwindet er hier von selbst.
### `sn_basis` – 87 von 87 Testfällen ohne Beschreibung ### `sn_basis` – 87 von 94 Testfällen ohne Beschreibung
#### `Plugin_SN_Basis/tests/test_datagrabber.py` (6 von 6) – auch der Modul-Docstring fehlt #### `Plugin_SN_Basis/tests/test_datagrabber.py` (6 von 6) – auch der Modul-Docstring fehlt
+13
View File
@@ -52,6 +52,19 @@ Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik
- **`print()` ist im Code verbreitet**, aber kein Logging. Neue Ausgaben über `message_wrapper` (Nutzer) oder gar nicht; Debug-`print` vor dem PR entfernen. - **`print()` ist im Code verbreitet**, aber kein Logging. Neue Ausgaben über `message_wrapper` (Nutzer) oder gar nicht; Debug-`print` vor dem PR entfernen.
- **Testrunner pro Repo (`tests/run_tests.py`) und Workspace-Runner unterscheiden sich.** Für PR-Nachweise zählt `scripts/run_tests.py --baseline`. - **Testrunner pro Repo (`tests/run_tests.py`) und Workspace-Runner unterscheiden sich.** Für PR-Nachweise zählt `scripts/run_tests.py --baseline`.
- **Tests der Verfahrensgebiet-Suite sind teils veraltet** (erwarten Einzeldatei-/Einzellayer-Schnittstellen vor der Mehrfachauswahl). Vor dem Fixen prüfen, ob der Test oder der Code falsch ist; siehe Baseline. - **Tests der Verfahrensgebiet-Suite sind teils veraltet** (erwarten Einzeldatei-/Einzellayer-Schnittstellen vor der Mehrfachauswahl). Vor dem Fixen prüfen, ob der Test oder der Code falsch ist; siehe Baseline.
- **Bezeichner (Layernamen, Dateinamen) nie mit eigenem Regex bereinigen.** `sanitize_bezeichner` aus
`sn_basis.functions.text_wrapper` verwenden – Umlaute, `ß`, `_` und `-` wurden in mehreren KI-generierten
Regexen vergessen (Plugin_SN_Basis #42).
- **Deutscher Text ohne Umlaute/ß ist ein eigenständiges Muster, kein Einzelfall.** Bei der Aufarbeitung von
#42 fanden sich über alle Fachplugins verteilt MessageBox-Texte, Labels und Docstrings, die wortwörtlich als
ASCII generiert wurden (`gueltig` statt `gültig`, `ueber` statt `über`, `Massstab`/`Groesse` als Anzeigetext
statt `Maßstab`/`Größe`) – typisch für KI-generierten Text, der Sonderzeichen vermeidet. Neuen deutschen Text
(Meldungen, Labels, Kommentare, Docstrings) immer mit echten Umlauten/ß schreiben, nicht mit `ae`/`oe`/`ue`/`ss`
behelfen. Vor dem Commit `python scripts/pruefe_umlaute.py [<repo>]` laufen lassen – findet genau dieses
Muster (String-Inhalte und Kommentare mit Leerzeichen, die einen bekannten ASCII-Wortstamm enthalten;
technische Bezeichner ohne Leerzeichen werden nicht angetastet). Die Wortstammliste im Skript ist bewusst
konservativ (keine Fehlalarme wichtiger als Vollständigkeit) – Ersatz fürs Lesen des eigenen Textes ist sie
nicht.
## Zusammenarbeit ## Zusammenarbeit
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Findet deutschen Text in .py-Dateien, der offenbar ohne Umlaute/ß geschrieben wurde.
Hintergrund: Plugin_SN_Basis #42. KI-generierter Text weicht gelegentlich auf eine
ASCII-Ersatzschreibweise aus ("gueltig" statt "gültig", "ueber" statt "über") statt
echte Umlaute zu verwenden. Das betrifft vor allem Meldungstexte (MessageBox,
zeige_hinweis, QLabel) und Docstrings/Kommentare.
Technische Bezeichner (Variablennamen, "aktion"-Codes, WFS-Feldnamen,
Projektvariablen-Keys) sind davon ausdrücklich nicht betroffen: Sie sind bewusst
ASCII und enthalten kein Leerzeichen, während geschriebener deutscher Text so gut
wie immer eines enthält. Deshalb prüft dieses Skript nur String-Inhalte mit
mindestens einem Leerzeichen sowie Kommentartext - keine Bezeichner.
Kein Ersatz fürs Korrekturlesen: Die Liste verdächtiger Wortstämme ist bewusst kurz
gehalten (nur eindeutige Fälle), um keine Fehlalarme zu erzeugen. Neue, hier nicht
erfasste Wortstämme ergänzen.
Aufrufe (aus dem Workspace-Root):
python scripts/pruefe_umlaute.py # alle Repos aus workspace.toml mit Paketnamen
python scripts/pruefe_umlaute.py Basis # nur ein Repo (Kurzname genügt)
Exit-Code: 0 = keine Funde, 1 = mindestens ein Fund.
"""
from __future__ import annotations
import argparse
import re
import sys
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST = ROOT / "workspace.toml"
# Nur eindeutige Fälle: Wortstämme, die als ASCII-Ersatzschreibweise so gut wie nie
# etwas anderes als ein fehlender Umlaut/ß sein können. Absichtlich kein "ss" für
# "ß" (zu viele legitime Doppel-S-Wörter) und kein "ae/oe/ue" pauschal (zu viele
# Fehlalarme in technischen Bezeichnern und Fremdwörtern).
VERDAECHTIGE_STAEMME = [
r"gueltig\w*", r"ungueltig\w*", r"verfuegbar\w*", r"ausgewaehlt\w*",
r"gewaehlt\w*", r"waehlen\b", r"auswaehlen\b", r"loeschen\b", r"loescht\w*",
r"groesse\w*", r"zuruecksetzen\b", r"erlaeuter\w*", r"bestaetig\w*",
r"natuerlich\w*", r"moeglich\w*", r"zusaetzlich\w*", r"abhaengig\w*",
r"unzulaessig\w*", r"ueberpruef\w*", r"vollstaendig\w*", r"geaendert\w*",
r"aenderung\w*", r"einfuegen\b", r"ausfuehr\w*", r"benoetig\w*",
r"gewuenscht\w*", r"ueber\w*", r"fuer\b", r"hoehe\w*", r"waere\b",
]
_STAMM_PATTERN = re.compile(
r"\b(?:" + "|".join(VERDAECHTIGE_STAEMME) + r")\b", re.IGNORECASE
)
# Erlaubt genau diese exakten Fundstellen (Datei, Zeile) - für Ausnahmefälle, die
# geprüft und bewusst akzeptiert wurden. Aktuell leer: siehe Docstring, warum
# technische Bezeichner die Heuristik ohnehin nicht erreichen.
ALLOWLIST: set[tuple[str, int]] = set()
_STRING_PATTERN = re.compile(
r'"""(?P<triple_d>(?:.|\n)*?)"""'
r"|'''(?P<triple_s>(?:.|\n)*?)'''"
r'|"(?P<double>(?:[^"\\\n]|\\.)*)"'
r"|'(?P<single>(?:[^'\\\n]|\\.)*)'"
)
_KOMMENTAR_PATTERN = re.compile(r"#(?P<text>.*)$", re.MULTILINE)
def _kandidaten(quelltext: str):
"""Liefert (start_offset, text) für String-Inhalte und Kommentare mit Leerzeichen."""
for m in _STRING_PATTERN.finditer(quelltext):
text = next(g for g in m.groups() if g is not None)
if " " in text:
yield m.start(), text
for m in _KOMMENTAR_PATTERN.finditer(quelltext):
text = m.group("text")
if " " in text:
yield m.start("text"), text
def pruefe_datei(pfad: Path) -> list[tuple[int, str, str]]:
"""Liefert Funde als (Zeilennummer, Wortstamm, Kontext) für eine Datei.
Ein Kandidat (String-Inhalt oder Kommentar) kann mehrzeilig sein (Docstrings);
deshalb wird für jeden Treffer einzeln die Zeile anhand seiner eigenen Position
berechnet, nicht anhand des Kandidaten-Anfangs.
"""
quelltext = pfad.read_text(encoding="utf-8", errors="replace")
funde = []
for offset, text in _kandidaten(quelltext):
for treffer in _STAMM_PATTERN.finditer(text):
zeile = quelltext.count("\n", 0, offset + treffer.start()) + 1
zeilen_text = text.splitlines()[treffer.string.count("\n", 0, treffer.start())] \
if "\n" in text else text
kontext = zeilen_text.strip()
if len(kontext) > 90:
kontext = kontext[:87] + "..."
funde.append((zeile, treffer.group(0), kontext))
return funde
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("repos", nargs="*", help="Kurzname(n) aus workspace.toml (Standard: alle mit Paketnamen)")
args = ap.parse_args()
with MANIFEST.open("rb") as f:
manifest = tomllib.load(f)
alle_repos = [r for r in manifest["repos"] if r.get("paket")]
if args.repos:
gesucht = {r.lower() for r in args.repos}
repos = [
r for r in alle_repos
if r["name"].lower() in gesucht
or r["name"].lower().replace("plugin_sn_", "") in gesucht
or r["paket"].lower() in gesucht
]
unbekannt = gesucht - {
n for r in repos for n in (r["name"].lower(), r["name"].lower().replace("plugin_sn_", ""), r["paket"].lower())
}
if unbekannt:
print(f"Unbekannte Repo-Angabe: {', '.join(sorted(unbekannt))}", file=sys.stderr)
return 2
else:
repos = alle_repos
gesamt_funde = 0
for repo in repos:
basis = ROOT / repo["name"]
if not basis.is_dir():
print(f"[{repo['paket']}] Ordner fehlt ({basis}) - übersprungen")
continue
repo_funde = 0
for py_datei in sorted(basis.rglob("*.py")):
if any(teil in (".git", "__pycache__") for teil in py_datei.parts):
continue
relativ = py_datei.relative_to(ROOT).as_posix()
for zeile, stamm, kontext in pruefe_datei(py_datei):
if (relativ, zeile) in ALLOWLIST:
continue
print(f"{relativ}:{zeile}: '{stamm}' in: {kontext}")
repo_funde += 1
status = "GRÜN" if repo_funde == 0 else "ROT"
print(f"[{repo['paket']}] {status}: {repo_funde} Fund/e")
gesamt_funde += repo_funde
print(f"\nGesamt: {gesamt_funde} Fund/e in {len(repos)} Repo(s).")
return 1 if gesamt_funde else 0
if __name__ == "__main__":
raise SystemExit(main())