Analyser un fichier .py, lire l'AST, le modifier, puis réécrire le code source modifié

168

Je souhaite modifier le code source Python par programmation. Fondamentalement, je veux lire un .pyfichier, générer l' AST , puis réécrire le code source python modifié (c'est-à-dire un autre .pyfichier).

Il existe des moyens d'analyser / compiler le code source python à l'aide de modules python standard, tels que astou compiler. Cependant, je ne pense pas qu'aucun d'entre eux prenne en charge les moyens de modifier le code source (par exemple, supprimez cette déclaration de fonction), puis de réécrire le code source python de modification.

MISE À JOUR: La raison pour laquelle je veux faire cela est que j'aimerais écrire une bibliothèque de tests de mutation pour python, principalement en supprimant des instructions / expressions, en réexécutant des tests et en voyant ce qui se brise.

Rory
la source
4
Obsolète depuis la version 2.6: le package du compilateur a été supprimé dans Python 3.0.
dfa le
1
Que ne pouvez-vous pas modifier la source? Pourquoi ne pouvez-vous pas écrire un décorateur?
S.Lott
3
Sainte vache! Je voulais créer un testeur de mutation pour python en utilisant la même technique (en particulier en créant un plugin de nez), envisagez-vous de l'open sourcing?
Ryan
2
@Ryan Ouais, je vais ouvrir tout ce que je crée. Nous devrions rester en contact à ce sujet
Rory
1
Certainement, je vous ai envoyé un e-mail via Launchpad.
Ryan

Réponses:

73

Pythoscope le fait pour les cas de test qu'il génère automatiquement, tout comme l' outil 2to3 pour python 2.6 (il convertit la source python 2.x en source python 3.x).

Ces deux outils utilisent la bibliothèque lib2to3 qui est une implémentation de la machinerie de l'analyseur / compilateur python qui peut conserver les commentaires dans la source lorsqu'il est déclenché à partir de la source -> AST -> source.

Le projet de corde peut répondre à vos besoins si vous souhaitez faire plus de refactoring comme des transformations.

Le module ast est votre autre option, et il existe un exemple plus ancien de la façon de "désanalyser" les arbres de syntaxe dans le code (en utilisant le module parser). Mais le astmodule est plus utile lors d'une transformation AST sur du code qui est ensuite transformé en objet code.

Le projet redbaron peut également convenir (ht Xavier Combelle)

Ryan
la source
5
l'exemple non analysé est toujours maintenu, voici la version py3k mise à jour: hg.python.org/cpython/log/tip/Tools/parser/unparse.py
Janus Troelsen
2
En ce qui concerne le unparse.pyscript - il peut être très fastidieux de l'utiliser à partir d'un autre script. Mais, il existe un package appelé astunparse ( sur github , sur pypi ) qui est essentiellement une version correctement empaquetée de unparse.py.
mbdevpl
Pourriez-vous peut-être mettre à jour votre réponse en ajoutant le parso comme option préférée? C'est très bon et mis à jour.
boîte
59

Le module ast intégré ne semble pas avoir de méthode pour revenir à la source. Cependant, le module codegen fournit ici une jolie imprimante pour l'ast qui vous permettrait de le faire. par exemple.

import ast
import codegen

expr="""
def foo():
   print("hello world")
"""
p=ast.parse(expr)

p.body[0].body = [ ast.parse("return 42").body[0] ] # Replace function body with "return 42"

print(codegen.to_source(p))

Cela imprimera:

def foo():
    return 42

Notez que vous risquez de perdre la mise en forme exacte et les commentaires, car ils ne sont pas conservés.

Cependant, vous n'en aurez peut-être pas besoin. Si tout ce dont vous avez besoin est d'exécuter l'AST remplacé, vous pouvez le faire simplement en appelant compile () sur l'ast et en exécutant l'objet de code résultant.

Brian
la source
20
Pour tous ceux qui l'utiliseront à l'avenir, codegen est largement obsolète et présente quelques bogues. J'en ai réparé quelques-uns; J'ai ceci pour l'essentiel sur github: gist.github.com/791312
mattbasta
Notez que le dernier codegen est mis à jour en 2012, après le commentaire ci-dessus, donc je suppose que codegen est mis à jour. @mattbasta
zjffdu
4
astor semble être un successeur maintenu de codegen
medmunds
20

Dans une réponse différente, j'ai suggéré d'utiliser le astorpackage, mais j'ai depuis trouvé un package de décompression AST plus à jour appelé astunparse:

>>> import ast
>>> import astunparse
>>> print(astunparse.unparse(ast.parse('def foo(x): return 2 * x')))


def foo(x):
    return (2 * x)

J'ai testé cela sur Python 3.5.

argentpepper
la source
19

Vous n'aurez peut-être pas besoin de régénérer le code source. C'est un peu dangereux pour moi de dire, bien sûr, puisque vous n'avez pas réellement expliqué pourquoi vous pensez avoir besoin de générer un fichier .py plein de code; mais:

  • Si vous voulez générer un fichier .py que les gens utiliseront réellement, peut-être pour qu'ils puissent remplir un formulaire et obtenir un fichier .py utile à insérer dans leur projet, alors vous ne voulez pas le changer en AST et retour parce que vous perdrez tout le formatage (pensez aux lignes vides qui rendent Python si lisible en regroupant des ensembles de lignes connexes ensemble) (les nœuds ast linenoet les col_offsetattributs ) des commentaires. Au lieu de cela, vous voudrez probablement utiliser un moteur de création de modèles (le langage de modèle Django , par exemple, est conçu pour faciliter la création de modèles même de fichiers texte) pour personnaliser le fichier .py, ou bien utiliser l' extension MetaPython de Rick Copeland .

  • Si vous essayez d'apporter une modification lors de la compilation d'un module, notez que vous n'êtes pas obligé de revenir au texte; vous pouvez simplement compiler l'AST directement au lieu de le transformer en un fichier .py.

  • Mais dans presque tous les cas, vous essayez probablement de faire quelque chose de dynamique qu'un langage comme Python rend très facile, sans écrire de nouveaux fichiers .py! Si vous développez votre question pour nous faire savoir ce que vous voulez réellement accomplir, les nouveaux fichiers .py ne seront probablement pas du tout impliqués dans la réponse; J'ai vu des centaines de projets Python faire des centaines de choses du monde réel, et pas un seul d'entre eux n'avait besoin d'écrire un fichier .py. Donc, je dois admettre que je suis un peu sceptique que vous ayez trouvé le premier bon cas d'utilisation. :-)

Mise à jour: maintenant que vous avez expliqué ce que vous essayez de faire, je serais tenté d'opérer de toute façon sur l'AST. Vous voudrez muter en supprimant, non des lignes d'un fichier (ce qui pourrait entraîner des demi-instructions qui meurent simplement avec une SyntaxError), mais des instructions entières - et quel meilleur endroit pour le faire que dans l'AST?

Brandon Rhodes
la source
Bon aperçu des solutions possibles et des alternatives probables.
Ryan
1
Cas d'utilisation réel pour la génération de code: Kid et Genshi (je crois) génèrent Python à partir de modèles XML pour un rendu rapide des pages dynamiques.
Rick Copeland
10

L'analyse et la modification de la structure du code est certainement possible à l'aide du astmodule et je vais le montrer dans un exemple dans un instant. Cependant, la réécriture du code source modifié n'est pas possible avec le astmodule seul. Il existe d'autres modules disponibles pour ce travail, comme un ici .

REMARQUE: l'exemple ci-dessous peut être traité comme un didacticiel d'introduction sur l'utilisation du astmodule, mais un guide plus complet sur l'utilisation du astmodule est disponible ici sur le didacticiel Green Tree serpents et la documentation officielle sur le astmodule .

Introduction à ast:

>>> import ast
>>> tree = ast.parse("print 'Hello Python!!'")
>>> exec(compile(tree, filename="<ast>", mode="exec"))
Hello Python!!

Vous pouvez analyser le code python (représenté sous forme de chaîne) en appelant simplement l'API ast.parse(). Cela renvoie le descripteur à la structure AST (Abstract Syntax Tree). Il est intéressant de noter que vous pouvez recompiler cette structure et l'exécuter comme indiqué ci-dessus.

Une autre API très utile est de ast.dump()vider tout l'AST sous forme de chaîne. Il peut être utilisé pour inspecter l'arborescence et est très utile pour le débogage. Par exemple,

Sur Python 2.7:

>>> import ast
>>> tree = ast.parse("print 'Hello Python!!'")
>>> ast.dump(tree)
"Module(body=[Print(dest=None, values=[Str(s='Hello Python!!')], nl=True)])"

Sur Python 3.5:

>>> import ast
>>> tree = ast.parse("print ('Hello Python!!')")
>>> ast.dump(tree)
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hello Python!!')], keywords=[]))])"

Notez la différence de syntaxe pour l'instruction d'impression dans Python 2.7 par rapport à Python 3.5 et la différence de type de nœud AST dans les arbres respectifs.


Comment modifier le code en utilisant ast:

Voyons maintenant un exemple de modification de code python par astmodule. Le principal outil de modification de la structure AST est la ast.NodeTransformerclasse. Chaque fois que l'on a besoin de modifier l'AST, il / elle doit en faire une sous-classe et écrire la (les) transformation (s) de nœud en conséquence.

Pour notre exemple, essayons d'écrire un utilitaire simple qui transforme les instructions Python 2, print en appels de fonction Python 3.

Imprimer l'instruction dans l'utilitaire de conversion d'appel Fun: print2to3.py:

#!/usr/bin/env python
'''
This utility converts the python (2.7) statements to Python 3 alike function calls before running the code.

USAGE:
     python print2to3.py <filename>
'''
import ast
import sys

class P2to3(ast.NodeTransformer):
    def visit_Print(self, node):
        new_node = ast.Expr(value=ast.Call(func=ast.Name(id='print', ctx=ast.Load()),
            args=node.values,
            keywords=[], starargs=None, kwargs=None))
        ast.copy_location(new_node, node)
        return new_node

def main(filename=None):
    if not filename:
        return

    with open(filename, 'r') as fp:
        data = fp.readlines()
    data = ''.join(data)
    tree = ast.parse(data)

    print "Converting python 2 print statements to Python 3 function calls"
    print "-" * 35
    P2to3().visit(tree)
    ast.fix_missing_locations(tree)
    # print ast.dump(tree)

    exec(compile(tree, filename="p23", mode="exec"))

if __name__ == '__main__':
    if len(sys.argv) <=1:
        print ("\nUSAGE:\n\t print2to3.py <filename>")
        sys.exit(1)
    else:
        main(sys.argv[1])

Cet utilitaire peut être essayé sur un petit fichier d'exemple, tel que celui ci-dessous, et il devrait fonctionner correctement.

Fichier d'entrée de test: py2.py

class A(object):
    def __init__(self):
        pass

def good():
    print "I am good"

main = good

if __name__ == '__main__':
    print "I am in main"
    main()

Veuillez noter que la transformation ci-dessus est uniquement à astdes fins de didacticiel et que dans un scénario réel, il faudra examiner tous les différents scénarios tels que print " x is %s" % ("Hello Python").

ViFI
la source
6

J'ai créé récemment un morceau de code assez stable (le noyau est vraiment bien testé) et extensible qui génère du code à partir de l' astarbre: https://github.com/paluh/code-formatter .

J'utilise mon projet comme base pour un petit plugin vim (que j'utilise tous les jours), donc mon objectif est de générer un code python vraiment sympa et lisible.

PS J'ai essayé d'étendre codegenmais son architecture est basée sur l' ast.NodeVisitorinterface, donc les formateurs ( visitor_méthodes) ne sont que des fonctions. J'ai trouvé cette structure assez restrictive et difficile à optimiser (dans le cas d'expressions longues et imbriquées, il est plus facile de conserver l'arborescence des objets et de mettre en cache certains résultats partiels - d'une autre manière, vous pouvez atteindre une complexité exponentielle si vous souhaitez rechercher la meilleure mise en page). MAIS codegen comme chaque morceau du travail de mitsuhiko (que j'ai lu) est très bien écrit et concis.

paluh
la source
4

L'une des autres réponses recommande codegen, qui semble avoir été remplacée par astor. La version de astorsur PyPI (version 0.5 à ce jour) semble également un peu obsolète, vous pouvez donc installer la version de développement de astorcomme suit.

pip install git+https://github.com/berkerpeksag/astor.git#egg=astor

Ensuite, vous pouvez utiliser astor.to_sourcepour convertir un Python AST en code source Python lisible par l'homme:

>>> import ast
>>> import astor
>>> print(astor.to_source(ast.parse('def foo(x): return 2 * x')))
def foo(x):
    return 2 * x

J'ai testé cela sur Python 3.5.

argentpepper
la source
4

Si vous regardez cela en 2019, vous pouvez utiliser cette libcst package . Il a une syntaxe similaire à ast. Cela fonctionne à merveille et préserve la structure du code. C'est fondamentalement utile pour le projet où vous devez conserver les commentaires, les espaces, les sauts de ligne, etc.

Si vous n'avez pas besoin de vous soucier des commentaires, des espaces blancs et autres, alors la combinaison d'ast et astor fonctionne bien.

Saurav Gharti
la source
2

Nous avions un besoin similaire, qui n'a pas été résolu par d'autres réponses ici. Nous avons donc créé une bibliothèque pour cela, ASTTokens , qui prend un arbre AST produit avec les modules ast ou astroid , et le marque avec les plages de texte dans le code source d'origine.

Il ne modifie pas directement le code, mais ce n'est pas difficile à ajouter, car il vous indique la plage de texte que vous devez modifier.

Par exemple, cela encapsule un appel de fonction WRAP(...), en préservant les commentaires et tout le reste:

example = """
def foo(): # Test
  '''My func'''
  log("hello world")  # Print
"""

import ast, asttokens
atok = asttokens.ASTTokens(example, parse=True)

call = next(n for n in ast.walk(atok.tree) if isinstance(n, ast.Call))
start, end = atok.get_text_range(call)
print(atok.text[:start] + ('WRAP(%s)' % atok.text[start:end])  + atok.text[end:])

Produit:

def foo(): # Test
  '''My func'''
  WRAP(log("hello world"))  # Print

J'espère que cela t'aides!

DS.
la source
1

Un système de transformation de programme est un outil qui analyse le texte source, construit des AST, vous permet de les modifier en utilisant des transformations source-à-source («si vous voyez ce modèle, remplacez-le par ce modèle»). De tels outils sont idéaux pour faire la mutation des codes sources existants, qui sont simplement "si vous voyez ce modèle, remplacez-le par une variante de modèle".

Bien sûr, vous avez besoin d'un moteur de transformation de programme capable d'analyser le langage qui vous intéresse, tout en effectuant les transformations orientées modèle. Notre boîte à outils de réingénierie logicielle DMS est un système qui peut le faire et gère Python et une variété d'autres langages.

Voir cette réponse SO pour un exemple d'un AST analysé par DMS pour Python capturant les commentaires avec précision. Le DMS peut apporter des modifications à l'AST et régénérer du texte valide, y compris les commentaires. Vous pouvez lui demander d'imprimer l'AST, en utilisant ses propres conventions de mise en forme (vous pouvez les modifier), ou de faire une "impression de fidélité", qui utilise les informations de ligne et de colonne d'origine pour préserver au maximum la mise en page d'origine (certains changements de mise en page où le nouveau code est inséré est inévitable).

Pour implémenter une règle de «mutation» pour Python avec DMS, vous pouvez écrire ce qui suit:

rule mutate_addition(s:sum, p:product):sum->sum =
  " \s + \p " -> " \s - \p"
 if mutate_this_place(s);

Cette règle remplace "+" par "-" d'une manière syntaxiquement correcte; il fonctionne sur l'AST et ne touchera donc pas les chaînes ou les commentaires qui semblent corrects. La condition supplémentaire sur "mutate_this_place" est de vous permettre de contrôler la fréquence à laquelle cela se produit; vous ne voulez pas muter à chaque endroit du programme.

Vous voudriez évidemment un tas d'autres règles comme celle-ci qui détectent diverses structures de code et les remplacent par les versions mutées. DMS est heureux d'appliquer un ensemble de règles. L'AST muté est alors joliment imprimé.

Ira Baxter
la source
Je n'ai pas regardé cette réponse depuis 4 ans. Wow, il a été critiqué à plusieurs reprises. C'est vraiment étonnant, car cela répond directement à la question d'OP, et montre même comment faire les mutations qu'il veut faire. Je suppose qu'aucun des votants défavorables ne voudrait expliquer pourquoi ils ont voté contre.
Ira Baxter
4
Parce qu'il fait la promotion d'un outil source fermé très coûteux.
Zoran Pavlovic
@ZoranPavlovic: Vous ne vous opposez donc à aucune de sa précision technique ou de son utilité?
Ira Baxter
2
@Zoran: Il n'a pas dit qu'il avait une bibliothèque open source. Il a dit qu'il voulait modifier le code source de Python (en utilisant les AST), et les solutions qu'il a pu trouver ne l'ont pas fait. C'est une telle solution. Vous ne pensez pas que les gens utilisent des outils commerciaux sur des programmes écrits dans des langages comme Python sur Java?
Ira Baxter
1
Je ne suis pas un contre-votant, mais le message se lit un peu comme une publicité. Pour améliorer la réponse, vous pouvez indiquer que vous êtes affilié au produit
wim
0

J'utilisais baron pour cela, mais je suis maintenant passé au parso car il est à jour avec le python moderne. Cela fonctionne très bien.

J'en avais également besoin pour un testeur de mutation. C'est vraiment assez simple d'en faire un avec parso, consultez mon code sur https://github.com/boxed/mutmut

en boîte
la source