rechnungstool/pruef_selbstbezug.py
TheMockTv 73042b771c Logo in der Statuszeile, Bau an einer Stelle, Startprobe fuer die EXE
Logo
  Das Programmlogo steht jetzt klein neben "powered by ravokk" - dort sitzt
  die Kennung des Werkzeugs, nicht die des Kunden. Eine eigene Kopfzeile hat
  dieses Programm nicht; oben sind die Reiter.

bauen.py
  Der Weg von der Quelle zur Setup-Datei war eine Folge von Befehlen, die
  jemand von Hand eintippen musste. Wer einen Schritt vergisst, liefert eine
  Setup-Datei mit einer alten EXE aus - und sieht es ihr nicht an. Genau das
  ist heute passiert. Jetzt ist es ein Befehl, die Nummer kommt aus
  version.py, und am Ende stehen die SHA256-Summen fuer den Release-Text.

pruef_start_exe.py
  Prueft, was kein anderer Pruefstand prueft: ob die GEBAUTE EXE ein
  sichtbares Fenster zeigt. Alle uebrigen fahren das Programm aus der Quelle,
  dort trat der Fehler nie auf. "Der Prozess laeuft" beweist dabei nichts -
  er lief ja, waehrend nichts zu sehen war.

Aufgeraeumt
  "Rechnung Campinghof.exe" ist weg; gebaute Dateien gehoeren ins Release,
  nicht ins Repo. Die .spec-Dateien dagegen schon: ohne sie kann niemand die
  EXE nachbauen, die der Updater ausliefert.

Fassung 1.7.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
2026-09-06 20:34:59 +02:00

123 lines
4.2 KiB
Python

# -*- coding: utf-8 -*-
"""Pruefstand: `self` darf nur dort stehen, wo es auch eines gibt.
DER FALL, DER DAZU GEFUEHRT HAT (06.09.2026)
--------------------------------------------
Nach dem Installieren meldete das Programm beim Start:
File "app.py", line 1824, in <module>
NameError: name 'self' is not defined
Im `if __name__ == "__main__"`-Block stand `self.melden_fehler(...)`. Dort gibt
es kein `self` - das Fenster wird ja gerade NICHT gebaut. Beim Umbau auf die
eigenen Meldungsfenster war die Zeile mitgezogen worden.
WARUM DAS KEINE SYNTAXPRUEFUNG FINDET
-------------------------------------
`ast.parse` und `py_compile` pruefen die FORM, nicht die Bedeutung. Ein
undefinierter Name faellt erst auf, wenn die Zeile ausgefuehrt wird - und diese
hier laeuft nur, wenn ein ZWEITES Fenster gestartet wird und sich das erste
nicht nach vorn holen laesst. Also fast nie. Genau deshalb ist sie durch alle
Pruefstaende gekommen.
WAS HIER GEPRUEFT WIRD
----------------------
Jede Verwendung des Namens `self` muss in einer Funktion stehen, die `self` auch
als ersten Parameter hat. Alles andere ist ein Ueberbleibsel aus einer Methode.
Geprueft werden beide Programme - der Fehler war in einem, haette aber genauso
im anderen stecken koennen.
"""
import ast
import io
import os
import sys
ORDNER = [r"C:\claude\rechnungstool", r"C:\claude\beherbergungssteuer"]
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)
def fundstellen(quelltext):
"""Alle Zeilen, in denen `self` steht, ohne dass es eines gibt."""
baum = ast.parse(quelltext)
treffer = []
def erster_parameter(knoten):
args = knoten.args
alle = list(args.posonlyargs) + list(args.args)
return alle[0].arg if alle else None
def durchgehen(knoten, hat_self):
for kind in ast.iter_child_nodes(knoten):
if isinstance(kind, (ast.FunctionDef, ast.AsyncFunctionDef)):
# Eine verschachtelte Funktion in einer Methode darf `self` aus
# der umgebenden Methode benutzen - das ist ein Abschluss und
# voellig richtig. Verfuegbar ist es also, sobald IRGENDEINE
# umgebende Funktion es als ersten Parameter hat.
durchgehen(kind, hat_self or erster_parameter(kind) == "self")
elif isinstance(kind, ast.Lambda):
durchgehen(kind, hat_self)
else:
if isinstance(kind, ast.Name) and kind.id == "self" and not hat_self:
treffer.append(kind.lineno)
durchgehen(kind, hat_self)
durchgehen(baum, False)
return sorted(set(treffer))
# ---- 1. Die Pruefung muss den bekannten Fehler FINDEN -------------------
# Sonst prueft sie nichts und wiegt uns in Sicherheit.
kaputt = (
"class A:\n"
" def m(self):\n"
" self.x = 1\n"
"\n"
'if __name__ == "__main__":\n'
" self.melden_fehler('geht nicht')\n"
)
gefunden = fundstellen(kaputt)
pruefe(gefunden == [6],
f"die Pruefung findet den bekannten Fehler in Zeile 6 (gefunden: {gefunden})")
sauber = (
"class A:\n"
" def m(self):\n"
" self.x = 1\n"
" f = lambda: self.x\n"
" return f\n"
)
pruefe(fundstellen(sauber) == [],
"und meldet sauberen Code nicht faelschlich")
# ---- 2. Beide Programme durchgehen -------------------------------------
for ordner in ORDNER:
for name in sorted(os.listdir(ordner)):
if not name.endswith(".py"):
continue
pfad = os.path.join(ordner, name)
try:
quelle = io.open(pfad, encoding="utf-8").read()
stellen = fundstellen(quelle)
except (OSError, SyntaxError) as e:
pruefe(False, f"{name} liess sich nicht lesen ({e})")
continue
if stellen:
pruefe(False, f"{os.path.basename(ordner)}/{name}: `self` ohne self "
f"in Zeile(n) {stellen}")
pruefe(True, f"{len(ORDNER)} Programme durchgegangen")
if fehler:
print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt")
sys.exit(1)
print("GRUEN | Nirgends steht `self`, wo es keines gibt")