From 151376aee33bd92d0dae4a67bf5a53befa5e9122 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 28 Aug 2026 14:00:34 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Pfad-Regeln=20an=20EINE=20Stelle=20?= =?UTF-8?q?=E2=80=94=20dreimal=20dieselbe=20Falle=20war=20zweimal=20zu=20v?= =?UTF-8?q?iel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ampel-Lauf zu b47d215 rot, drei Fehlschlaege, alle mein eigener Code: platz_orte os.path.splitdrive kannte auf Linux kein "D:" -> zwei Laufwerke galten als eines naechster_vorhandener os.path.dirname zerlegte C:\Users\Test\... nicht pruefe_windows die Plattform-Pruefung stand VOR der eingespritzten Version -> pruefe_windows(22631) gab auf Linux FEHLER `os.path` richtet sich nach der Maschine, auf der es laeuft. Das ist fast immer richtig -- und an jeder Stelle falsch, an der ueber Pfade einer ANDEREN Maschine gerechnet wird. Genau das war heute schon zweimal da: tools/katalog.py C:\Program Files (x86)\MakeMKV/makemkvcon64.exe platform/verknuepfungen dirname() einer .lnk gab einen leeren String Zweimal einzeln repariert, beim dritten Mal gehoert es an EINE Stelle: `rippy/pfade.py` mit `ist_windows_pfad`, `verbinden`, `ordner_von`, `laufwerk_von`, `gleiches_laufwerk`, `naechster_vorhandener`. Die Regel steht dort im Kopf: **Der Pfad entscheidet, nicht der Rechner.** katalog, verknuepfungen, betrieb und einrichtung benutzen jetzt alle dasselbe. Und `pruefe_windows` beachtet einen eingespritzten Wert wieder. Ein Einspritzpunkt, der ignoriert wird, ist keiner -- dann sind die Tests daneben, die ihn benutzen. Sieben neue Tests in test_pfade.py, die BEIDE Zweige pruefen und deshalb ueberall laufen. Das war der eigentliche Mangel: Jede dieser drei Fallen war nur auf der jeweils anderen Plattform sichtbar. Co-Authored-By: Claude Opus 5 --- src/rippy/betrieb.py | 30 +++------ src/rippy/einrichtung.py | 15 +++-- src/rippy/pfade.py | 94 ++++++++++++++++++++++++++++ src/rippy/platform/verknuepfungen.py | 5 +- src/rippy/test_pfade.py | 54 ++++++++++++++++ src/rippy/tools/katalog.py | 42 +++---------- 6 files changed, 180 insertions(+), 60 deletions(-) create mode 100644 src/rippy/pfade.py create mode 100644 src/rippy/test_pfade.py diff --git a/src/rippy/betrieb.py b/src/rippy/betrieb.py index efba49c..a4360b7 100644 --- a/src/rippy/betrieb.py +++ b/src/rippy/betrieb.py @@ -61,6 +61,8 @@ Wo es geht, wird nachgesehen statt geglaubt: import os import sys +from rippy import pfade + # Docker legt diese Datei in jedem Container an. Sie ist das verlässlichste # Kennzeichen, das ohne Zusatzpakete zu haben ist — verlässlicher als eine # Umgebungsvariable, die jeder setzen (und vergessen) kann. @@ -138,24 +140,8 @@ def auskunft(werte: dict, container: bool = None, def naechster_vorhandener(pfad: str, existiert=None) -> str: - """Der nächste Ordner nach oben, den es WIRKLICH gibt. "" wenn keiner. - - Frisch installiert gibt es den Ablage-Ordner noch nicht — `disk_usage` - wirft dann, und im Dashboard stand „unbekannt". Der Nutzer will aber - wissen, ob auf dem LAUFWERK Platz ist, und das lässt sich beantworten. - """ - existiert = existiert or os.path.isdir - pfad = (pfad or "").rstrip("\\/") - gesehen = set() - while pfad and pfad not in gesehen: - if existiert(pfad): - return pfad - gesehen.add(pfad) - eltern = os.path.dirname(pfad) - if eltern == pfad: # Wurzel erreicht - break - pfad = eltern - return "" + """Der naechste vorhandene Ordner nach oben — siehe `rippy.pfade`.""" + return pfade.naechster_vorhandener(pfad, existiert) def platz_orte(werte: dict, container: bool = None, @@ -186,11 +172,15 @@ def platz_orte(werte: dict, container: bool = None, {"name": "Arbeitsverzeichnis (/app/temp)", "pfad": "/app/temp"}] ablage = ablage_vorgabe(werte, container, system) - temp = lager.get("temp") or os.path.join(ablage, "_arbeit") + temp = lager.get("temp") or pfade.verbinden(ablage, "_arbeit") orte = [{"name": "Ablage", "pfad": ablage}] # Nur wenn es ein ANDERES Laufwerk ist, lohnt eine zweite Zeile. Zweimal # dieselbe Zahl unter zwei Namen sieht aus wie zwei Auskünfte und ist eine. - if os.path.splitdrive(temp)[0].lower() != os.path.splitdrive(ablage)[0].lower(): + # + # Ueber `pfade`, nicht ueber `os.path`: Auf dem Linux-Runner der Ampel + # kannte `splitdrive` kein `D:` und hielt zwei verschiedene Laufwerke + # fuer eines. + if not pfade.gleiches_laufwerk(temp, ablage): orte.append({"name": "Arbeitsverzeichnis", "pfad": temp}) return orte diff --git a/src/rippy/einrichtung.py b/src/rippy/einrichtung.py index fa086df..e2a4431 100644 --- a/src/rippy/einrichtung.py +++ b/src/rippy/einrichtung.py @@ -63,8 +63,15 @@ def _eintrag(name, zustand, text, abhilfe=""): # ── Die einzelnen Prüfungen ───────────────────────────────────────────── def pruefe_windows(version=None) -> dict: - """Windows 10 oder neuer. Darunter gibt es kein WebView2.""" - if os.name != "nt": + """Windows 10 oder neuer. Darunter gibt es kein WebView2. + + ⚠️ Ein eingespritzter `version`-Wert wird BEACHTET, auch auf Linux. Vorher + stand die Plattform-Prüfung davor, und damit war jeder eingespritzte Wert + wirkungslos: Auf dem Linux-Runner der Ampel gab `pruefe_windows(22631)` + ein FEHLER zurück, obwohl der Test genau eine gültige Build-Nummer + übergeben hatte. Ein Einspritzpunkt, der ignoriert wird, ist keiner. + """ + if version is None and os.name != "nt": return _eintrag("Betriebssystem", FEHLER, "Kein Windows", "Für Linux gibt es die Docker- und die Headless-Fassung.") bau = version if version is not None else sys.getwindowsversion().build @@ -96,10 +103,10 @@ def pruefe_webview(version=None) -> dict: def pruefe_platz(ziel: str, frei_mb=None) -> dict: """Genug Platz am Zielort? Gemessen am nächsten vorhandenen Ordner.""" - from rippy import betrieb + from rippy import pfade if frei_mb is None: - pfad = betrieb.naechster_vorhandener(ziel) + pfad = pfade.naechster_vorhandener(ziel) if not pfad: return _eintrag("Speicherplatz", FEHLER, "Der Ort ist nicht erreichbar: %s" % ziel, diff --git a/src/rippy/pfade.py b/src/rippy/pfade.py new file mode 100644 index 0000000..5e51378 --- /dev/null +++ b/src/rippy/pfade.py @@ -0,0 +1,94 @@ +r"""Pfade nach IHREN Regeln behandeln, nicht nach denen der laufenden Maschine. + +## Warum es dieses Modul gibt (28.08.2026, zweimal bezahlt) + +`os.path` richtet sich nach der Plattform, auf der es gerade läuft. Das ist +fast immer richtig — und in diesem Projekt an drei Stellen falsch, weil dort +über Pfade einer ANDEREN Maschine gerechnet wird: + +* `tools/katalog.py` baut Windows-Installationsorte (`C:\Program Files\…`). + Auf dem Linux-Runner der Ampel wurde daraus + `C:\Program Files (x86)\MakeMKV/makemkvcon64.exe` — ein Pfad, den es auf + keiner Maschine gibt. Fünf Tests rot, und zwar NUR auf Linux. +* `platform/verknuepfungen.py` leitet den Arbeitsordner einer `.lnk` ab. Eine + `.lnk` zeigt IMMER auf einen Windows-Pfad, auch wenn der Code gerade auf + Linux läuft. `os.path.dirname` gab dort einen leeren String. +* `betrieb.py` vergleicht Laufwerksbuchstaben und sucht das nächste + vorhandene Elternverzeichnis. Auf Linux kannte `splitdrive` kein `D:`, und + `dirname` zerlegte `C:\Users\Test\…` nicht. + +Dreimal dasselbe Muster, dreimal einzeln repariert. Beim dritten Mal gehört +es an EINE Stelle — sonst kommt es ein viertes Mal wieder. + +## Die Regel + +**Der Pfad entscheidet, nicht der Rechner.** Ein Laufwerksbuchstabe oder ein +Backslash heißt Windows; alles andere heißt POSIX. +""" + +import ntpath +import posixpath + + +def ist_windows_pfad(pfad: str) -> bool: + r"""`C:\…` oder irgendein Backslash — dann ist es ein Windows-Pfad.""" + pfad = pfad or "" + return "\\" in pfad or (len(pfad) > 1 and pfad[1] == ":") + + +def _modul(pfad: str): + return ntpath if ist_windows_pfad(pfad) else posixpath + + +def verbinden(basis: str, *teile) -> str: + r"""Pfadteile mit dem Trenner der BASIS verbinden. + + Achtung, Windows-Eigenheit: `verbinden("D:", "Rippy")` ergibt `D:Rippy`, + NICHT `D:\Rippy`. `D:` ohne Backslash bedeutet „der aktuelle Ordner auf + Laufwerk D" — `ntpath.join` setzt deshalb absichtlich keinen Trenner. Das + ist richtig so, auch wenn es überrascht. + """ + return _modul(basis).join(basis, *teile) + + +def ordner_von(datei: str) -> str: + """Der Ordner einer Datei — nach dem Trenner der DATEI.""" + return _modul(datei).dirname(datei) + + +def laufwerk_von(pfad: str) -> str: + r"""Der Laufwerksteil (`C:`, `\\server\freigabe`) — leer bei POSIX-Pfaden. + + Für die Frage „liegen zwei Pfade auf demselben Laufwerk". Zwei leere + Ergebnisse heißen dabei „beide POSIX" und damit ebenfalls: dasselbe. + """ + return _modul(pfad).splitdrive(pfad)[0] + + +def gleiches_laufwerk(a: str, b: str) -> bool: + """Liegen beide auf demselben Laufwerk? (ohne Rücksicht auf Groß/Klein)""" + return laufwerk_von(a).lower() == laufwerk_von(b).lower() + + +def naechster_vorhandener(pfad: str, existiert=None) -> str: + """Der nächste Ordner nach oben, den es WIRKLICH gibt. "" wenn keiner. + + Frisch installiert gibt es den Ablage-Ordner noch nicht — `disk_usage` + wirft dann, und im Dashboard stand „unbekannt". Der Nutzer will aber + wissen, ob auf dem LAUFWERK Platz ist, und das lässt sich beantworten. + """ + import os + + existiert = existiert or os.path.isdir + modul = _modul(pfad) + pfad = (pfad or "").rstrip("\\/") + gesehen = set() + while pfad and pfad not in gesehen: + if existiert(pfad): + return pfad + gesehen.add(pfad) + eltern = modul.dirname(pfad) + if eltern == pfad: # Wurzel erreicht + break + pfad = eltern + return "" diff --git a/src/rippy/platform/verknuepfungen.py b/src/rippy/platform/verknuepfungen.py index 1ed2e18..9317855 100644 --- a/src/rippy/platform/verknuepfungen.py +++ b/src/rippy/platform/verknuepfungen.py @@ -33,10 +33,11 @@ der Browser auf; läuft er nicht, wird er zuerst gestartet. Ein Symbol, das beim zweiten Klick einen zweiten Server startet, wäre eine Falle. """ -import ntpath import os import subprocess +from rippy import pfade + def shell_ordner(name: str) -> str: """Der ECHTE Pfad eines Windows-Sonderordners („Desktop", „Programs"). @@ -90,7 +91,7 @@ def skript(lnk: str, ziel: str, argumente: str = "", icon: str = "", # C:\Programme\Rippy\Rippy.exe einen LEEREN String zurueck, und der # Test schlug nur dort fehl. zeilen.append("$s.WorkingDirectory = '%s'" - % _ps_text(arbeitsordner or ntpath.dirname(ziel))) + % _ps_text(arbeitsordner or pfade.ordner_von(ziel))) zeilen.append("$s.Save()") return "; ".join(zeilen) diff --git a/src/rippy/test_pfade.py b/src/rippy/test_pfade.py new file mode 100644 index 0000000..0cb4453 --- /dev/null +++ b/src/rippy/test_pfade.py @@ -0,0 +1,54 @@ +"""Pfade nach IHREN Regeln, nicht nach denen der laufenden Maschine. + +Dreimal dieselbe Falle in drei Dateien (katalog, verknuepfungen, betrieb) — +jedes Mal nur auf der ANDEREN Plattform sichtbar. Diese Tests laufen ueberall +und pruefen beide Zweige, damit es kein viertes Mal gibt. +""" + +from rippy import pfade + +B = chr(92) + + +def test_windows_pfad_wird_erkannt(): + assert pfade.ist_windows_pfad("C:" + B + "Users") is True + assert pfade.ist_windows_pfad("D:") is True + assert pfade.ist_windows_pfad("/usr/local/bin") is False + assert pfade.ist_windows_pfad("") is False + assert pfade.ist_windows_pfad(None) is False + + +def test_verbinden_folgt_dem_pfad(): + assert pfade.verbinden("C:" + B + "Program Files", "MakeMKV") == \ + "C:" + B + "Program Files" + B + "MakeMKV" + assert pfade.verbinden("/usr/local/bin", "makemkvcon") == \ + "/usr/local/bin/makemkvcon" + + +def test_ordner_von_folgt_dem_pfad(): + assert pfade.ordner_von("C:" + B + "R" + B + "x.exe") == "C:" + B + "R" + assert pfade.ordner_von("/opt/r/x") == "/opt/r" + + +def test_laufwerke_werden_unterschieden(): + """Auf dem Linux-Runner kannte `os.path.splitdrive` kein `D:` und hielt + zwei verschiedene Laufwerke fuer eines.""" + assert pfade.gleiches_laufwerk("D:" + B + "Filme", "E:" + B + "Arbeit") is False + assert pfade.gleiches_laufwerk("D:" + B + "Filme", "d:" + B + "Anderes") is True + assert pfade.gleiches_laufwerk("/a", "/b") is True + + +def test_naechster_vorhandener_geht_nach_oben(): + da = "C:" + B + "Users" + B + "Test" + assert pfade.naechster_vorhandener( + da + B + "Videos" + B + "Rippy", existiert=lambda p: p == da) == da + + +def test_naechster_vorhandener_auf_posix(): + assert pfade.naechster_vorhandener( + "/srv/rippy/media", existiert=lambda p: p == "/srv") == "/srv" + + +def test_wenn_gar_nichts_existiert_wird_nichts_behauptet(): + assert pfade.naechster_vorhandener("Z:" + B + "nix", + existiert=lambda p: False) == "" diff --git a/src/rippy/tools/katalog.py b/src/rippy/tools/katalog.py index 81ba342..9338644 100644 --- a/src/rippy/tools/katalog.py +++ b/src/rippy/tools/katalog.py @@ -38,12 +38,12 @@ Uninstall-Zweig der Registry (`DisplayVersion`). Das ist kein Notbehelf, sondern die einzige Stelle, an der sie unter Windows überhaupt steht. """ -import ntpath import os -import posixpath import shutil import subprocess +from rippy import pfade + # ── Die Werkzeuge ─────────────────────────────────────────────────────── # `programme` Dateinamen, in dieser Reihenfolge probiert (64 Bit zuerst). # `orte` Bekannte Installationsordner, mit %UMGEBUNGSVARIABLEN%. @@ -86,38 +86,12 @@ def werkzeug_ordner(basis: str = None) -> str: return os.path.join(os.path.expanduser("~"), ".local", "share", "rippy", "tools") -def verbinden(basis: str, *teile) -> str: - r"""Pfadteile mit dem Trenner der BASIS verbinden, nicht dem des Systems. - - ## Warum nicht `os.path.join` (Ampel rot 28.08.2026, Läufe 181–184) - - `os.path.join` nimmt immer den Trenner der laufenden Maschine. Auf dem - Linux-Runner der Ampel wurde aus `C:\Program Files (x86)\MakeMKV` und - `makemkvcon64.exe` dann: - - C:\Program Files (x86)\MakeMKV/makemkvcon64.exe - - Ein Pfad, den es auf keiner Maschine gibt. Fünf Tests sind daran - gescheitert — und zwar NUR auf Linux, weshalb es hier lange niemand sah. - - Ein Werkzeug-Pfad beschreibt einen Ort auf der Maschine, die ihn - NENNT — nicht auf der, die gerade rechnet. Deshalb entscheidet die - Basis: Laufwerksbuchstabe oder Backslash heißt Windows. - """ - modul = ntpath if _sieht_nach_windows_aus(basis) else posixpath - return modul.join(basis, *teile) - - -def _sieht_nach_windows_aus(pfad: str) -> bool: - """`C:\\…` oder irgendein Backslash — dann ist es ein Windows-Pfad.""" - pfad = pfad or "" - return "\\" in pfad or (len(pfad) > 1 and pfad[1] == ":") - - -def ordner_von(datei: str) -> str: - """Der Ordner einer Datei — nach dem Trenner der DATEI (siehe `verbinden`).""" - modul = ntpath if _sieht_nach_windows_aus(datei) else posixpath - return modul.dirname(datei) +# Die drei Pfad-Helfer stehen in `rippy.pfade` — dreimal dieselbe Falle in +# drei Dateien war zweimal zu viel. Hier bleiben nur die Namen, damit die +# Aufrufe unten lesbar bleiben. +verbinden = pfade.verbinden +ordner_von = pfade.ordner_von +_sieht_nach_windows_aus = pfade.ist_windows_pfad def _entfalten(pfad: str, umgebung: dict) -> str: