1 Punkte von GN⁺ 2024-08-23 | 1 Kommentare | Auf WhatsApp teilen
  • Python kann mithilfe der magischen Kommentare zur Quellcode-Kodierung in den ersten beiden Zeilen und benutzerdefinierter Codecs Dateiinhalte vor der Ausführung verändern oder durch völlig anderen Code ersetzen
  • Benutzerdefinierte Codecs können während der Initialisierung des Interpreters über die Ausführung von import in einer .pth-Pfadkonfigurationsdatei registriert werden; mit codecs.register wird eine Suchfunktion hinzugefügt
  • Eine Codec-Implementierung benötigt decode(data: bytes) -> tuple[str, int] und einen inkrementellen Decoder; ohne Exception-Handling sieht man statt der eigentlichen Ursache unter Umständen nur SyntaxError: encoding problem: your_codec
  • Inkrement-/Dekrement-Operatoren ++/--, Python mit geschweiften Klammern, die Ausführung von C/C++ über cppyy und die Validierung von TOML mit JSON Schema lassen sich über denselben Einstiegspunkt implementieren
  • Über spielerische Beispiele hinaus kann dies auch für Python-Erweiterungen und Backports wie pythonql, future-typing, future-fstrings und future-annotations genutzt werden; magic_codec reduziert wiederkehrende Arbeit

Die Quellcode-Kodierung als Einstiegspunkt für die Vorverarbeitung nutzen

  • Gemäß PEP-0263 kann in einer der ersten beiden Zeilen einer Python-Datei die Quellcode-Kodierung angegeben werden
    • Beispiel: # coding=utf8, # -*- coding: utf8 -*-, # vim: set fileencoding=utf8 :
  • Die magische Zeile muss zum regulären Ausdruck ^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+) passen
    • Der Codec-Name muss zu [-_.a-zA-Z0-9]+ passen
  • Ein benutzerdefinierter Codec muss die Quelle nicht nur dekodieren, sondern kann den Quellstring verändern und anschließend an den Python-Interpreter übergeben

Codecs mit .pth-Dateien registrieren

  • Wenn der Python-Interpreter ohne die Option -S startet, wird während der Initialisierung das Paket site geladen
  • .pth-Pfadkonfigurationsdateien in site-packages fügen Inhalte, ausgenommen leere Zeilen und Zeilen, die mit # beginnen, zum Modulsuchpfad hinzu
  • Laut Python-Dokumentation werden Zeilen ausgeführt, in denen auf import ein Leerzeichen oder Tab folgt
    • Beispiel: Wenn packagename.pth import packagename.register_codec enthält, wird dieses Modul während der Initialisierung importiert
  • Das importierte Modul kann codecs.register aufrufen, um eine Codec-Suchfunktion zu registrieren
    • Der Import wird nur einmal ausgeführt, daher wird auch die Suchfunktion nur einmal registriert

Einen benutzerdefinierten Codec implementieren

  • Ein benutzerdefinierter Codec benötigt zwei Dinge
    • decode(data: bytes) -> tuple[str, int]
    • Eine Klasse für einen inkrementellen Decoder
  • Die Funktion decode kann mit codecs.utf_8_decode die eigentliche UTF-8-Dekodierung durchführen und den Ergebnisstring an eine Vorverarbeitungsfunktion übergeben
  • Wenn Exceptions innerhalb des Codecs nicht abgefangen werden, wird statt eines normalen Tracebacks unter Umständen nur SyntaxError: encoding problem: your_codec ausgegeben
    • Exceptions aus der Vorverarbeitungsfunktion sollten daher mit traceback.print_exc() direkt ausgegeben und anschließend erneut geworfen werden
  • Der inkrementelle Decoder kann die gesamte Datei in einem Buffer sammeln und die Vorverarbeitung nur einmal beim letzten decode-Aufruf durchführen
    • Eine Beispielimplementierung erbt von codecs.BufferedIncrementalDecoder und verarbeitet in decode(self, data, final=False) nur dann, wenn final gesetzt ist
  • Das Ergebnis der Vorverarbeitung muss nicht zwingend den Inhalt der Originaldatei verwenden; es kann auch vollständig beliebigen Python-Code zurückgeben
    • Allerdings wird die erste Zeile als magische Zeile erwartet und daher entfernt, und das Ergebnis muss gültiges Python sein

Beispiele für Python-Syntaxerweiterungen

  • Inkrement- und Dekrement-Operatoren ++ und --

    • Python hat keine unären Inkrement-/Dekrement-Operatoren
    • x++, x-- sind syntaktisch ungültig
    • ++x, --x sind syntaktisch zwar gültig, werden aber jeweils zu Aufrufen von x.__pos__().__pos__() bzw. x.__neg__().__neg__()
    • Ein Präprozessor kann den Token-Stream so verändern, dass er sich wie ein Inkrement-/Dekrement-Operator verhält
      • x++(x, x := x + 1)[0]
      • x--(x, x := x - 1)[0]
      • ++x(x, x := x + 1)[1]
      • --x(x, x := x - 1)[1]
    • Diese Transformation nutzt den Python-Zuweisungsausdruck, den Walrus Operator
    • Mit bloßer Token-Ersetzung kann dies bei Ausdrücken wie x++ - -y fehlschlagen; mit Klammern wie x++ - (-y) lässt sich die Mehrdeutigkeit verringern
    • incdec.py ersetzt per regulärem Ausdruck, kann aber trotz Versuchen, Ersetzungen innerhalb von String-Literalen zu vermeiden, anfällig sein
    • Eine Implementierung, die den Token-Stream direkt modifiziert, findet sich in magic.incdec
  • Python mit geschweiften Klammern

    • from __future__ import braces löst SyntaxError: not a chance aus
    • Ein Präprozessor kann den Token-Stream verändern und Scopes mit geschweiften Klammern in einrückungsbasiertes Python übersetzen
    • Der Implementierungsablauf sieht folgendermaßen aus
      • Tokens mit tokenize.generate_tokens erzeugen
      • Den Eingabestring zeilenweise über readline von io.StringIO bereitstellen
      • Vorhandene INDENT- und DEDENT-Tokens entfernen
      • Bei { die Einrückungsebene erhöhen und : ausgeben
      • Bei } die Einrückungsebene verringern
      • Nach NL ein INDENT-Token entsprechend der aktuellen Einrückungsebene hinzufügen
    • Um Konflikte mit Python-Dictionary-Literalen zu reduzieren, kann man die Einrückungsebene nur dann anpassen, wenn auf { ein Zeilenumbruch folgt, und } nur dann als schließenden Scope behandeln, wenn davor ein Zeilenumbruch steht
    • Mehrzeilige Dictionaries können mit Backslashes genutzt werden, weil innerhalb der geschweiften Klammern dann kein Newline-Token entsteht

Andere Sprachen als Python ausführen

  • C und C++

    • Bei Sprachen wie Shell-Skripten, CMake-Skripten, PHP oder Ruby, die # für Kommentare verwenden, lässt sich eine magische Kodierungszeile leicht zusammen mit einem Shebang einfügen
    • In C und C++ verwenden Kommentare zwar /* ... */ oder // ..., aber Präprozessor-Direktiven beginnen mit #, sodass sie zum Encoding-Regex passen können
    • Eine beispielhafte magische Zeile ist auch in C/C++-Quellcode gültig und passt zugleich zu Pythons Encoding-Muster
      • #define CODEC "coding:magic.cpp"
    • Mit cppyy kann Python C/C++-Code interpretieren und Bindings erzeugen
      • cppyy verwendet intern cling
    • Das Ergebnis der Vorverarbeitung wird ungefähr zu folgendem Python-Code
      • import cppyy
      • cppyy.cppdef("<input source file content>")
      • from cppyy.gbl import main
      • Aufruf von main() aus C/C++, wenn __name__ == "__main__" gilt
    • Eine Beispielimplementierung findet sich in magic.cpp

Als TOML-Validierungstool verwenden

  • Da TOML Kommentare mit # beginnt, kann eine magische Kodierungszeile wie # coding: magic.toml eingefügt werden
  • Wenn das Ergebnis der Vorverarbeitung durch ein Python-Validierungsskript ersetzt wird, kann der Python-Interpreter wie ein TOML-Validierungstool verwendet werden
  • Das Validierungsbeispiel nutzt folgende Module
    • tomllib, um die TOML-Datei zu lesen
    • json, um die JSON-Schema-Datei zu lesen
    • jsonschema zur Validierung
  • Ausführungsbeispiel:
    • python tests/toml/data_valid.toml -s tests/toml/schema.json
    • Wenn die Datei gültig ist, wird Successfully validated. ausgegeben
  • Im Beispiel mit ungültigem TOML wird ein Validierungsfehler ausgegeben, weil der String '20' im Array scores keine Zahl ist
  • Eine Beispielimplementierung findet sich in magic.toml

Praktische Nutzung und magic_codec

  • Mit benutzerdefinierten Codecs und .pth-Dateien zusammen lässt sich das Verhalten des Python-Interpreters stark verändern
  • Die meisten Beispiele sind zum Spaß gedacht, es gibt aber auch praktische Anwendungsfälle
  • Wer site-packages nicht direkt anfassen oder .pth-Dateien und wiederkehrenden Code nicht selbst schreiben möchte, kann magic_codec verwenden
  • magic_codec-Erweiterungen können als Python-Pakete mit dem Präfix magic_ erstellt werden
    • Wenn der Datei-Codec auf magic_foo gesetzt ist, wird das Paket magic_foo geladen
    • Es wird geprüft, ob dieses Paket eine Funktion preprocess enthält
  • Die erwartete preprocess-Signatur lautet wie folgt
    • def preprocess(data: str) -> str:
  • Erweiterungsbeispiele finden sich unter example/

1 Kommentare

 
GN⁺ 2024-08-23
Hacker-News-Meinungen
  • Führt man from __future__ import braces aus, erscheint die witzige Fehlermeldung SyntaxError: not a chance; diese Formulierung war seit 2001 in CPython fest eincodiert
    https://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
    Der Autor Jeremy Hylton arbeitet heute bei Google als Principal Engineer für die Qualität der KI-Suche. Ziemlich beeindruckend, dass seine Karriere in 24 Jahren von einer scherzhaften Würdigung verbotener Syntax zu einem universellen Abfragesystem geführt hat, das keine eigene Syntax braucht

    • Das erinnert mich daran, dass break rust; im Rust-Compiler einen internen Compilerfehler auslöste. Ich frage mich, wie viele ähnliche Easter Eggs es in anderen Sprachen gibt
    • Ich weiß nicht, warum das überraschend sein soll. 2001 konnte nicht einfach irgendwer etwas in Python einbauen; damals war es ein Nischenprojekt, und die Beitragenden waren klug und engagiert, also war die Wahrscheinlichkeit hoch, dass sie einflussreiche Karrieren machen würden
      Ich halte es für ein Missverständnis, spielerisches, informelles Hobby-Hacking als eine von professioneller Entwicklung getrennte Welt zu betrachten
    • Das waren naive Zeiten. Hylton könnte sich wohl einer Misstrauensabstimmung zugunsten von Tim Peters anschließen
      https://news.ycombinator.com/item?id=41314393
    • Solche Easter Eggs zu sehen ist immer schön. Schade, dass sie nicht mehr so häufig sind wie früher
  • Ich dachte, mit Import-Hooks herumzuspielen sei die kreativste Art, gefeuert zu werden, aber das war naiv. Schade, dass man wegen der Codec-Regex wohl nicht richtig mit Dingen wie μtf8 trollen kann; jetzt bleibt wohl nur noch, mit Import-Hooks, einem Präprozessor und sys.settrace alle Funktionen auf die unmittelbar zuvor aufgerufene Funktion zu monkeypatchen und alle 17 Minuten stdout und stderr zu vertauschen

    • Wie es sich für eine gute Sprache gehört, sollte man auch die Verwendung von geschweiften Klammern erzwingen
  • Es gibt gute Gründe dafür, dass Python Präprozessor-Hooks nicht absichtlich sichtbar macht, und vernünftige Erwachsene sollten sich davon meiner Meinung nach fernhalten
    Andererseits möchte ich mit vernünftigen Erwachsenen nichts zu tun haben. Damit ließe sich vermutlich sehr viel wirklich Lustiges anstellen

    • Python hat die Philosophie, eine Sprache für „einvernehmliche Erwachsene“ (consenting adults) zu sein; deshalb gibt es keine public/private-Sichtbarkeitsmodifikatoren, und vermutlich hat man auch allerlei Metaprogramming-Magie offengelassen. Wenn man sich um „vernünftige Erwachsene“ gesorgt hätte, wären solche Designentscheidungen etwas seltsam gewesen :)
    • Wenn man Menschen daran hindert, das auf einfache und offensichtliche Weise zu tun, werden sie es auf schlimmere, hackigere Weise versuchen
  • Das wirkt praktisch und wirklich nützlich. Wenn ich unsinnige Import-Hacks gebaut habe, habe ich normalerweise ein Modul importiert, den Code mit dem ast-Modul umgeschrieben, ihn per exec ausgeführt und dann ein exit() eingeschoben; mit einem Präprozessor wäre das viel angenehmer zu benutzen
    Bevor alle dicts die Reihenfolge garantiert haben, habe ich AST-Rewriting hauptsächlich genutzt, um Listenliterale in Ordered-Dict-Aufrufe umzuwandeln, und das war tatsächlich nützlich
    Ich mag die Flexibilität von Python. Das Verfluchteste, was ich je getan habe, war, Strings in-place zu verändern, und am Ende habe ich sogar mmap missbraucht, um ein Skript zu schreiben, das sich selbst verändert. Jetzt habe ich das Gefühl, als Producer einen Lisp-Interpreter schreiben zu müssen

    • „Strings in-place verändern“? Strings sind unveränderlich. Hast du mit etwas wie ctypes direkt an Speicheradressen geschrieben?
  • Der beste Anwendungsfall, den ich bisher gefunden habe, ist das von JSX inspirierte pyxl: https://github.com/dropbox/pyxl
    Damit kann man solchen Code schreiben
    # coding: pyxl
    print Hello World!

  • Ich frage mich, ob man damit den Übergang von Python 2 zu 3 besser hätte bewältigen können. Zum Beispiel so, dass # coding: six.python2 Python-2-Code in gültigen Python-3-Code überführt oder # coding: six.python3 Python-3-Code so umwandelt, dass er unter Python 2 läuft
    Auch das Hinzufügen oder Entfernen von Präfixen wie b"..." oder u"..." wäre wohl möglich gewesen

    • Es hätte helfen können, aber die Teile, bei denen es hilft, sind die einfachen. Die Schwierigkeiten beim Wechsel von Python 2 zu 3 lagen in den Änderungen des Laufzeitverhaltens. In Python 2 verhielten sich Unicode mit ASCII-Inhalt und normale Strings wie derselbe String; als dict-Keys verwendet, zeigten sie daher auf denselben Eintrag. In Python 3 zeigen bytes und str mit demselben ASCII-Inhalt innerhalb desselben dict auf unterschiedliche Einträge
      Es gibt noch kniffligere Änderungen. Viele eingebaute Funktionen wie .keys() und .values() geben in Python 2 Listen zurück, in Python 3 dagegen Iteratoren. Wenn man Code mit six-Utilities oder anderen Workarounds sicher übersetzen will, wird er sehr ausschweifend, weil die meisten Werte nur einmal, manche aber gelegentlich zweimal verwendet werden
      Wenn es ein Tool gibt, das Code beim Import umschreiben kann, wäre es meiner Meinung nach besser, den transformierten Code zu committen und schrittweise aufzuräumen. Der schwierige Teil sind Verhaltensänderungen wie str gegenüber bytes, die sich auf weit entfernte Codebereiche auswirken
  • Werden Abhängigkeiten, die durch diese Coding-Hook-Strategie entstehen, von pip freeze oder uv erkannt?
    Wenn nicht, dürfte das lustig werden :). Wenn jemand so etwas eingebaut hat, ist fast garantiert, dass es noch weitere Fallstricke gibt; statt gegen solche Drachen zu kämpfen, wäre es wahrscheinlich einfacher, die Bibliothek neu zu schreiben

  • Es wäre ziemlich witzig, Pseudocode-Python zu bauen und es von einem LLM decodieren zu lassen. Natürlich wäre das furchtbar, aber Spaß machen würde es wohl