Bon, d'abord il faut savoir que les AST, se situent dans le module standard ast
Commençons par isoler tous les noeuds possibles des AST. C'est-à-dire toutes les classes du module ast
qui héritent de ast.AST
:
| >>> AST_CLASSES = {
... name: cls for name, cls in ast.__dict__.items()
... if isinstance(cls, type) and issubclass(cls, ast.AST)
... }
|
Si vous voulez une rapide aide sur ces classes : for key, cls in AST_CLASSES: print(key, ":\n", cls.__doc__, "\n")
.
Maintenant comment pouvez-vous faire pour savoir quelle tête ils doivent avoir ?
Sachez que :
ast.parse(CODE)
produit un ast à partir d'un code Python contenu dans une chaîne de caractères,
ast.dump(AST)
"dumpe" la représentation d'un ast,
| >>> ast.dump(ast.parse("print('Hello, world!')"))
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hello, world!')], keywords=[]))])"
|
C'est assez touffu pour un hello world, je suis d'accord, mais ceci est l'AST d'un programme complet. Représentons-le sous la forme de dictionnaires et de listes (un json, quoi)…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | >>> my_ast = {
... "type": "Module",
... "body": [
... {
... "type": "Expr",
... "value": {
... "type": "Call",
... "func": {
... "type": "Name",
... "id": "print",
... "ctx": {"type": "Load"}
... },
... "args": [
... {
... "type": "Str",
... "s": "Hello, World!"
... },
... ],
... "keywords": []
... },
... }
... ]
... }
|
Il ne nous reste plus qu'à interpréter ce dictionnaire pour reconstituer l'AST Python. Ça peut se faire avec un peu de magie noire.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | >>> from functools import singledispatch
>>> @singledispatch
... def deserialize(struct):
... return struct
...
>>> @deserialize.register(dict)
... def _(struct):
... clsname = struct.pop("type", None)
... if not clsname: # Ceci n'est pas un noeud AST
... return struct
... kwargs = {key: deserialize(val) for key, val in struct.items()}
... cls = AST_CLASSES[clsname]
... if issubclass(cls, ast.stmt) or issubclass(cls, ast.expr):
... kwargs.setdefault("lineno", 1)
... kwargs.setdefault("col_offset", 0)
... return cls(**kwargs)
...
>>> @deserialize.register(list)
... def _(struct):
... return [deserialize(elt) for elt in struct]
...
>>> tmp = deserialize(my_ast)
>>> ast.dump(tmp)
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hello, World!')], keywords=[]))])"
|
Le hack à propos de "lineno" et "col_offset" est là parce qu'en principe, toutes les expressions et les instructions (expr, stmt) en Python doivent être associé(e)s à un numéro de ligne et une position dans la ligne. Ici, il n'y en a pas puisqu'on produit un AST "à poil", donc je mets les valeurs par défaut : ligne 1, colonne 0.
Maintenant, il vous suffit de charger ce dictionnaire à partir d'un fichier json (avec le module standard json
) ou d'un yaml (avec le package PyYaml
) pour charger un AST depuis n'importe quel fichier.
Mais comment l'exécuter ?
La builtin compile
produit un code object à partir d'un AST :
| >>> code = compile(tmp, 'hello world', 'exec')
>>> code
<code object <module> at 0x7fda655659c0, file "hello world", line 1>
|
Ce code dispose de quelques attributs. Vous pouvez par exemple en extraire le bytecode, ou bien l'exécuter directement :
| >>> exec(code)
Hello, World!
|
Voilà. Ça devrait faire un bon début.