Feat: XSL-Abhängigkeitsgraph für import/include-Erkennung in Transformations-Pipeline
is_up_to_date() prüft nun auch transitiv importierte/inkludierte XSL-Dateien. Abhängigkeiten werden per Tooltip und Kontextmenü-Aktion im TreeWidget angezeigt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
XSL-Abhängigkeitsgraph für die Erkennung transitiver Imports/Includes.
|
||||
|
||||
Dieses Modul parst XSL-Dateien und baut einen Abhängigkeitsgraph auf,
|
||||
um alle direkt und transitiv importierten/inkludierten Dateien zu ermitteln.
|
||||
Der Graph wird im Speicher gehalten und bei Änderungen (mtime) automatisch invalidiert.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Regex für xsl:import und xsl:include href-Attribute
|
||||
_IMPORT_INCLUDE_PATTERN = re.compile(r'<xsl:(?:import|include)\s+href=["\']([^"\']+)["\']', re.IGNORECASE)
|
||||
|
||||
|
||||
class XslDependencyGraph:
|
||||
"""
|
||||
Verwaltet einen Cache von XSL-Abhängigkeiten (import/include).
|
||||
|
||||
Für jede XSL-Datei wird die Menge aller transitiv referenzierten
|
||||
XSL-Dateien gespeichert. Der Cache wird über die mtime der Dateien
|
||||
automatisch invalidiert.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Cache: xsl_path -> (mtime_bei_parse, set[Path] aller Abhängigkeiten)
|
||||
self._cache: dict[Path, tuple[float, set[Path]]] = {}
|
||||
|
||||
def get_dependencies(self, xsl_file: Path) -> set[Path]:
|
||||
"""
|
||||
Gibt alle transitiven Abhängigkeiten einer XSL-Datei zurück.
|
||||
|
||||
Args:
|
||||
xsl_file: Absoluter Pfad zur XSL-Datei
|
||||
|
||||
Returns:
|
||||
set[Path]: Menge aller transitiv importierten/inkludierten XSL-Dateien
|
||||
"""
|
||||
xsl_file = xsl_file.resolve()
|
||||
|
||||
if not xsl_file.exists():
|
||||
return set()
|
||||
|
||||
current_mtime = xsl_file.stat().st_mtime
|
||||
|
||||
# Cache-Hit prüfen
|
||||
if xsl_file in self._cache:
|
||||
cached_mtime, cached_deps = self._cache[xsl_file]
|
||||
if cached_mtime == current_mtime:
|
||||
return cached_deps
|
||||
|
||||
# Neu parsen
|
||||
deps = self._resolve_recursive(xsl_file, set())
|
||||
self._cache[xsl_file] = (current_mtime, deps)
|
||||
logger.debug(f"XSL-Abhängigkeiten aufgelöst für {xsl_file.name}: {len(deps)} Abhängigkeit(en)")
|
||||
return deps
|
||||
|
||||
def _resolve_recursive(self, xsl_file: Path, visited: set[Path]) -> set[Path]:
|
||||
"""
|
||||
Löst rekursiv alle import/include-Referenzen auf.
|
||||
|
||||
Args:
|
||||
xsl_file: Absoluter Pfad zur XSL-Datei
|
||||
visited: Bereits besuchte Dateien (Zykluserkennung)
|
||||
|
||||
Returns:
|
||||
set[Path]: Alle transitiven Abhängigkeiten (ohne die Datei selbst)
|
||||
"""
|
||||
xsl_file = xsl_file.resolve()
|
||||
|
||||
if xsl_file in visited or not xsl_file.exists():
|
||||
return set()
|
||||
|
||||
visited.add(xsl_file)
|
||||
result: set[Path] = set()
|
||||
|
||||
try:
|
||||
content = xsl_file.read_text(encoding="utf-8", errors="replace")
|
||||
except Exception as e:
|
||||
logger.warning(f"Konnte XSL-Datei nicht lesen: {xsl_file} ({e})")
|
||||
return result
|
||||
|
||||
# Finde alle import/include-Referenzen
|
||||
for match in _IMPORT_INCLUDE_PATTERN.finditer(content):
|
||||
href = match.group(1)
|
||||
referenced_path = (xsl_file.parent / href).resolve()
|
||||
|
||||
if referenced_path.exists() and referenced_path not in visited:
|
||||
result.add(referenced_path)
|
||||
# Rekursiv Abhängigkeiten der referenzierten Datei auflösen
|
||||
result.update(self._resolve_recursive(referenced_path, visited))
|
||||
|
||||
return result
|
||||
|
||||
def invalidate(self, xsl_file: Path | None = None):
|
||||
"""
|
||||
Invalidiert den Cache für eine bestimmte Datei oder den gesamten Cache.
|
||||
|
||||
Args:
|
||||
xsl_file: Pfad zur XSL-Datei oder None für vollständige Invalidierung
|
||||
"""
|
||||
if xsl_file is None:
|
||||
self._cache.clear()
|
||||
logger.debug("XSL-Abhängigkeitscache vollständig invalidiert")
|
||||
else:
|
||||
xsl_file = xsl_file.resolve()
|
||||
# Entferne nicht nur den direkten Eintrag, sondern auch alle Einträge,
|
||||
# die diese Datei als Abhängigkeit haben
|
||||
keys_to_remove = [xsl_file]
|
||||
for cached_path, (_, deps) in self._cache.items():
|
||||
if xsl_file in deps:
|
||||
keys_to_remove.append(cached_path)
|
||||
|
||||
for key in keys_to_remove:
|
||||
self._cache.pop(key, None)
|
||||
|
||||
if keys_to_remove:
|
||||
logger.debug(f"XSL-Abhängigkeitscache invalidiert für {len(keys_to_remove)} Einträg(e)")
|
||||
Reference in New Issue
Block a user