Analyser a.py fichier, Lire L'AST, le modifier, puis réécrire le code source modifié

Je veux éditer par programmation le code source python. Fondamentalement, 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 en utilisant des modules Python standard, tels que ast ou compiler. Cependant, je ne pense pas qu'aucun d'entre eux supporte des moyens de modifier le code source (par exemple supprimer cette déclaration de fonction), puis réécrire la modification code source python.

UPDATE: la raison pour laquelle je veux le faire 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 casse.

137
demandé sur Jonathan Leffler 2009-04-20 18:51:55

10 réponses

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

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

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

Le module ast est votre autre option, et Il y a un exemple plus ancien de comment "analyser" les arbres de syntaxe dans le code (en utilisant le module d'analyseur). Mais le module ast est plus utile lors d'une transformation AST sur du code qui est ensuite transformé en un objet de code.

Le projet redbaron peut également être un bon ajustement (HT Xavier Combelle)

65
répondu Ryan 2018-08-18 22:31:23

Le module AST intégré ne semble pas avoir de méthode pour le convertir en source. Cependant, le modulecodegen 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 va imprimer:

def foo():
    return 42

Notez que vous risquez de perdre le formatage exact et les commentaires, car ceux-ci 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 exécuter l'objet de code résultant.

55
répondu Brian 2014-05-23 18:10:15

Vous n'avez peut - être pas besoin de générer à nouveau du code source. C'est un peu dangereux pour moi de le 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 vont réellement utiliser, 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 à la lignes vides qui rendent Python Si lisible en regroupant les ensembles de lignes connexes ensemble) (les nœuds ast ont des Commentaires lineno et col_offset attributs). Au lieu de cela, vous voudrez probablement utiliser un moteur de modèle (le langage de modèle Django, par exemple, est conçu pour faciliter la création de modèles de fichiers texte) pour personnaliser le fichier .PY, ou bien utiliser L'extension MetaPython de Rick Copeland.

  • Si vous essayez d'effectuer une modification lors de la compilation d'un module, notez que vous n'avez pas à revenir au texte; vous pouvez simplement compiler L'AST directement au lieu de le retourner dans 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 impliqués dans la réponse du tout; j'ai vu des centaines de Projets Python faisant des centaines de choses du monde réel, et pas un seul d'entre eux avait besoin de jamais écrire un fichier .py. Donc, je dois admettre, je suis un peu sceptique que vous avez trouvé le premier bon cas d'utilisation. :-)

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

20
répondu Brandon Rhodes 2011-05-21 21:41:45

Dans une réponse différente, j'ai suggéré d'utiliser le paquet astor, mais j'ai depuis trouvé un paquet AST unsing 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é ceci sur Python 3.5.

14
répondu argentpepper 2016-09-09 13:52:09

J'ai créé récemment assez stable (le noyau est vraiment bien testé) et un morceau de code extensible qui génère du code à partir de ast tree: 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 but est de générer du 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, donc les formatters (visitor_ méthodes) ne sont que des fonctions. J'ai trouvé cette structure assez limitante 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 la complexité exponentielle si vous voulez 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.

6
répondu paluh 2014-12-08 12:52:16

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

NOTE: L'exemple ci-dessous peut être traité comme un tutoriel d'introduction sur l'utilisation du module ast mais un guide plus complet sur l'utilisation du module ast est disponible ici à tutoriel sur les serpents des arbres verts et documentation officielle sur le module ast .

Introduction à la 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é en chaîne) en appelant simplement L'API ast.parse(). Cela renvoie la structure handle to Abstract Syntax Tree (AST). 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'AST entier sous une forme de chaîne. Il peut être utilisé pour inspecter la structure arborescente et est très utile dans 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 print 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 à l'aide ast:

Maintenant, regardons un exemple de modification du code python par le module ast. L'outil principal pour modifier la structure AST est ast.NodeTransformer classe. Chaque fois que l'on a besoin de modifier L'AST, il / elle doit en sous-classer 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.

Déclaration D'impression à L'utilitaire de convertisseur D'appel amusant: 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 cela devrait bien fonctionner.

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 pour le but du tutoriel ast et dans un scénario de cas réel, il faudra regarder tous les différents scénarios tels que print " x is %s" % ("Hello Python").

6
répondu ViFI 2017-01-23 08:38:57

L'Une des autres réponses recommande codegen, ce qui semble avoir été remplacé par astor. La version de astor sur PyPI (version 0.5 à ce jour) semble être un peu obsolète, donc vous pouvez 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 AST Python 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é ceci sur Python 3.5.

3
répondu argentpepper 2017-05-23 12:02:47

Un système de Transformation de programme est un outil qui analyse le texte source, construit les 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 juste "si vous voyez Ce modèle, remplacez par une variante de modèle".

Bien sûr, vous avez besoin d'un moteur de transformation de programme qui peut analyser la langue qui vous intéresse, et toujours faire le transformations dirigées par un motif. Notre DMS Software Reengineering Toolkit est un système qui peut le faire, et gère Python, et une variété d'autres langages.

Voir ceci alors répondez à un exemple D'AST analysé par DMS pour Python capturant les commentaires avec précision. DMS peut apporter des modifications à L'AST, et régénérer le texte valide, y compris les commentaires. Vous pouvez lui demander de prettyprint L'AST, en utilisant ses propres conventions de formatage (vous pouvez les modifier), ou faire " fidélité printing", qui utilise les informations de ligne et de colonne d'origine pour préserver au maximum la mise en page d'origine (un changement de mise en page où un nouveau code est inséré est inévitable).

Pour implémenter une règle "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 " - " de manière syntaxique correcte; elle 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 laisser contrôlez la fréquence à laquelle cela se produit; vous ne voulez pas muter chaque place dans le programme.

Vous voudriez évidemment un tas de 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 jolieimprimé.

2
répondu Ira Baxter 2017-05-23 12:02:47

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 original.

Il ne fait pas de modifications de code directement, 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 dans WRAP(...), préserver 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

Espérons que cela aide!

2
répondu DS. 2016-12-14 02:39:21

J'avais l'habitude d'utiliser baron pour cela, mais je suis maintenant passé à parso parce qu'il est à jour avec Python moderne. Il fonctionne très bien.

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

0
répondu boxed 2018-08-11 05:22:58