Refactor: Code-Duplikation reduziert und Dead Code entfernt

- blake2b-Hash-Berechnung in zentrale Utility-Funktion extrahiert (src/utils.py) mit chunk-basiertem Hashing für bessere RAM-Effizienz
- _transform_all_xml_files und _transform_all_xml_files_force zu einer Methode mit force-Parameter zusammengeführt
- Project-Lookup-Methoden (getXsl, getJavaVm, etc.) über gemeinsame _lookup()-Hilfsmethode konsolidiert
- Duplizierte XML-Sammel-Methoden entfernt, Set-basierte Duplikatsprüfung eingeführt
- Ungenutzte Imports, Dead Code und wirkungslose Ausdrücke entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 20:21:02 +01:00
parent 6fe61b9a42
commit cb90f9e483
10 changed files with 99 additions and 288 deletions
+12 -24
View File
@@ -104,41 +104,29 @@ class Project(BaseModel):
postgre_sql_db_id: int = Field(..., description="ID der PostgreSQL Datenbank", gt=0)
fop_config_dir: Path | None = Field(None, description="Optionaler Pfad zum Apache FOP Config-Verzeichnis")
def getXsl(self) -> str:
global app_settings
value = [x.name for x in app_settings.xsl_dirs if x.id == self.xsl_dir_id]
@staticmethod
def _lookup(collection, item_id: int, attr: str) -> str:
"""Sucht einen Wert in einer Konfigurationsliste anhand der ID."""
value = [getattr(x, attr) for x in collection if x.id == item_id]
return value[0] if value else ""
return value[0] if len(value) else ""
def getXsl(self) -> str:
return self._lookup(app_settings.xsl_dirs, self.xsl_dir_id, "name")
def getJavaVm(self) -> str:
global app_settings
value = [x.version for x in app_settings.java_vms if x.id == self.java_vm_id]
return value[0] if len(value) else ""
return self._lookup(app_settings.java_vms, self.java_vm_id, "version")
def getSaxon(self) -> str:
global app_settings
value = [x.version for x in app_settings.saxon_jars if x.id == self.saxon_jar_id]
return value[0] if len(value) else ""
return self._lookup(app_settings.saxon_jars, self.saxon_jar_id, "version")
def getApacheFop(self) -> str:
global app_settings
value = [x.version for x in app_settings.apache_fops if x.id == self.apache_fop_id]
return value[0] if len(value) else ""
return self._lookup(app_settings.apache_fops, self.apache_fop_id, "version")
def getDiffPdf(self) -> str:
global app_settings
value = [x.version for x in app_settings.diff_pdfs if x.id == self.diff_pdf_id]
return value[0] if len(value) else ""
return self._lookup(app_settings.diff_pdfs, self.diff_pdf_id, "version")
def getPostgreSqlDb(self) -> str:
global app_settings
value = [x.name for x in app_settings.postgresql_dbs if x.id == self.postgre_sql_db_id]
return value[0] if len(value) else ""
return self._lookup(app_settings.postgresql_dbs, self.postgre_sql_db_id, "name")
class AppSettings(BaseSettings):
+1 -4
View File
@@ -11,7 +11,6 @@ import threading
import time
import psutil
from pathlib import Path
from queue import Queue
from typing import Optional
import tempfile
@@ -196,10 +195,8 @@ class FopWorkerPool:
self.fop_config_file = fop_config_file
self.log_dir = log_dir
# Worker-Prozesse und Queues
# Worker-Prozesse
self.workers: list[subprocess.Popen] = []
self.job_queue: Queue = Queue()
self.result_queue: Queue = Queue()
self.worker_locks: list[threading.Lock] = []
# Temporäres Verzeichnis für kompilierte Java-Klasse
+1 -4
View File
@@ -11,7 +11,6 @@ import threading
import time
import psutil
from pathlib import Path
from queue import Queue
from typing import Optional
import tempfile
@@ -207,10 +206,8 @@ class SaxonWorkerPool:
self.classpath_cache = classpath_cache
self.log_dir = log_dir
# Worker-Prozesse und Queues
# Worker-Prozesse
self.workers: list[subprocess.Popen] = []
self.job_queue: Queue = Queue()
self.result_queue: Queue = Queue()
self.worker_locks: list[threading.Lock] = []
# Temporäres Verzeichnis für kompilierte Java-Klasse
+1 -4
View File
@@ -12,7 +12,6 @@ import threading
import time
import psutil
from pathlib import Path
from queue import Queue
from typing import Optional
import tempfile
@@ -188,10 +187,8 @@ class SaxonWorkerPoolS9Api:
self.classpath_cache = classpath_cache
self.log_dir = log_dir
# Worker-Prozesse und Queues
# Worker-Prozesse
self.workers: list[subprocess.Popen] = []
self.job_queue: Queue = Queue()
self.result_queue: Queue = Queue()
self.worker_locks: list[threading.Lock] = []
# Temporäres Verzeichnis für kompilierte Java-Klasse
-47
View File
@@ -291,50 +291,3 @@ class PdfProjectDlg(QDialog):
self.project_data = project_data
self._load_project_data()
# Convenience-Funktionen für einfache Verwendung
def create_project_dialog(parent=None):
"""
Erstellt einen neuen Projekt-Dialog für ein neues Projekt.
Args:
parent: Übergeordnetes Widget
Returns:
PdfProjectDlg: Der Dialog
"""
return PdfProjectDlg(parent)
def edit_project_dialog(parent=None, project_data=None):
"""
Erstellt einen Projekt-Dialog zum Bearbeiten eines bestehenden Projekts.
Args:
parent: Übergeordnetes Widget
project_data: Bestehende Projektdaten
Returns:
PdfProjectDlg: Der Dialog
"""
return PdfProjectDlg(parent, project_data)
def show_project_dialog(parent=None, project_data=None):
"""
Zeigt einen Projekt-Dialog an und gibt die Ergebnisse zurück.
Args:
parent: Übergeordnetes Widget
project_data: Bestehende Projektdaten (optional)
Returns:
tuple: (accepted: bool, project_data: dict)
"""
dialog = PdfProjectDlg(parent, project_data)
result = dialog.exec()
if result == QDialog.DialogCode.Accepted:
return True, dialog.get_project_data()
else:
return False, None
-3
View File
@@ -12,12 +12,9 @@ from PySide6.QtWidgets import (
QGroupBox,
QLabel,
QPushButton,
QTextEdit,
QTabWidget,
QWidget,
)
from PySide6.QtCore import Qt
logger = logging.getLogger(__name__)
+13 -85
View File
@@ -5,9 +5,8 @@ Dieses Mixin enthält alle Methoden zur blake2b-Hash-Berechnung,
XML-Datei-Zuordnung und Duplikatserkennung für das MainWindow.
"""
import time
import hashlib
import shutil
import time
import logging
from pathlib import Path
from typing import List
@@ -17,6 +16,7 @@ from PySide6.QtWidgets import QMessageBox
from conf import TreeNode, XslFile, XmlFile
from ui.XmlToXslAssignDialog import XmlToXslAssignDialog
from ui.threads import XmlHashCalculatorThread
from utils import calculate_blake2b_hash
logger = logging.getLogger(__name__)
@@ -185,14 +185,14 @@ class HashCalculationMixin:
nodes: Liste der zu durchsuchenden Nodes
xml_files: Liste zum Sammeln der XML-Dateien
"""
seen_paths = {xf.xml for xf in xml_files}
for node in nodes:
if isinstance(node, XslFile) and node.xmls:
# Füge alle XML-Dateien dieser XSL-Datei hinzu
for xml_file in node.xmls:
if xml_file not in xml_files: # Vermeide Duplikate
if xml_file.xml not in seen_paths:
xml_files.append(xml_file)
seen_paths.add(xml_file.xml)
elif isinstance(node, TreeNode) and node.children:
# Rekursiv in Kinder-Nodes suchen
self._collect_xml_files_recursive(node.children, xml_files)
def _on_hash_calculated(self, xml_file: XmlFile, hash_value: str):
@@ -253,68 +253,22 @@ class HashCalculationMixin:
logger.debug(f"Hash bereits vorhanden für {xml_file.xml}: {xml_file.hashsum}")
return
# Prüfe ob Projekt verfügbar ist
if not self.project or not self.project.project_dir:
logger.warning("Kein Projekt-Verzeichnis für Hash-Berechnung verfügbar")
return
xml_file_path = Path(self.project.project_dir) / xml_file.xml
if not xml_file_path.exists():
logger.warning(f"XML-Datei nicht gefunden: {xml_file_path}")
return
# Datei binär lesen und Hash berechnen
with open(xml_file_path, "rb") as f:
file_content = f.read()
hash_obj = hashlib.blake2b(file_content)
hash_hex = hash_obj.hexdigest()
# Hash mit Präfix setzen
xml_file.hashsum = f"blake2b:{hash_hex}"
logger.debug(f"Hash berechnet für {xml_file.xml}: {xml_file.hashsum}")
hash_value = calculate_blake2b_hash(xml_file_path)
if hash_value:
xml_file.hashsum = hash_value
logger.debug(f"Hash berechnet für {xml_file.xml}: {xml_file.hashsum}")
except Exception as e:
logger.error(f"Fehler beim Berechnen des Hash für {xml_file.xml}: {e}")
def _get_all_project_xml_files(self) -> List[XmlFile]:
"""
Sammelt alle XmlFile-Objekte aus dem gesamten Projekt für Hash-Vergleiche.
Returns:
List[XmlFile]: Liste aller XML-Dateien im Projekt
"""
xml_files = []
try:
if self.pdf_project and self.pdf_project.nodes:
self._collect_xml_files_for_hash_comparison(self.pdf_project.nodes, xml_files)
logger.debug(f"Hash-Vergleich: {len(xml_files)} XML-Dateien im Projekt gefunden")
return xml_files
except Exception as e:
logger.error(f"Fehler beim Sammeln der XML-Dateien für Hash-Vergleich: {e}")
return []
def _collect_xml_files_for_hash_comparison(self, nodes, xml_files: List[XmlFile]):
"""
Sammelt rekursiv alle XML-Dateien aus den Nodes für Hash-Vergleiche.
Args:
nodes: Liste der zu durchsuchenden Nodes
xml_files: Liste zum Sammeln der XML-Dateien
"""
for node in nodes:
if isinstance(node, XslFile) and node.xmls:
# Füge alle XML-Dateien dieser XSL-Datei hinzu
for xml_file in node.xmls:
# Vermeide Duplikate basierend auf Pfad
if not any(existing.xml == xml_file.xml for existing in xml_files):
xml_files.append(xml_file)
elif isinstance(node, TreeNode) and node.children:
# Rekursiv in Kinder-Nodes suchen
self._collect_xml_files_for_hash_comparison(node.children, xml_files)
"""Sammelt alle XmlFile-Objekte aus dem gesamten Projekt."""
return self._collect_all_xml_files()
def _find_xml_file_by_hash(self, target_hash: str) -> XmlFile | None:
"""
@@ -410,34 +364,8 @@ class HashCalculationMixin:
return True # Im Zweifelsfall annehmen, dass der Name verwendet wird
def _calculate_hash_for_file(self, file_path: Path) -> str | None:
"""
Berechnet synchron den blake2b-Hash für eine Datei.
Args:
file_path: Pfad zur Datei
Returns:
str|None: Hash-Wert mit blake2b: Präfix oder None bei Fehler
"""
try:
if not file_path.exists():
logger.warning(f"Datei für Hash-Berechnung nicht gefunden: {file_path}")
return None
# Datei binär lesen und Hash berechnen
with open(file_path, "rb") as f:
file_content = f.read()
hash_obj = hashlib.blake2b(file_content)
hash_hex = hash_obj.hexdigest()
# Hash mit Präfix zurückgeben
hash_value = f"blake2b:{hash_hex}"
logger.debug(f"Hash berechnet für {file_path}: {hash_value}")
return hash_value
except Exception as e:
logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}")
return None
"""Berechnet synchron den blake2b-Hash für eine Datei."""
return calculate_blake2b_hash(file_path)
def _assign_existing_xml_to_nodes(self, existing_xml: XmlFile, selected_xsl_nodes: list):
"""
+28 -73
View File
@@ -579,9 +579,6 @@ class TransformationMixin:
# Progress Bar anzeigen
map_key = f"{xml_file_name}|{xsl_id_str}"
if map_key not in self.xml_item_map and self.xml_item_map:
# Zeige erste Keys zur Diagnose
list(self.xml_item_map.keys())[:3]
logger.info(f"Suche TreeWidget-Item für: '{map_key}'")
logger.info(f"Map hat {len(self.xml_item_map)} Einträge")
tree_item = self.xml_item_map.get(map_key)
@@ -741,10 +738,12 @@ class TransformationMixin:
f"{successful_count} von {total_count} Transformationen erfolgreich\n{failed_count} fehlgeschlagen\n\nGesamtdauer: {duration_str}",
)
def _transform_all_xml_files(self):
def _transform_all_xml_files(self, force: bool = False):
"""
Transformiert ALLE XML-Dateien in allen TreeNodes des TreeWidgets.
Nur Dateien, die nicht up-to-date sind, werden transformiert.
Args:
force: Wenn True, werden alle Dateien unabhängig vom Änderungsstatus neu transformiert.
"""
try:
if not self.project or not self.pdf_project:
@@ -781,87 +780,43 @@ class TransformationMixin:
return
# Frage Benutzer um Bestätigung
if force:
title = "Alle XML-Dateien neu transformieren (force)"
message = (
f"Möchten Sie wirklich ALLE {len(all_jobs)} XML-Dateien NEU transformieren?\n\n"
f"⚠ WARNUNG: Im Force-Modus werden alle Dateien unabhängig von ihrem Status neu verarbeitet!\n"
f"Dies kann längere Zeit in Anspruch nehmen."
)
default_button = QMessageBox.StandardButton.No
else:
title = "Alle XML-Dateien transformieren"
message = (
f"Möchten Sie wirklich alle {len(all_jobs)} XML-Dateien transformieren?\n\n"
f"Nur Dateien mit Änderungen werden verarbeitet."
)
default_button = QMessageBox.StandardButton.Yes
reply = QMessageBox.question(
self,
"Alle XML-Dateien transformieren",
f"Möchten Sie wirklich alle {len(all_jobs)} XML-Dateien transformieren?\n\n"
f"Nur Dateien mit Änderungen werden verarbeitet.",
title,
message,
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes,
default_button,
)
if reply != QMessageBox.StandardButton.Yes:
logger.info("Transformation abgebrochen durch Benutzer")
logger.info(f"{'Force-' if force else ''}Transformation abgebrochen durch Benutzer")
return
logger.info(f"Starte Transformation für alle {len(all_jobs)} XML-Dateien (nicht-force)")
logger.info(f"Starte {'Force-' if force else ''}Transformation für alle {len(all_jobs)} XML-Dateien")
# Starte Transformation in separatem Thread
self._start_transformation(all_jobs, force=False)
self._start_transformation(all_jobs, force=force)
except Exception as e:
logger.error(f"Fehler beim Transformieren aller XML-Dateien: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten der Transformation: {str(e)}")
def _transform_all_xml_files_force(self):
"""
Transformiert ALLE XML-Dateien in allen TreeNodes des TreeWidgets (force).
Alle Dateien werden unabhängig vom Änderungsstatus neu transformiert.
"""
try:
if not self.project or not self.pdf_project:
QMessageBox.warning(self, "Fehler", "Kein Projekt geladen")
return
# Sammle alle XSL/XML-Paare aus allen Root-Nodes
all_jobs = []
root = self.ui.treeWidget.invisibleRootItem()
for i in range(root.childCount()):
root_item = root.child(i)
root_node = root_item.data(0, Qt.ItemDataRole.UserRole)
if isinstance(root_node, TreeNode):
# Sammle alle XSL/XML-Paare rekursiv
xsl_xml_pairs = self._collect_all_xsl_xml_pairs_recursive(root_node, root_item)
# Erstelle TransformationJobs
for xsl_file_obj, xml_file_obj, xsl_file_item in xsl_xml_pairs:
job = self._create_transformation_job(xsl_file_obj, xml_file_obj, xsl_file_item)
if job:
all_jobs.append(job)
elif isinstance(root_node, XslFile):
# Direkt XslFile als Root-Element
for xml_file_obj in root_node.xmls:
job = self._create_transformation_job(root_node, xml_file_obj, root_item)
if job:
all_jobs.append(job)
if not all_jobs:
QMessageBox.information(self, "Info", "Keine XML-Dateien zum Transformieren gefunden")
return
# Frage Benutzer um Bestätigung (mit Warnung wegen force)
reply = QMessageBox.question(
self,
"Alle XML-Dateien neu transformieren (force)",
f"Möchten Sie wirklich ALLE {len(all_jobs)} XML-Dateien NEU transformieren?\n\n"
f"⚠ WARNUNG: Im Force-Modus werden alle Dateien unabhängig von ihrem Status neu verarbeitet!\n"
f"Dies kann längere Zeit in Anspruch nehmen.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
logger.info("Force-Transformation abgebrochen durch Benutzer")
return
logger.info(f"Starte Force-Transformation für alle {len(all_jobs)} XML-Dateien")
# Starte Transformation in separatem Thread (mit force=True)
self._start_transformation(all_jobs, force=True)
except Exception as e:
logger.error(f"Fehler beim Force-Transformieren aller XML-Dateien: {e}")
QMessageBox.critical(self, "Fehler", f"Fehler beim Starten der Force-Transformation: {str(e)}")
"""Transformiert ALLE XML-Dateien im Force-Modus."""
self._transform_all_xml_files(force=True)
+5 -44
View File
@@ -7,16 +7,14 @@ Dieses Modul enthält alle QThread-Klassen, die für Hintergrundoperationen verw
- TransformationThread: Ausführung von XSL-Transformationen
"""
import hashlib
import logging
import shutil
from pathlib import Path
from typing import List
from PySide6.QtCore import QThread, Signal
from conf import TreeNode, XslFile, XmlFile
from transform import TransformationJob
from utils import calculate_blake2b_hash
logger = logging.getLogger(__name__)
@@ -32,7 +30,7 @@ class XmlHashCalculatorThread(QThread):
calculation_finished = Signal(int, int) # processed_count, total_count
error_occurred = Signal(str, str) # xml_file_path, error_message
def __init__(self, project_dir: Path, xml_files: List[XmlFile]):
def __init__(self, project_dir: Path, xml_files: list[XmlFile]):
"""
Initialisiert den Hash-Berechnungs-Thread.
@@ -81,32 +79,8 @@ class XmlHashCalculatorThread(QThread):
logger.info(f"Hash-Berechnung abgeschlossen: {self.processed_count}/{len(self.xml_files)} verarbeitet")
def _calculate_blake2b_hash(self, file_path: Path) -> str | None:
"""
Berechnet den blake2b-Hash einer XML-Datei.
Args:
file_path: Pfad zur XML-Datei
Returns:
str: Hash-Wert mit "blake2b:" Präfix oder None bei Fehler
"""
try:
if not file_path.exists():
logger.warning(f"XML-Datei nicht gefunden: {file_path}")
return None
# Datei binär lesen und Hash berechnen
with open(file_path, "rb") as f:
file_content = f.read()
hash_obj = hashlib.blake2b(file_content)
hash_hex = hash_obj.hexdigest()
# Präfix hinzufügen
return f"blake2b:{hash_hex}"
except Exception as e:
logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}")
return None
"""Berechnet den blake2b-Hash einer XML-Datei."""
return calculate_blake2b_hash(file_path)
class XmlBatchProcessingThread(QThread):
@@ -217,20 +191,7 @@ class XmlBatchProcessingThread(QThread):
def _calculate_hash_for_file(self, file_path: Path) -> str | None:
"""Berechnet blake2b Hash für eine Datei."""
try:
if not file_path.exists():
return None
with open(file_path, "rb") as f:
file_content = f.read()
hash_obj = hashlib.blake2b(file_content)
hash_hex = hash_obj.hexdigest()
return f"blake2b:{hash_hex}"
except Exception as e:
logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}")
return None
return calculate_blake2b_hash(file_path)
def _find_xml_file_by_hash(self, hash_value: str) -> XmlFile | None:
"""Sucht eine XML-Datei anhand ihres Hash-Werts."""
+38
View File
@@ -0,0 +1,38 @@
"""
Gemeinsame Utility-Funktionen für DocuMentor.
"""
import hashlib
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
HASH_PREFIX = "blake2b"
def calculate_blake2b_hash(file_path: Path) -> str | None:
"""
Berechnet den blake2b-Hash einer Datei.
Args:
file_path: Pfad zur Datei
Returns:
str: Hash-Wert mit "blake2b:" Präfix oder None bei Fehler
"""
try:
if not file_path.exists():
logger.warning(f"Datei für Hash-Berechnung nicht gefunden: {file_path}")
return None
hash_obj = hashlib.blake2b()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
hash_obj.update(chunk)
return f"{HASH_PREFIX}:{hash_obj.hexdigest()}"
except Exception as e:
logger.error(f"Fehler beim Berechnen des Hash für {file_path}: {e}")
return None