Analice un archivo .py, lea el AST, modifíquelo y luego vuelva a escribir el código fuente modificado

168

Quiero editar programáticamente el código fuente de Python. Básicamente, quiero leer un .pyarchivo, generar el AST y luego escribir el código fuente modificado de Python (es decir, otro .pyarchivo).

Hay formas de analizar / compilar el código fuente de Python utilizando módulos Python estándar, como asto compiler. Sin embargo, no creo que ninguno de ellos admita formas de modificar el código fuente (por ejemplo, eliminar esta declaración de función) y luego volver a escribir el código fuente de modificación de Python.

ACTUALIZACIÓN: La razón por la que quiero hacer esto es que me gustaría escribir una biblioteca de prueba de Mutación para python, principalmente eliminando declaraciones / expresiones, volviendo a ejecutar pruebas y viendo qué se rompe.

Rory
fuente
44
En desuso desde la versión 2.6: el paquete del compilador se ha eliminado en Python 3.0.
dfa
1
¿Qué no puedes editar la fuente? ¿Por qué no puedes escribir un decorador?
S.Lott
3
Vaca santa! Quería hacer un probador de mutaciones para Python usando la misma técnica (específicamente creando un plugin nasal), ¿estás planeando abrirlo?
Ryan el
2
@ Ryan Sí, abriré el código fuente de todo lo que cree. Deberíamos mantenernos en contacto sobre esto
Rory
1
Definitivamente, te envié un correo electrónico a través de Launchpad.
Ryan el

Respuestas:

73

Pythoscope hace esto a los casos de prueba que genera automáticamente al igual que la herramienta 2to3 para python 2.6 (convierte la fuente python 2.x en la fuente python 3.x).

Ambas herramientas utilizan la biblioteca lib2to3 , que es una implementación de la maquinaria del compilador / analizador de Python que puede preservar los comentarios en la fuente cuando se activa por completo desde la fuente -> AST -> fuente.

El proyecto de la cuerda puede satisfacer sus necesidades si desea hacer más refactorización como transformaciones.

El módulo ast es su otra opción, y hay un ejemplo más antiguo de cómo "analizar" los árboles de sintaxis de nuevo en el código (usando el módulo analizador). Pero el astmódulo es más útil cuando se realiza una transformación AST en el código que luego se transforma en un objeto de código.

El proyecto redbaron también puede encajar bien (ht Xavier Combelle)

Ryan
fuente
55
el ejemplo sin analizar aún se mantiene, aquí está la versión actualizada de py3k: hg.python.org/cpython/log/tip/Tools/parser/unparse.py
Janus Troelsen
2
Con respecto al unparse.pyscript, puede ser realmente engorroso usarlo desde otro script. Pero, hay un paquete llamado astunparse ( en github , en pypi ) que es básicamente una versión correctamente empaquetada de unparse.py.
mbdevpl
¿Podrías actualizar tu respuesta agregando parso como la opción preferida? Es muy bueno y actualizado.
caja el
59

El módulo ast incorporado no parece tener un método para volver a convertir a la fuente. Sin embargo, el módulo codegen aquí proporciona una impresora bonita para el ast que le permitiría hacerlo. p.ej.

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))

Esto imprimirá:

def foo():
    return 42

Tenga en cuenta que puede perder el formato exacto y los comentarios, ya que no se conservan.

Sin embargo, es posible que no sea necesario. Si todo lo que necesita es ejecutar el AST reemplazado, puede hacerlo simplemente llamando a compile () en el ast y ejecutando el objeto de código resultante.

Brian
fuente
20
Solo para cualquiera que use esto en el futuro, codegen está desactualizado y tiene algunos errores. He arreglado un par de ellos; Tengo esto como una esencia en github: gist.github.com/791312
mattbasta
Observe que el último codegen se actualiza en 2012, después del comentario anterior, así que supongo que se actualizó codegen. @mattbasta
zjffdu
44
Astor parece ser un sucesor mantenido de codegen
medmunds 05 de
20

En una respuesta diferente, sugerí usar el astorpaquete, pero desde entonces he encontrado un paquete de análisis AST más actualizado llamado astunparse:

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


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

He probado esto en Python 3.5.

argentpepper
fuente
19

Es posible que no necesite volver a generar el código fuente. Es un poco peligroso para mí decirlo, por supuesto, ya que en realidad no ha explicado por qué cree que necesita generar un archivo .py lleno de código; pero:

  • Si desea generar un archivo .py que la gente realmente usará, tal vez para que puedan completar un formulario y obtener un archivo .py útil para insertar en su proyecto, entonces no desea cambiarlo a un AST y de vuelta porque si no se pierden todo el formato (pensar en las líneas en blanco que hacen Python de manera legible mediante la agrupación de conjuntos relacionados de líneas juntas) ( nodos AST linenoy col_offsetatributos ) comentarios. En su lugar, es probable que desee utilizar un motor de plantillas (el lenguaje de plantillas de Django , por ejemplo, está diseñado para facilitar la creación de plantillas incluso de archivos de texto) para personalizar el archivo .py o utilizar la extensión MetaPython de Rick Copeland .

  • Si está intentando realizar un cambio durante la compilación de un módulo, tenga en cuenta que no tiene que volver al texto; simplemente puede compilar el AST directamente en lugar de convertirlo nuevamente en un archivo .py.

  • Pero en casi todos los casos, probablemente esté intentando hacer algo dinámico que un lenguaje como Python realmente hace muy fácil, ¡sin escribir nuevos archivos .py! Si expande su pregunta para hacernos saber lo que realmente quiere lograr, los nuevos archivos .py probablemente no estarán involucrados en la respuesta; He visto cientos de proyectos de Python haciendo cientos de cosas del mundo real, y ninguno de ellos necesitaba escribir un archivo .py. Entonces, debo admitir, soy un poco escéptico de que hayas encontrado el primer buen caso de uso. :-)

Actualización: ahora que ha explicado lo que está tratando de hacer, me sentiría tentado a operar el AST de todos modos. Querrá mutar eliminando, no líneas de un archivo (lo que podría dar como resultado medias declaraciones que simplemente mueren con un SyntaxError), sino declaraciones completas, ¿y qué mejor lugar para hacerlo que en el AST?

Brandon Rhodes
fuente
Buena visión general de la posible solución y las posibles alternativas.
Ryan el
1
Caso de uso del mundo real para la generación de código: Kid y Genshi (creo) generan Python a partir de plantillas XML para la representación rápida de páginas dinámicas.
Rick Copeland el
10

Sin duda, es posible analizar y modificar la estructura del código con la ayuda del astmódulo y lo mostraré en un ejemplo en un momento. Sin embargo, no es posible volver a escribir el código fuente modificado astsolo con el módulo. Hay otros módulos disponibles para este trabajo, como uno aquí .

NOTA: El ejemplo a continuación puede tratarse como un tutorial introductorio sobre el uso del astmódulo, pero una guía más completa sobre el uso del astmódulo está disponible aquí en el tutorial sobre serpientes de Green Tree y la documentación oficial sobre el astmódulo .

Introducción a ast:

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

Puede analizar el código de Python (representado en una cadena) simplemente llamando a la API ast.parse(). Esto devuelve el identificador a la estructura del Árbol de sintaxis abstracta (AST). Curiosamente, puede volver a compilar esta estructura y ejecutarla como se muestra arriba.

Otra API muy útil es la ast.dump()que descarga todo el AST en forma de cadena. Se puede usar para inspeccionar la estructura de árbol y es muy útil en la depuración. Por ejemplo,

En 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)])"

En 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=[]))])"

Observe la diferencia en la sintaxis para la declaración de impresión en Python 2.7 frente a Python 3.5 y la diferencia en el tipo de nodo AST en los árboles respectivos.


Cómo modificar el código usando ast:

Ahora, echemos un vistazo a un ejemplo de modificación del código de Python por astmódulo. La herramienta principal para modificar la estructura AST es la ast.NodeTransformerclase. Cada vez que uno necesita modificar el AST, él / ella necesita subclase de él y escribir Transformaciones de Nodo en consecuencia.

Para nuestro ejemplo, intentemos escribir una utilidad simple que transforme las declaraciones de Python 2, print en llamadas a funciones de Python 3.

Declaración de impresión en la utilidad de conversión de llamadas 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])

Esta utilidad se puede probar en un pequeño archivo de ejemplo, como el siguiente, y debería funcionar bien.

Archivo de entrada de prueba: 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()

Tenga en cuenta que la transformación anterior es solo para astfines de tutoría y, en el caso real, el escenario tendrá que observar todos los escenarios diferentes, tales como print " x is %s" % ("Hello Python").

ViFI
fuente
6

Recientemente he creado una pieza de código bastante estable (el núcleo está muy bien probado) y extensible que genera código del astárbol: https://github.com/paluh/code-formatter .

Estoy usando mi proyecto como base para un pequeño complemento vim (que uso todos los días), por lo que mi objetivo es generar un código Python realmente agradable y legible.

PD: Intenté extenderlo, codegenpero su arquitectura se basa en la ast.NodeVisitorinterfaz, por lo que los formateadores ( visitor_métodos) son solo funciones. He encontrado esta estructura bastante limitante y difícil de optimizar (en caso de expresiones largas y anidadas, es más fácil mantener el árbol de objetos y almacenar en caché algunos resultados parciales; de otra manera, puede alcanzar la complejidad exponencial si desea buscar el mejor diseño). PERO codegen como cada pieza del trabajo de mitsuhiko (que he leído) está muy bien escrita y concisa.

paluh
fuente
4

Una de las otras respuestas recomienda codegen, que parece haber sido reemplazado por astor. La versión de astoren PyPI (versión 0.5 a partir de este escrito) también parece estar un poco desactualizada, por lo que puede instalar la versión de desarrollo de la astorsiguiente manera.

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

Luego puede usar astor.to_sourcepara convertir un Python AST a un código fuente Python legible para humanos:

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

He probado esto en Python 3.5.

argentpepper
fuente
4

Si está viendo esto en 2019, puede usar este libcst paquete . Tiene una sintaxis similar a ast. Esto funciona de maravilla y preserva la estructura del código. Básicamente es útil para el proyecto donde debe conservar comentarios, espacios en blanco, nueva línea, etc.

Si no necesita preocuparse por la conservación de comentarios, espacios en blanco y otros, entonces la combinación de ast y astor funciona bien.

Saurav Gharti
fuente
2

Tuvimos una necesidad similar, que no fue resuelta por otras respuestas aquí. Entonces creamos una biblioteca para esto, ASTTokens , que toma un árbol AST producido con el ast o astroid módulos , y lo marca con los rangos de texto en el código fuente original.

No realiza modificaciones de código directamente, pero eso no es difícil de agregar en la parte superior, ya que le indica el rango de texto que debe modificar.

Por ejemplo, esto envuelve una llamada de función WRAP(...), conservando comentarios y todo lo demás:

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:])

Produce:

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

¡Espero que esto ayude!

DS.
fuente
1

Un sistema de transformación del programa es una herramienta que analiza el texto fuente, crea AST, le permite modificarlos usando transformaciones de fuente a fuente ("si ve este patrón, reemplácelo por ese patrón"). Dichas herramientas son ideales para la mutación de los códigos fuente existentes, que son solo "si ve este patrón, reemplácelo por una variante de patrón".

Por supuesto, necesita un motor de transformación de programas que pueda analizar el lenguaje que le interese y aún así realizar las transformaciones dirigidas por patrones. Nuestro DMS Software Reengineering Toolkit es un sistema que puede hacer eso y maneja Python y una variedad de otros idiomas.

Vea esta respuesta SO para ver un ejemplo de AST analizado por DMS para Python capturando comentarios con precisión. DMS puede realizar cambios en el AST y regenerar texto válido, incluidos los comentarios. Puede pedirle que imprima el AST, utilizando sus propias convenciones de formato (puede cambiarlas), o hacer "impresión de fidelidad", que utiliza la información original de la línea y la columna para preservar al máximo el diseño original (algunos cambios en el diseño donde el nuevo código se inserta es inevitable).

Para implementar una regla de "mutación" para Python con DMS, puede escribir lo siguiente:

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

Esta regla reemplaza "+" con "-" de una manera sintácticamente correcta; funciona en el AST y, por lo tanto, no tocará cadenas o comentarios que se vean bien. La condición adicional en "mutate_this_place" es permitirle controlar con qué frecuencia ocurre esto; no quieres mutar todos los lugares del programa.

Obviamente, querría un montón más de reglas como esta que detecten varias estructuras de código y las reemplacen por las versiones mutadas. DMS se complace en aplicar un conjunto de reglas. El AST mutado es entonces bastante impreso.

Ira Baxter
fuente
No he visto esta respuesta en 4 años. Wow, ha sido rechazado varias veces. Eso es realmente sorprendente, ya que responde a la pregunta de OP directamente, e incluso muestra cómo hacer las mutaciones que quiere hacer. No creo que a ninguno de los votantes negativos les importe explicar por qué votaron negativamente.
Ira Baxter
44
Porque promueve una herramienta de código cerrado muy costosa.
Zoran Pavlovic
@ZoranPavlovic: ¿Entonces no se opone a ninguna de su precisión técnica o utilidad?
Ira Baxter
2
@Zoran: No dijo que tenía una biblioteca de código abierto. Dijo que quería modificar el código fuente de Python (usando AST), y las soluciones que pudo encontrar no lo hicieron. Esta es una solución. ¿No crees que la gente usa herramientas comerciales en programas escritos en lenguajes como Python en Java?
Ira Baxter
1
No soy un votante negativo, pero la publicación se lee un poco como un anuncio. Para mejorar la respuesta, puede revelar que está afiliado al producto
wim
0

Solía ​​usar baron para esto, pero ahora he cambiado a parso porque está actualizado con Python moderno. Funciona muy bien

También necesitaba esto para un probador de mutaciones. Realmente es bastante simple hacer uno con parso, mira mi código en https://github.com/boxed/mutmut

en caja
fuente