Feat: Hilfe-Menü mit Info-Dialog und Lizenz-Parser hinzugefügt (v1.1.0)
Neues Menü "Hilfe > Info" zeigt Programmversion, Python-Version und alle Drittanbieter-Bibliotheken mit installierten Versionen und Lizenzinfos an. Der license_parser liest THIRD_PARTY_LICENSES.txt als Datenquelle und ergänzt tatsächlich installierte Versionen via importlib.metadata. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Parser für THIRD_PARTY_LICENSES.txt.
|
||||
|
||||
Extrahiert strukturierte Lizenzinformationen und ergänzt sie
|
||||
mit den tatsächlich installierten Paketversionen via importlib.metadata.
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Pfad zur Lizenzdatei relativ zum Projektroot
|
||||
LICENSE_FILE = Path(__file__).parent.parent / "THIRD_PARTY_LICENSES.txt"
|
||||
|
||||
# Mapping von Anzeigenamen zu PyPI-Paketnamen für Sonderfälle
|
||||
_PACKAGE_NAME_MAP: dict[str, str] = {
|
||||
"PySide6": "PySide6",
|
||||
"Pydantic": "pydantic",
|
||||
"Pydantic-Settings": "pydantic-settings",
|
||||
"Pydantic-YAML": "pydantic-yaml",
|
||||
"Polars": "polars",
|
||||
"ConnectorX (via Polars)": "connectorx",
|
||||
"PyArrow (via Polars)": "pyarrow",
|
||||
"psutil": "psutil",
|
||||
"lxml": "lxml",
|
||||
"Ruff (Development)": "ruff",
|
||||
"PyInstaller (Development)": "pyinstaller",
|
||||
"Pillow (Development)": "pillow",
|
||||
}
|
||||
|
||||
# Regex für den Beginn eines nummerierten Eintrags (z.B. "1. PySide6" oder "10. PyInstaller (Development)")
|
||||
_ENTRY_PATTERN = re.compile(r"^\d+\.\s+(.+)$")
|
||||
|
||||
# Bekannte Feldschlüssel in der Lizenzdatei
|
||||
_KNOWN_KEYS = {"Version", "Lizenz", "Webseite", "GitHub", "Beschreibung", "Copyright", "Datei", "Hinweis"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class LicenseEntry:
|
||||
"""Ein einzelner Eintrag aus THIRD_PARTY_LICENSES.txt."""
|
||||
|
||||
name: str
|
||||
license: str = ""
|
||||
installed_version: str = ""
|
||||
website: str = ""
|
||||
description: str = ""
|
||||
copyright: str = ""
|
||||
category: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParsedLicenses:
|
||||
"""Ergebnis des Parsens der Lizenzdatei."""
|
||||
|
||||
entries: list[LicenseEntry] = field(default_factory=list)
|
||||
|
||||
|
||||
def _normalize_package_name(display_name: str) -> str:
|
||||
"""Normalisiert einen Anzeigenamen zu einem PyPI-Paketnamen."""
|
||||
if display_name in _PACKAGE_NAME_MAP:
|
||||
return _PACKAGE_NAME_MAP[display_name]
|
||||
# Fallback: lowercase, Leerzeichen/Klammern entfernen
|
||||
return display_name.lower().split("(")[0].strip()
|
||||
|
||||
|
||||
def _get_installed_version(package_name: str) -> str:
|
||||
"""Ermittelt die installierte Version eines Pakets."""
|
||||
try:
|
||||
return version(package_name)
|
||||
except PackageNotFoundError:
|
||||
return ""
|
||||
|
||||
|
||||
def parse_license_file(license_file: Path | None = None) -> ParsedLicenses:
|
||||
"""
|
||||
Parst THIRD_PARTY_LICENSES.txt und gibt strukturierte Einträge zurück.
|
||||
|
||||
Args:
|
||||
license_file: Pfad zur Lizenzdatei. Standard: LICENSE_FILE
|
||||
|
||||
Returns:
|
||||
ParsedLicenses mit allen gefundenen Einträgen inkl. installierter Versionen
|
||||
"""
|
||||
if license_file is None:
|
||||
license_file = LICENSE_FILE
|
||||
|
||||
if not license_file.exists():
|
||||
logger.warning(f"Lizenzdatei nicht gefunden: {license_file}")
|
||||
return ParsedLicenses()
|
||||
|
||||
text = license_file.read_text(encoding="utf-8")
|
||||
lines = text.splitlines()
|
||||
|
||||
result = ParsedLicenses()
|
||||
current_category = ""
|
||||
current_entry: LicenseEntry | None = None
|
||||
last_key: str | None = None # Letzter erkannter Schlüssel (für Fortsetzungszeilen)
|
||||
separator_line = "=" * 20 # Mindestlänge für Sektions-Trennlinien
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Sektions-Trennlinie erkannt → nächste Zeile ist Sektionsname
|
||||
if line.startswith(separator_line):
|
||||
i += 1
|
||||
if i < len(lines):
|
||||
section_name = lines[i].strip()
|
||||
|
||||
# Bei "Lizenztexte" aufhören — ab hier kommen nur noch Volltexte
|
||||
if section_name == "Lizenztexte":
|
||||
# Letzten Eintrag noch sichern
|
||||
if current_entry:
|
||||
result.entries.append(current_entry)
|
||||
break
|
||||
|
||||
# Neue Kategorie setzen (ignoriere den Datei-Header "THIRD PARTY LICENSES")
|
||||
if section_name != "THIRD PARTY LICENSES":
|
||||
current_category = section_name
|
||||
|
||||
# Schließende Trennlinie der Sektion überspringen
|
||||
i += 1
|
||||
if i < len(lines) and lines[i].startswith(separator_line):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Nummerierter Eintrag (z.B. "1. PySide6")
|
||||
match = _ENTRY_PATTERN.match(line.strip())
|
||||
if match:
|
||||
# Vorherigen Eintrag speichern
|
||||
if current_entry:
|
||||
result.entries.append(current_entry)
|
||||
|
||||
entry_name = match.group(1).strip()
|
||||
current_entry = LicenseEntry(name=entry_name, category=current_category)
|
||||
|
||||
# Installierte Version ermitteln
|
||||
pypi_name = _normalize_package_name(entry_name)
|
||||
current_entry.installed_version = _get_installed_version(pypi_name)
|
||||
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Schlüssel-Wert-Zeile innerhalb eines Eintrags (z.B. " Version: >=6.10.1")
|
||||
stripped = line.strip()
|
||||
if current_entry and ":" in stripped:
|
||||
key, _, value = stripped.partition(":")
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
if key in _KNOWN_KEYS:
|
||||
last_key = key
|
||||
if key == "Lizenz":
|
||||
current_entry.license = value
|
||||
elif key == "Webseite":
|
||||
current_entry.website = value
|
||||
elif key == "GitHub" and not current_entry.website:
|
||||
# GitHub nur als Fallback wenn keine Webseite vorhanden
|
||||
current_entry.website = value
|
||||
elif key == "Beschreibung":
|
||||
current_entry.description = value
|
||||
elif key == "Copyright":
|
||||
current_entry.copyright = value
|
||||
else:
|
||||
last_key = None
|
||||
elif current_entry and stripped and not stripped.startswith("="):
|
||||
# Fortsetzungszeile (z.B. mehrzeiliger Copyright-Eintrag)
|
||||
if last_key == "Copyright":
|
||||
current_entry.copyright += "\n" + stripped
|
||||
|
||||
i += 1
|
||||
|
||||
# Letzten Eintrag sichern (falls Datei nicht mit "Lizenztexte" endet)
|
||||
if current_entry and current_entry not in result.entries:
|
||||
result.entries.append(current_entry)
|
||||
|
||||
logger.info(f"{len(result.entries)} Lizenzeinträge aus {license_file.name} geladen")
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Standalone-Test
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
parsed = parse_license_file()
|
||||
for entry in parsed.entries:
|
||||
ver = entry.installed_version or "—"
|
||||
logger.debug(f"[{entry.category}] {entry.name}: {ver} ({entry.license})")
|
||||
Reference in New Issue
Block a user