Umlaut-Pruefskript ergaenzt und in Fallstricke dokumentiert (#42)

scripts/pruefe_umlaute.py findet String-Inhalte und Kommentare mit
Leerzeichen, die einen bekannten ASCII-Wortstamm statt Umlaut/ss enthalten
(gueltig, verfuegbar, fuer, ueber, ...). Technische Bezeichner ohne
Leerzeichen (aktion-Codes, Variablen-Keys, WFS-Feldnamen) werden von der
Heuristik nicht erfasst. Aktuell ueber den gesamten Workspace grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MneDjzrjmbpQt8udFYxN9e
This commit is contained in:
2026-09-14 12:21:13 +02:00
co-authored by Claude Sonnet 5
parent f0d7efddf2
commit d8f88df0ba
2 changed files with 162 additions and 0 deletions
+10
View File
@@ -55,6 +55,16 @@ Kurz, konkret, mit Datum und Quelle. Neue Einträge oben in der passenden Rubrik
- **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
+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())