Zwei echte Fehler aus der Pruefkette behoben

## 1. Der Temp-Schutz war geschrieben, aber nicht angeschlossen

_ist_temp() stand seit heute Vormittag in ablage.py, ausfuehrlich begruendet
("Ein Merkzettel, der dorthin zeigt, wird deshalb verworfen") - und wurde von
NIEMANDEM gerufen. Der Schutz war ein Kommentar, keine Massnahme.

Zwei Pruefagenten haben das unabhaengig voneinander gefunden, einer mit
Messung: das Journal landete weiterhin im Temp-Ordner. Windows raeumt den von
selbst auf; es geht um aufbewahrungspflichtige Aufzeichnungen (§ 147 AO).

⚠️ Das ist die schlimmste Sorte Fehler - ein Kommentar, der eine
Schutzmassnahme zusichert, die es nicht gibt. Wer die Datei liest, haelt die
Falle fuer geschlossen. Und ich hatte sie dem Auftraggeber als behoben
gemeldet.

Behoben: _gemerkter_ordner() prueft jetzt wirklich.
pruef_temp_merkzettel.py sichert es ab - er prueft nicht, ob die FUNKTION
existiert, sondern ob sie WIRKT. Dazu die Gegenprobe, dass ein GUELTIGER
Merkzettel weiter befolgt wird: sonst haette der Schutz jemandem sein
Netzlaufwerk weggenommen.

⚠️ Im ersten Anlauf war dieser Pruefstand rot - weil ich seine Spielwiese mit
tempfile.mkdtemp() angelegt hatte und die selbst im Temp liegt. Wer das
Verhalten IM Temp-Ordner prueft, darf seine Spielwiese nicht dort aufbauen.

## 2. Der Darstellungsdialog zerstoerte sich beim ersten Klick selbst

Gleiche Ursache und gleiche Behebung wie im Rechnungstool: _oberflaeche_neu()
loeschte alle Kinder des Hauptfensters, und ein tk.Toplevel ist eines.
pruef_darstellung drueckt jetzt den Radioknopf statt cfg von Hand zu setzen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
TheMockTv 2026-09-07 18:10:41 +02:00
parent dcc556217b
commit 039a23478f
4 changed files with 190 additions and 1 deletions

View file

@ -103,6 +103,14 @@ def _gemerkter_ordner():
if not pfad or "\x00" in pfad or not os.path.isabs(pfad):
log.warning("Merkzettel unbrauchbar - es wird neu angelegt")
return None, None
# HIER wird _ist_temp tatsaechlich benutzt. Bis zum 07.09.2026 stand die
# Funktion samt Begruendung da und wurde von niemandem gerufen - der
# Schutz war ein Kommentar, keine Massnahme. Zwei Pruefagenten haben das
# unabhaengig gefunden, einer mit Messung: das Journal landete weiter im
# Temp-Ordner.
if _ist_temp(pfad):
log.warning("Merkzettel zeigt in den Temp-Ordner - verworfen: %s", pfad)
return None, None
try:
laufwerk = os.path.splitdrive(pfad)[0]
erreichbar = (not laufwerk) or os.path.exists(

13
app.py
View file

@ -216,8 +216,19 @@ class App(theme.ThemeMixin, tk.Tk):
except tk.TclError:
pass
# Fremde Fenster stehen lassen! Ein tk.Toplevel IST ein Kind des
# Hauptfensters - der offene Darstellungsdialog wurde hier bisher
# mitgeloescht, und die Zeile danach fasste eine Leiche an
# ("invalid command name .!toplevel"). Gefunden am 07.09.2026 von der
# Pruefkette, mit dem echten Bedienweg gemessen: Klick auf "Dunkel"
# -> Farbe wechselt, Fenster ist weg, Traceback im Log.
#
# Der Pruefstand hat es nicht gefunden, weil er cfg["darstellung"] von
# Hand setzte und _oberflaeche_neu() direkt rief - also alles ausser
# dem Weg, den ein Mensch geht.
for kind in self.winfo_children():
kind.destroy()
if not isinstance(kind, tk.Toplevel):
kind.destroy()
self._apply_theme()
self._menue()

View file

@ -178,6 +178,66 @@ pruefe(d.winfo_rooty() >= f.winfo_rooty() - 40,
d.destroy()
pumpen(0.3)
# ---- Der Dialog muss den Klick UEBERLEBEN -------------------------------
# Gefunden am 07.09.2026 von der Pruefkette: _oberflaeche_neu() loeschte alle
# Kinder des Hauptfensters - und ein Toplevel IST ein Kind. Der offene Dialog
# wurde also beim ersten Klick mitgeloescht, die Zeile danach fasste eine
# Leiche an ("invalid command name .!toplevel").
#
# Dieser Pruefstand hat es NICHT gefunden, weil er cfg["darstellung"] von Hand
# setzte. Hier wird jetzt der Radioknopf gedrueckt - derselbe Weg, den ein
# Mensch geht.
d = f.dlg_darstellung()
pumpen(1.0)
def radioknoepfe(fenster):
"""Alle Radiobuttons unterhalb eines Fensters, mit ihrer Beschriftung."""
treffer = []
def durchgehen(w):
for kind in w.winfo_children():
if kind.winfo_class() == "TRadiobutton":
try:
treffer.append((str(kind.cget("text")), kind))
except tk.TclError:
pass
durchgehen(kind)
durchgehen(fenster)
return treffer
knoepfe = radioknoepfe(d)
pruefe(len(knoepfe) == len(theme.MODI),
f"der Dialog zeigt alle {len(theme.MODI)} Moeglichkeiten "
f"({[t for t, _ in knoepfe]})")
ziel = next((w for t, w in knoepfe if t == theme.MODUS_NAMEN["dunkel"]), None)
pruefe(ziel is not None, "der Knopf 'Dunkel' ist da")
if ziel is not None:
ziel.invoke() # der echte Klick
pumpen(1.2)
pruefe(bool(d.winfo_exists()),
"der Dialog lebt nach dem Klick noch")
pruefe(f._dark_mode is True,
f"und die Darstellung ist umgestellt (Hintergrund {f.col_bg})")
# Und noch einmal, denn genau das war vorher unmoeglich:
hell = next((w for t, w in knoepfe if t == theme.MODUS_NAMEN["hell"]), None)
if hell is not None:
try:
hell.invoke()
pumpen(1.0)
pruefe(f._dark_mode is False,
"ein ZWEITER Klick im selben Dialog wirkt auch")
except tk.TclError as e:
pruefe(False, f"der zweite Klick scheiterte ({e})")
try:
d.destroy()
except tk.TclError:
pass
pumpen(0.3)
# ---- Zurueck auf automatisch --------------------------------------------
f.cfg["darstellung"] = "auto"
f._oberflaeche_neu()

110
pruef_temp_merkzettel.py Normal file
View file

@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
"""Pruefstand: ein Merkzettel in den Temp-Ordner wird verworfen.
DER FALL, DER DAZU GEFUEHRT HAT (07.09.2026)
--------------------------------------------
Die installierte Fassung fuehrte ihr Journal in
`%TEMP%/abnahme_bst/Daten` - haengengeblieben aus einem Abnahmelauf vom 03.09.
Windows raeumt den Temp-Ordner von selbst auf (Datentraegerbereinigung,
Speicheroptimierung); das Journal waere ohne Vorwarnung verschwunden. Es geht
um aufbewahrungspflichtige Aufzeichnungen, zehn Jahre (§ 147 AO).
Ich habe daraufhin `_ist_temp()` geschrieben, ausfuehrlich begruendet - und
NICHT ANGESCHLOSSEN. Die Funktion stand da und wurde von niemandem gerufen;
der Schutz war ein Kommentar, keine Massnahme. Zwei Pruefagenten haben das
unabhaengig voneinander gefunden, einer davon mit einer Messung, die zeigte,
dass das Journal weiterhin im Temp landet.
⚠️ Das ist die schlimmste Sorte Fehler: ein Kommentar, der eine Schutzmassnahme
zusichert, die es nicht gibt. Wer die Datei liest, haelt die Falle fuer
geschlossen. Deshalb dieser Pruefstand - er prueft nicht, ob die FUNKTION
existiert, sondern ob sie WIRKT.
Angefasst wird nichts Echtes: LOCALAPPDATA zeigt waehrend des Laufs auf einen
eigenen Ordner, der danach wieder verschwindet.
⚠️ Dieser Ordner liegt AUSSERHALB des Temp-Ordners - im ersten Anlauf hatte
ich ihn mit tempfile.mkdtemp() angelegt, und der liegt selbst im Temp. Dann
erkennt _ist_temp auch die Vergleichspfade als Temp und der Pruefstand meldet
Fehler, die es nicht gibt. Wer das Verhalten IM Temp-Ordner prueft, darf seine
Spielwiese nicht dort aufbauen.
"""
import importlib
import os
import shutil
import sys
import tempfile
HIER = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HIER)
fehler = []
def pruefe(bedingung, text):
zeile = (" OK | " if bedingung else "FEHLER | ") + text
kod = sys.stdout.encoding or "utf-8"
print(zeile.encode(kod, "replace").decode(kod))
if not bedingung:
fehler.append(text)
echtes_lad = os.environ.get("LOCALAPPDATA", "")
spielwiese = os.path.join(os.path.expanduser("~"), ".pruef_merkzettel")
shutil.rmtree(spielwiese, ignore_errors=True)
os.makedirs(spielwiese, exist_ok=True)
os.environ["LOCALAPPDATA"] = spielwiese
# Als EXE tun, sonst nimmt ablage.py den Quellordner und der Merkzettel
# spielt gar keine Rolle.
sys.frozen = True
sys.argv = [os.path.join(spielwiese, "Steuerrechnungstool.exe")]
import ablage # noqa: E402
importlib.reload(ablage)
# ---- 1. Die Funktion erkennt einen Temp-Pfad ---------------------------
temp = tempfile.gettempdir()
pruefe(ablage._ist_temp(os.path.join(temp, "abnahme_bst", "Daten")),
"ein Pfad im Temp-Ordner wird als solcher erkannt")
pruefe(not ablage._ist_temp(os.path.join(spielwiese, "ravokk")),
"ein normaler Pfad nicht")
# ---- 2. Und sie WIRKT - das ist der Punkt ------------------------------
# Genau so sah der Merkzettel bei ihm aus.
falscher = os.path.join(temp, "abnahme_bst_pruefstand", "Daten")
os.makedirs(falscher, exist_ok=True)
zeiger = ablage._zeiger_datei()
os.makedirs(os.path.dirname(zeiger), exist_ok=True)
with open(zeiger, "w", encoding="utf-8") as f:
f.write(falscher)
gewaehlt = ablage.daten_ordner()
print(f" (Merkzettel zeigte auf {falscher})")
print(f" (gewaehlt wurde {gewaehlt})")
pruefe(not ablage._ist_temp(gewaehlt),
f"der Datenordner liegt NICHT im Temp-Ordner ({gewaehlt})")
pruefe(os.path.normcase(gewaehlt) != os.path.normcase(falscher),
"dem Merkzettel wurde nicht gefolgt")
pruefe("ravokk" in gewaehlt.lower(),
f"stattdessen der Ordner der Firma ({gewaehlt})")
# ---- 3. Ein GUELTIGER Merkzettel wird weiter befolgt -------------------
# Der Schutz darf nicht dazu fuehren, dass jemand seinen bewusst gewaehlten
# Ordner verliert - etwa ein Netzlaufwerk.
gewollt = os.path.join(spielwiese, "woanders", "Daten")
os.makedirs(gewollt, exist_ok=True)
with open(zeiger, "w", encoding="utf-8") as f:
f.write(gewollt)
pruefe(os.path.normcase(ablage.daten_ordner()) == os.path.normcase(gewollt),
"ein gueltiger Merkzettel wird weiter befolgt")
os.environ["LOCALAPPDATA"] = echtes_lad
shutil.rmtree(spielwiese, ignore_errors=True)
shutil.rmtree(os.path.dirname(falscher), ignore_errors=True)
if fehler:
print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt")
sys.exit(1)
print("GRUEN | Der Temp-Schutz ist angeschlossen und wirkt")