Du verstehst, dass Quelltext eine Baumstruktur ist — nicht nur Text — und du kannst diesen Baum mit Python selbst durchwandern. Damit baust du statische Wächter, die ganze Klassen von Bugs vor der Ausführung erkennen: in 300 Millisekunden, ohne dass deine App jemals startet.
Konkret nach Tier 3 dieser Reise: Du schreibst eigenständig einen AST-Wächter für eine Klasse von Bugs, die in deinem Code wiederkehrt, und integrierst ihn als Pre-Commit-Prüfung.
In der iMenu-Entwicklung wurde im SessionsOverlay eine Methode umbenannt:
# Vorher
def _render(self) -> None:
self.query_one("#sessions-list").update(self._format_list())
# Nachher
def _update_display(self) -> None:
self.query_one("#sessions-list").update(self._format_list())
Sechs Aufrufstellen wurden mitgezogen — eine wurde übersehen:
def on_mount(self) -> None:
self.set_interval(2.0, self._render) # ← der vergessene Aufruf
Zur Laufzeit, zwei Sekunden nach dem Öffnen des Overlays:
AttributeError: 'SessionsOverlay' object has no attribute '_render'
Diese Bug-Klasse — „undefiniertes self.method()" — ist mit einem 30-Zeilen-AST-Wächter in 0,3 Sekunden findbar. Du kannst sie nie wieder committen, ohne dass eine CI-Stufe sie meldet. Genau das bauen wir in dieser Reise.
Lies, sieh, erkenne die Struktur.
Pythons Standardbibliothek enthält das Modul ast — Abstract Syntax Tree. Es parst Quelltext nicht in Text, sondern in eine Baumstruktur aus Python-Objekten.
import ast
tree = ast.parse("self.foo()")
print(ast.dump(tree, indent=2))
Ergibt:
Module(
body=[
Expr(
value=Call(
func=Attribute(
value=Name(id='self'),
attr='foo'),
args=[],
keywords=[]))],
type_ignores=[])
Lies das Stück für Stück. Der Ausdruck self.foo() ist:
Call (Funktionsaufruf)func ein Attribute ist (Punkt-Zugriff)value ein Name mit id='self' istattr='foo' der Methodenname istWenn du das siehst, hast du den entscheidenden Begriff verstanden: Quellcode ist bereits eine Datenstruktur — und Python kann sie dir geben.
| Knoten | Was er repräsentiert |
|---|---|
ast.Module |
die ganze Datei |
ast.ClassDef |
eine class Foo:-Deklaration |
ast.FunctionDef |
eine def bar(...):-Deklaration |
ast.Call |
ein Funktionsaufruf wie foo() |
ast.Attribute |
ein Punkt-Zugriff wie self.foo |
ast.Name |
ein Bezeichner wie self, Foo, x |
ast.walk(tree) liefert dir alle Knoten in beliebiger Reihenfolge — perfekt für „finde alle X" Fragen.
Diese Erkenntnis ist die teure: Methoden auf Elternklassen sind in deiner Datei nicht sichtbar.
class MyOverlay(ModalScreen):
def compose(self):
yield Static("hello")
def on_mount(self):
self.dismiss() # ← `dismiss` ist auf `ModalScreen` definiert, nicht hier
Ein naiver Wächter würde self.dismiss() als „undefiniert" melden — Falschmeldung. Du brauchst:
TEXTUAL_ACTIONS, MODAL_SCREEN_METHODS, …),In der iMenu-Suite wurde beides kombiniert.
Schreib das jetzt mit. Datei: tools/guard_undefined_self.py.
import ast
from pathlib import Path
def methods_of(class_node: ast.ClassDef) -> set[str]:
return {
node.name
for node in ast.walk(class_node)
if isinstance(node, ast.FunctionDef)
}
self.X()-Aufrufe einsammelndef self_calls_in(class_node: ast.ClassDef) -> set[str]:
calls = set()
for node in ast.walk(class_node):
if (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Attribute)
and isinstance(node.func.value, ast.Name)
and node.func.value.id == "self"
):
calls.add(node.func.attr)
return calls
TEXTUAL_ACTIONS = {
"dismiss", "push_screen", "pop_screen", "switch_screen",
"query_one", "query", "set_interval", "set_timer",
"mount", "remove", "refresh", "notify",
}
def check_file(path: Path) -> list[str]:
findings = []
tree = ast.parse(path.read_text())
for cls in ast.walk(tree):
if not isinstance(cls, ast.ClassDef):
continue
defined = methods_of(cls)
used = self_calls_in(cls)
undefined = used - defined - TEXTUAL_ACTIONS
for name in undefined:
findings.append(f"{path}:{cls.name}: self.{name}() ist undefiniert")
return findings
if __name__ == "__main__":
import sys
for arg in sys.argv[1:]:
for finding in check_file(Path(arg)):
print(finding)
Lege eine Datei demo.py an:
class SessionsOverlay:
def on_mount(self):
self.set_interval(2.0, self._render) # bewusster Bug
def _update_display(self):
pass
Lauf:
python tools/guard_undefined_self.py demo.py
Erwartete Ausgabe:
demo.py:SessionsOverlay: self._render() ist undefiniert
Du hast jetzt einen Wächter, der genau die Bug-Klasse erkennt, die in iMenu eingeführt wurde. Dauer: ~0,3 Sekunden. Vor jedem Commit aufrufbar. Nie wieder dieser Bug.
Such dir in deinem eigenen Code eine strukturelle Invariante — eine Regel, die sich rein durch Code-Form ausdrücken lässt, ohne den Code zu laufen.
Beispiele aus realen Projekten:
| Bug-Klasse | Strukturelle Invariante |
|---|---|
Doppelte compose()-IDs |
In einer compose-Methode taucht id="x" zweimal auf |
Verwaister watch_<feld> |
Eine Methode watch_foo existiert, aber keine reactive foo |
| Toter Binding-Handler | Binding(...) zeigt auf action_x, aber action_x fehlt |
Vergessenes super().__init__() |
__init__ einer Subklasse ruft nicht super().__init__(...) |
| Verwaiste Imports | from x import y, aber y taucht im Body nie auf |
Deine Übung: Wähle eine. Schreibe sie als AST-Wächter. Lass ihn auf deinem Repo laufen. Berichte, was du findest.
Bonus — als Pre-Commit-Hook einhängen:
# .git/hooks/pre-commit
#!/bin/sh
files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
[ -z "$files" ] && exit 0
python tools/guard_undefined_self.py $files || exit 1
ast — Abstract Syntax Treestests/test_static_guards.py (5 Wächter im Einsatz)_render → _update_display-Umbenennung, die diese Lernreise auslöste