web-dev-qa-db-fra.com

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

Je veux éditer par programmation le code source de python. En gros, je veux lire un fichier .py, générer le AST , puis réécrire le code source python modifié (c’est-à-dire un autre fichier .py).

Il existe des moyens d’analyser/compiler le code source Python à l’aide de modules Python standard, tels que ast ou compiler . Cependant, je ne pense pas qu’aucun d’entre eux ne permette de modifier le code source (par exemple, de supprimer cette déclaration de fonction), puis d’écrire le code source python modifiant.

UPDATE: La raison pour laquelle je veux faire cela est que j'aimerais écrire une bibliothèque de tests de mutations pour python, principalement en supprimant des instructions/expressions, en réexécutant des tests et en voyant ce qui ne fonctionne pas.

145
Rory

Pythoscope s’applique aux scénarios de test qu’il génère automatiquement, 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 machine d’analyseur/compilateur python qui peut conserver les commentaires dans le source lorsqu’il est déclenché par le source -> AST -> source.

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

Le module ast est votre autre option et un exemple plus ancien illustre comment "décomposer" les arbres de syntaxe dans le code (à l'aide du module analyseur). Mais le module ast est plus utile lorsque vous effectuez une transformation AST sur du code qui est ensuite transformé en un objet code.

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

66
Ryan

Le module intégré ne semble pas avoir une méthode pour reconvertir en source. Cependant, le module codegen fournit ici une jolie imprimante qui vous permettrait de le faire.

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 va imprimer:

def foo():
    return 42

Notez que vous pouvez 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 ast et en exécutant l'objet code résultant.

55
Brian

Vous n'aurez peut-être pas besoin de générer à nouveau 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 souhaitez générer un fichier .py que les gens utiliseront réellement, afin de pouvoir remplir un formulaire et obtenir un fichier .py utile à insérer dans leur projet, vous ne souhaitez pas le modifier en AST et retour parce que vous allez perdre toute la mise en forme (pensez aux lignes vides qui rendent Python très lisible en regroupant des ensembles de lignes connexes) Les nœuds ( ast ont des attributs lineno et col_offset ). Au lieu de cela, vous voudrez probablement utiliser un moteur de modélisation (le le langage de modèle Django , par exemple, est conçu pour faciliter la modélisation de fichiers texte même) pour personnaliser le fichier .py, ou bien utiliser MetaPython de Rick Copeland. extension.

  • Si vous essayez de faire un changement lors de la compilation d'un module, notez qu'il n'est pas nécessaire de revenir au texte; vous pouvez simplement compiler le AST directement au lieu de le reconvertir en un fichier .py.

  • Mais dans presque tous les cas, vous essayez probablement de faire quelque chose de dynamique qu'un langage comme Python facilite réellement, sans écrire de nouveaux fichiers .py! Si vous développez votre question pour nous indiquer ce que vous voulez réellement accomplir, les nouveaux fichiers .py ne seront probablement pas impliqués dans la réponse; J'ai vu des centaines de projets Python faire des centaines de choses dans le monde réel, et aucun d'entre eux n'avait besoin de rédiger un fichier .py. Donc, je dois admettre que je suis un peu sceptique quant au fait 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 simplement sur le AST =. Vous voudrez muter en supprimant, pas les lignes d'un fichier (ce qui pourrait entraîner des demi-déclarations mourant simplement avec SyntaxError), mais des déclarations entières - et quel meilleur endroit pour le faire que dans l'AST?

20
Brandon Rhodes

Dans une réponse différente, j'ai suggéré d'utiliser le paquetage astor, mais j'ai depuis trouvé un paquet d'analyse plus récent AST 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.

16
argentpepper

L'analyse et la modification de la structure du code sont certainement possibles à l'aide du module ast et je le montrerai dans un exemple dans un instant. Cependant, l'écriture du code source modifié n'est pas possible avec le module ast seul. D'autres modules sont disponibles pour ce travail, par exemple un ici

REMARQUE: l'exemple ci-dessous peut être traité comme un didacticiel d'introduction à l'utilisation du module ast, mais un guide plus complet sur l'utilisation du module ast est disponible à l'adresse suivante: Green Tree snakes tutorial et documentation officielle sur ast module

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é dans la chaîne) en appelant simplement l'API ast.parse(). Cela renvoie le descripteur à la structure AST (Abstract Syntax Tree). Fait intéressant, vous pouvez compiler cette structure et l’exécuter comme indiqué ci-dessus.

Une autre API très utile est ast.dump(), qui vide l'ensemble 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 entre l'instruction print dans Python 2.7 et Python 3.5, ainsi que la différence de type du nœud AST dans les arborescences respectives.


Comment modifier le code en utilisant ast:

Voyons maintenant un exemple de modification du code python par le module ast. Le principal outil permettant de modifier la structure AST est la classe ast.NodeTransformer. Chaque fois que l'on doit modifier l'AST, il doit en faire une sous-classe et écrire les transformations 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 fonctions Python 3. 

Imprime l'instruction dans l'utilitaire de conversion d'appels 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 __== '__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 exemple de fichier, tel que celui ci-dessous, et il devrait fonctionner correctement. 

Test Fichier d'entrée: py2.py

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

def good():
    print "I am good"

main = good

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

Veuillez noter que la transformation ci-dessus est uniquement à des fins de tutoriel ast et que, dans le cas réel, il faudra examiner tous les scénarios, tels que print " x is %s" % ("Hello Python").

6
ViFI

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

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

P.S . J'ai essayé d'étendre codegen mais son architecture est basée sur l'interface ast.NodeVisitor; les méthodes de formatage (méthodes visitor_) ne sont que des fonctions. J'ai trouvé cette structure assez contraignante 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 des résultats partiels. Vous pouvez également utiliser une complexité exponentielle si vous souhaitez rechercher la meilleure présentation. MAIScodegen comme chaque morceau du travail de Mitsuhiko (que j'ai lu) est très bien écrit et concis.

6
paluh

Une des autres réponses recommande codegen, qui semble avoir été remplacé par astor . La version de astor sur PyPI (version 0.5 à ce jour) semble également un peu dépassée. Vous pouvez donc installer la version de développement de astor comme suit.

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

Ensuite, vous pouvez utiliser astor.to_source pour convertir un code 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.

3
argentpepper

A Programme de transformation de programme est un outil qui analyse le texte source, construit des AST, vous permet de les modifier à l'aide de transformations source à source ("si vous voyez ce modèle, remplacez-le par ce modèle"). De tels outils sont parfaits pour effectuer la mutation de codes sources existants, qui sont simplement "si vous voyez ce motif, remplacez-le par une variante de motif".

Bien entendu, vous avez besoin d’un moteur de transformation de programme capable d’analyser le langage qui vous intéresse et d’effectuer les transformations dirigées par les motifs. Notre DMS Software Reengineering Toolkit est un système capable de le faire, qui gère Python et une variété d’autres langages. 

Voir cette réponse SO pour un exemple d'analyse AST par DMS pour la capture de commentaires Python avec précision. 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 formatage (vous pouvez les modifier), ou d'effectuer une "impression de fidélité", qui utilise les informations de ligne et de colonne d'origine pour conserver au maximum la mise en page d'origine 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 AST et ne touche donc pas les chaînes de caractères ni les commentaires qui semblent corrects. La condition supplémentaire sur "mutate_this_place" consiste à vous permettre de contrôler la fréquence à laquelle cela se produit; vous ne voulez pas muter chaque place dans le programme.

Il est évident que vous voudrez un ensemble plus de règles comme celle-ci, qui détecte diverses structures de code et les remplace par les versions mutées. DMS est heureux d'appliquer un ensemble de règles. Le AST muté est alors joli imprimé.

2
Ira Baxter

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 de l’original. code source.

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

Par exemple, ceci encapsule un appel de fonction dans 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!

2
DS.

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

J'avais aussi besoin de ça pour un testeur de mutation. C’est vraiment très simple d’en faire un avec Parso, consultez mon code sur https://github.com/boxed/mutmut

0
boxed