fix: Pfad-Regeln an EINE Stelle — dreimal dieselbe Falle war zweimal zu viel
Ampel / ampel (push) Successful in 54s
Ampel / ampel (push) Successful in 54s
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b47d21510d
commit
151376aee3
+10
-20
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ""
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) == ""
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user