Umlaute/Sonderzeichen in Bezeichnern zentral behandeln (#42)

Regex zur Bezeichner-Bereinigung aus Datenschreiber.py in
functions/text_wrapper.py (sanitize_bezeichner) ausgelagert und um
Bindestrich ergänzt, der bisher fehlte. Neue Tests decken Umlaute, ß,
Unterstrich, Bindestrich, eigenes Ersatzzeichen und Leerfälle ab.

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 11:09:41 +02:00
co-authored by Claude Sonnet 5
parent 7755e3e8cb
commit a754fcfe34
5 changed files with 75 additions and 2 deletions
+1
View File
@@ -1,3 +1,4 @@
- Bezeichner-Bereinigung (Layernamen) um Bindestrich ergänzt und in eine zentrale Funktion `sanitize_bezeichner` (`functions/text_wrapper.py`) ausgelagert, damit Umlaute/Sonderzeichen nicht mehr pro Aufrufstelle vergessen werden (#42)
- Landkreis-Dropdown (statt Freitext) in den Projekteigenschaften ergänzt, inkl. Landkreiskennzahl als eigene Projektvariable - Landkreis-Dropdown (statt Freitext) in den Projekteigenschaften ergänzt, inkl. Landkreiskennzahl als eigene Projektvariable
- Landkreisliste liegt jetzt als JSON (sn_basis/data/landkreise.json) statt hartcodiert vor, damit auch andere Module und Nicht-Entwickler sie pflegen können - Landkreisliste liegt jetzt als JSON (sn_basis/data/landkreise.json) statt hartcodiert vor, damit auch andere Module und Nicht-Entwickler sie pflegen können
- Auswahldialog für mehrere BauRaumOderBodenordnungsrecht-Treffer auf Mehrfachauswahl umgestellt (mehrere Objekte gemeinsam laden/verschmelzen) - Auswahldialog für mehrere BauRaumOderBodenordnungsrecht-Treffer auf Mehrfachauswahl umgestellt (mehrere Objekte gemeinsam laden/verschmelzen)
+1
View File
@@ -22,6 +22,7 @@ from .ly_metadata_wrapper import (
from .ly_style_wrapper import apply_style from .ly_style_wrapper import apply_style
from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen, ask_detailpruefung_oder_vg_laden from .dialog_wrapper import ask_yes_no, ask_overwrite_append_cancel_custom, ask_ergaenzen_entfernen_ersetzen_abbrechen, ask_detailpruefung_oder_vg_laden
from .laenderkennung import get_laenderkennung, LAENDERKENNUNGEN from .laenderkennung import get_laenderkennung, LAENDERKENNUNGEN
from .text_wrapper import sanitize_bezeichner
from .message_wrapper import ( from .message_wrapper import (
_get_message_bar, _get_message_bar,
+28
View File
@@ -0,0 +1,28 @@
"""
sn_basis/functions/text_wrapper.py Bezeichner-Hilfsfunktionen.
Zentrale Stelle, um Freitext (z. B. ein Thema oder ein Schriftfeldinhalt) in einen
technischen Bezeichner (Layername, Dateiname) umzuwandeln. Nicht pro Aufrufstelle
ein eigenes Regex schreiben Umlaute, Unterstrich und Bindestrich werden hier an
einer Stelle gepflegt (siehe docs/wissen/fallstricke.md, Issue Plugin_SN_Basis #42).
"""
import re
# Buchstaben, Ziffern, deutsche Umlaute/ß, Unterstrich und Bindestrich gelten immer
# als zulässig. Wurde in der Vergangenheit wiederholt vergessen, wenn ein neues
# Regex direkt im Aufrufer entstand statt diese Funktion zu nutzen.
_BEZEICHNER_ERLAUBT = r"A-Za-z0-9_\-ÄäÖöÜüß"
def sanitize_bezeichner(text: str, ersatz: str = "_") -> str:
"""Wandelt `text` in einen technischen Bezeichner um.
Alle Zeichen außerhalb von Buchstaben/Ziffern/Umlauten/ß/Unterstrich/Bindestrich
werden durch `ersatz` ersetzt, anschließend werden führende und folgende
Vorkommen von `ersatz` entfernt. Gedacht für Layer- und Dateinamen nicht für
Freitext in Schriftfeldern oder der Legende, wo auch andere Zeichen sichtbar
bleiben sollen.
"""
bereinigt = re.sub(rf"[^{_BEZEICHNER_ERLAUBT}]+", ersatz, text)
return bereinigt.strip(ersatz)
+2 -2
View File
@@ -30,13 +30,13 @@ from __future__ import annotations
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import os import os
import json import json
import re
import datetime import datetime
import sqlite3 import sqlite3
from sn_basis.functions import qgiscore_wrapper as qgiscore from sn_basis.functions import qgiscore_wrapper as qgiscore
from sn_basis.functions.os_wrapper import normalize_path, is_absolute_path from sn_basis.functions.os_wrapper import normalize_path, is_absolute_path
from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists from sn_basis.functions.sys_wrapper import get_plugin_root, join_path, file_exists
from sn_basis.functions.text_wrapper import sanitize_bezeichner
from sn_basis.modules.pruef_ergebnis import pruef_ergebnis from sn_basis.modules.pruef_ergebnis import pruef_ergebnis
@@ -209,7 +209,7 @@ class Datenschreiber:
continue continue
layer_name_raw = thema or str(ident) layer_name_raw = thema or str(ident)
layer_name = re.sub(r"[^A-Za-z0-9_ÄäÖöÜüß]+", "_", layer_name_raw).strip("_") layer_name = sanitize_bezeichner(layer_name_raw)
if not layer_name: if not layer_name:
layer_name = f"layer_{ident}" layer_name = f"layer_{ident}"
+43
View File
@@ -0,0 +1,43 @@
"""sn_basis/tests/test_text_wrapper.py
Unit-Tests für :mod:`sn_basis.functions.text_wrapper`.
"""
import unittest
from sn_basis.functions.text_wrapper import sanitize_bezeichner
class TestSanitizeBezeichner(unittest.TestCase):
def test_umlaute_bleiben_erhalten(self):
"""Prüft, dass Umlaute und ß nicht durch den Ersatz entfernt werden."""
self.assertEqual(sanitize_bezeichner("Grünfläche"), "Grünfläche")
self.assertEqual(sanitize_bezeichner("Straße"), "Straße")
def test_bindestrich_bleibt_erhalten(self):
"""Prüft, dass ein Bindestrich im Bezeichner erhalten bleibt (Issue #42)."""
self.assertEqual(sanitize_bezeichner("Wege-Plan"), "Wege-Plan")
def test_unterstrich_bleibt_erhalten(self):
"""Prüft, dass ein Unterstrich im Bezeichner erhalten bleibt."""
self.assertEqual(sanitize_bezeichner("thema_flur_1"), "thema_flur_1")
def test_leerzeichen_wird_durch_ersatz_zusammengefasst(self):
"""Prüft, dass Leerzeichen (kein zulässiges Zeichen) zu einem Ersatzzeichen werden."""
self.assertEqual(sanitize_bezeichner("Flurstücke Gemarkung"), "Flurstücke_Gemarkung")
def test_fuehrender_und_folgender_ersatz_wird_entfernt(self):
"""Prüft, dass ein Ersatzzeichen am Anfang oder Ende des Ergebnisses entfernt wird."""
self.assertEqual(sanitize_bezeichner("!Thema!"), "Thema")
def test_eigenes_ersatzzeichen_wird_verwendet(self):
"""Prüft, dass ein abweichendes Ersatzzeichen anstelle von Unterstrich genutzt wird."""
self.assertEqual(sanitize_bezeichner("a b", ersatz="-"), "a-b")
def test_nur_unzulaessige_zeichen_ergibt_leeren_string(self):
"""Prüft, dass ein Bezeichner ganz aus unzulässigen Zeichen zu einem leeren String wird."""
self.assertEqual(sanitize_bezeichner("!!!"), "")
if __name__ == "__main__":
unittest.main()