# -*- 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 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 from pruefhelfer import fehler, pruefe ORDNER = [r"C:\claude\rechnungstool", r"C:\claude\beherbergungssteuer"] 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")