122 lines
4.3 KiB
Python
122 lines
4.3 KiB
Python
|
|
"""
|
||
|
|
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)")
|