Le préprocesseur de Python
(pydong.org)- Python peut utiliser les deux premières lignes de commentaire magique d’encodage source et un codec personnalisé pour transformer le contenu d’un fichier avant son exécution, voire le remplacer entièrement par un autre code
- Un codec personnalisé peut être enregistré pendant l’initialisation de l’interpréteur via l’exécution d’un
importdans un fichier.pthde configuration de chemin, puis en ajoutant une fonction de recherche aveccodecs.register - L’implémentation d’un codec nécessite
decode(data: bytes) -> tuple[str, int]et un décodeur incrémental ; sans gestion des exceptions, on risque de ne voir queSyntaxError: encoding problem: your_codecau lieu de la cause réelle - Le même point d’entrée permet d’implémenter des opérateurs d’incrément/décrément
++/--, un Python à accolades, l’exécution de C/C++ viacppyy, ou même une validation de TOML par JSON Schema - Au-delà des exemples ludiques, cela peut aussi servir à des extensions ou backports de Python comme
pythonql,future-typing,future-fstringsetfuture-annotations;magic_codecaide à réduire le travail répétitif
Utiliser l’encodage source comme point d’entrée de prétraitement
- Selon la PEP-0263, on peut indiquer l’encodage du code source sur l’une des deux premières lignes d’un fichier Python
- Ex. :
# coding=utf8,# -*- coding: utf8 -*-,# vim: set fileencoding=utf8 :
- Ex. :
- La ligne magique doit correspondre à l’expression régulière
^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)- Le nom du codec doit donc correspondre à
[-_.a-zA-Z0-9]+
- Le nom du codec doit donc correspondre à
- Un codec personnalisé ne se limite pas à décoder la source : il peut modifier la chaîne source avant de la transmettre à l’interpréteur Python
Enregistrer un codec via un fichier .pth
- Quand l’interpréteur Python démarre sans l’option
-S, le packagesiteest chargé pendant l’initialisation - Les fichiers
.pthde configuration de chemin danssite-packagesajoutent leur contenu au chemin de recherche des modules, sauf les lignes vides et celles qui commencent par# - D’après la documentation Python, les lignes qui commencent par
importsuivi d’un espace ou d’une tabulation sont exécutées- Par exemple, si
packagename.pthcontientimport packagename.register_codec, ce module sera importé pendant l’initialisation
- Par exemple, si
- Le module importé peut appeler
codecs.registerpour enregistrer une fonction de recherche de codec- Comme l’import n’est exécuté qu’une seule fois, la fonction de recherche n’est elle aussi enregistrée qu’une seule fois
Implémenter un codec personnalisé
- Un codec personnalisé a besoin de deux éléments
decode(data: bytes) -> tuple[str, int]- une classe de décodeur incrémental
- La fonction
decodepeut utilisercodecs.utf_8_decodepour effectuer le vrai décodage UTF-8, puis transmettre la chaîne obtenue à une fonction de prétraitement - Si une exception interne au codec n’est pas interceptée, Python peut n’afficher que
SyntaxError: encoding problem: your_codecau lieu d’un traceback normal- Mieux vaut donc afficher explicitement l’exception avec
traceback.print_exc()dans la fonction de prétraitement avant de la relancer
- Mieux vaut donc afficher explicitement l’exception avec
- Le décodeur incrémental peut accumuler le fichier entier en mémoire puis n’effectuer le prétraitement qu’au dernier appel à
decode- L’exemple montré hérite de
codecs.BufferedIncrementalDecoderet ne traite les données dansdecode(self, data, final=False)que lorsquefinalvaut vrai
- L’exemple montré hérite de
- Le résultat du prétraitement n’a pas besoin de reprendre le contenu original du fichier et peut même renvoyer un code Python entièrement arbitraire
- En revanche, la première ligne est supprimée car elle est supposée être la ligne magique, et le résultat doit être du Python valide
Exemples d’extension de la syntaxe Python
-
Opérateurs d’incrément et de décrément
++et--- Python ne possède pas d’opérateur unaire d’incrément/décrément
x++etx--ne sont pas syntaxiquement valides++xet--xsont valides du point de vue syntaxique, mais appellent respectivementx.__pos__().__pos__()etx.__neg__().__neg__()- Un préprocesseur peut modifier le flux de tokens pour leur donner le comportement d’opérateurs d’incrément/décrément
x++→(x, x := x + 1)[0]x--→(x, x := x - 1)[0]++x→(x, x := x + 1)[1]--x→(x, x := x - 1)[1]
- Cette transformation utilise l’opérateur morse (walrus operator), c’est-à-dire l’expression d’affectation de Python
- Une simple substitution de tokens peut échouer sur une expression comme
x++ - -y; on peut réduire l’ambiguïté avec des parenthèses, par exemplex++ - (-y) - incdec.py effectue ce remplacement par expressions régulières, mais cela peut rester fragile même en essayant d’éviter les substitutions dans les littéraux de chaîne
- Une implémentation qui modifie directement le flux de tokens est disponible dans magic.incdec
-
Python à accolades
from __future__ import bracesprovoqueSyntaxError: not a chance- Un préprocesseur peut modifier le flux de tokens pour transformer une portée à accolades en Python basé sur l’indentation
- Le déroulement de l’implémentation est le suivant
- Génération des tokens avec
tokenize.generate_tokens - Fourniture de la chaîne d’entrée ligne par ligne via
readlinedeio.StringIO - Suppression des tokens
INDENTetDEDENTexistants - À la rencontre de
{, augmentation du niveau d’indentation et émission de: - À la rencontre de
}, diminution du niveau d’indentation - Après
NL, ajout d’un tokenINDENTcorrespondant au niveau d’indentation courant
- Génération des tokens avec
- Pour limiter les conflits avec les littéraux dictionnaire de Python, on peut ne modifier le niveau d’indentation que si
{est suivi d’un saut de ligne, et ne traiter}comme fermeture de portée que s’il est précédé d’un saut de ligne - Les dictionnaires sur plusieurs lignes restent utilisables avec un antislash, car cela évite la génération de tokens de saut de ligne à l’intérieur des accolades
Exécuter d’autres langages via Python
-
C et C++
- Les langages qui utilisent
#pour les commentaires, comme les scripts shell, les scripts CMake, PHP ou Ruby, permettent facilement d’ajouter une ligne magique d’encodage avec un shebang - En C et C++, les commentaires s’écrivent
/* ... */ou// ..., mais les directives du préprocesseur commencent par#, ce qui permet de correspondre à l’expression régulière d’encodage - La ligne magique d’exemple est valide à la fois en C/C++ et pour le motif d’encodage de Python
#define CODEC "coding:magic.cpp"
- Avec cppyy, Python peut interpréter du code C/C++ et générer des bindings
cppyys’appuie en interne surcling
- Le résultat du prétraitement devient grosso modo le code Python suivant
import cppyycppyy.cppdef("<input source file content>")from cppyy.gbl import main- appel à
main()du code C/C++ lorsque__name__ == "__main__"
- Une implémentation d’exemple est disponible dans magic.cpp
- Les langages qui utilisent
L’utiliser comme outil de validation TOML
- TOML utilise
#pour les commentaires, donc on peut y ajouter une ligne magique d’encodage comme# coding: magic.toml - Si le résultat du prétraitement est converti en script Python de validation, l’interpréteur Python peut alors servir d’outil de validation TOML
- L’exemple de validation utilise les modules suivants
tomllibpour lire le fichier TOMLjsonpour lire le fichier JSON Schema- jsonschema pour effectuer la validation
- Exécution :
python tests/toml/data_valid.toml -s tests/toml/schema.json- Si tout est valide, le programme affiche
Successfully validated.
- Dans un exemple de TOML invalide, une erreur de validation signale que la chaîne
'20'dans le tableauscoresn’est pas un nombre - Une implémentation d’exemple est disponible dans magic.toml
Cas d’usage réels et magic_codec
- En combinant un codec personnalisé et un fichier
.pth, on peut modifier en profondeur le comportement de l’interpréteur Python - La plupart des exemples sont là pour le plaisir, mais il existe aussi des usages concrets
- pythonql : extension de langage de requêtes pour Python
- future-typing : backport des annotations de type génériques et de la syntaxe d’union
|vers Python 3.6+ - future-fstrings
- future-annotations
- Si l’on veut éviter de modifier directement
site-packagesou d’écrire soi-même les fichiers.pthet le code répétitif, on peut utiliser magic_codec - Les extensions
magic_codecpeuvent être créées sous forme de packages Python préfixés parmagic_- Si on définit le codec du fichier sur
magic_foo, le packagemagic_fooest chargé - Le système vérifie ensuite si ce package expose une fonction
preprocess
- Si on définit le codec du fichier sur
- La signature attendue pour
preprocessest la suivantedef preprocess(data: str) -> str:
- Des exemples d’extension sont disponibles dans example/
1 commentaires
Avis sur Hacker News
Le message d’erreur amusant
SyntaxError: not a chance, qui apparaît quand on exécutefrom __future__ import braces, était codé en dur dans CPython depuis 2001https://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
Son auteur, Jeremy Hylton, travaille aujourd’hui chez Google comme Principal Engineer en charge de la qualité de la recherche IA ; c’est assez impressionnant de voir, en 24 ans, une carrière passer d’une célébration facétieuse d’une syntaxe interdite à des systèmes de requête universels qui n’ont pas besoin de syntaxe dédiée
break rust;, qui provoquait une erreur interne du compilateur dans le compilateur Rust. Je me demande combien d’easter eggs similaires existent dans d’autres langagesJe pense que c’est une idée reçue de croire que le hacking ludique et informel en amateur serait un monde séparé du vrai développement professionnel
https://news.ycombinator.com/item?id=41314393
Je pensais que jouer avec les import hooks était la façon la plus créative de se faire virer, mais c’était naïf. C’est dommage : à cause de la regex des codecs, on ne pourra sans doute pas troller correctement avec des trucs comme
μtf8. Il ne reste donc plus qu’à utiliser un import hook, un préprocesseur etsys.settracepour monkey-patcher chaque fonction avec celle appelée juste avant, et échanger stdout et stderr toutes les 17 minutesIl y a de bonnes raisons pour lesquelles Python n’expose pas volontairement de hook de préprocesseur, et c’est quelque chose dont des adultes raisonnables devraient, à mon avis, se tenir éloignés
Mais d’un autre côté, je n’ai pas envie de fréquenter des adultes raisonnables. On pourrait faire beaucoup de choses vraiment amusantes
public/private, et sans doute aussi l’ouverture à toutes sortes de magie de métaprogrammation. S’ils s’étaient inquiétés des « adultes raisonnables », ces choix de conception auraient été un peu étranges :)Ça a l’air pratique et vraiment utile. Quand je faisais des hacks d’import absurdes, j’importais généralement le module, je réécrivais le code avec le module
ast, je faisais unexec, puis j’insérais unexit(); avec un préprocesseur, ce serait beaucoup plus agréable à utiliserAvant que tous les
dictne garantissent l’ordre, je m’en servais surtout pour transformer, via réécritureast, des littéraux de liste en appels à des ordered dict, et c’était réellement utileJ’aime la flexibilité de Python. La chose la plus maudite que j’aie faite a été de modifier des chaînes en place, et j’ai fini par abuser de
mmappour écrire un script qui se modifie lui-même. J’ai l’impression que je devrais maintenant écrire un interpréteur Lisp comme producerctypes?Le meilleur cas d’usage que j’aie trouvé jusqu’ici est pyxl, inspiré de JSX : https://github.com/dropbox/pyxl
On peut écrire du code comme ceci
# coding: pyxlprint Hello World!Je me demande si cela aurait pu servir à mieux gérer la transition de Python 2 vers Python 3. Par exemple,
# coding: six.python2pourrait adapter du code Python 2 pour en faire du code Python 3 valide, ou# coding: six.python3transformer du code Python 3 pour qu’il tourne sous Python 2Il aurait peut-être aussi été possible d’ajouter ou de supprimer des préfixes
b"..."ouu"..."dict, elles pointaient vers la même entrée ; en Python 3, desbyteset unstrayant le même contenu ASCII pointent vers des entrées différentes dans le mêmedictIl y a aussi des changements plus subtils. Plusieurs fonctionnalités intégrées, comme
.keys()et.values(), renvoient des listes en Python 2 mais des itérateurs en Python 3. Si l’on essaie de traduire le code de façon sûre avec les utilitairessixou d’autres contournements, le code devient très verbeux, parce que la plupart des valeurs ne sont utilisées qu’une fois, mais parfois deuxSi l’on dispose d’un outil capable de réécrire le code au moment de l’import, je pense qu’il vaut mieux committer directement le code transformé et le nettoyer progressivement. La partie difficile, ce sont les changements de comportement qui se propagent à du code éloigné, comme
strcontrebytesLes dépendances créées par cette stratégie de coding hook, est-ce que
pip freezeouuvles détectent ?Sinon, ça promet de bons moments :). Si quelqu’un a glissé un truc comme ça, on peut presque être sûr qu’il y a d’autres pièges ; plutôt que de combattre ce genre de dragons, il serait probablement plus simple de réécrire la bibliothèque
Ce serait assez drôle de créer un pseudo-code Python et de le faire décoder par un LLM. Ce serait évidemment horrible, mais sans doute amusant