Analise um arquivo .py, leia o AST, modifique-o e escreva novamente o código-fonte modificado

168

Quero editar programaticamente o código-fonte python. Basicamente, quero ler um .pyarquivo, gerar o AST e, em seguida, escrever novamente o código-fonte python modificado (ou seja, outro .pyarquivo).

Existem maneiras de analisar / compilar o código-fonte python usando módulos python padrão, como astou compiler. No entanto, acho que nenhum deles suporta maneiras de modificar o código-fonte (por exemplo, exclua esta declaração de função) e depois escreva novamente o código-fonte python modificado.

ATUALIZAÇÃO: A razão pela qual eu quero fazer isso é que eu gostaria de escrever uma biblioteca de teste de mutação para python, principalmente excluindo instruções / expressões, executando novamente os testes e vendo o que quebra.

Rory
fonte
4
Descontinuado desde a versão 2.6: O pacote do compilador foi removido no Python 3.0.
Dfa
1
O que você não pode editar a fonte? Por que você não pode escrever um decorador?
S.Lott
3
Vaca sagrada! Eu queria fazer um testador de mutações para python usando a mesma técnica (especificamente criando um plugin para o nariz), você está planejando fazer o código-fonte aberto?
21909 Ryan
2
@ Ryan Sim, eu vou abrir o código qualquer coisa que eu criar. Deveríamos manter contato com isso #
Rory
1
Definitivamente, enviei um email pelo Launchpad.
Ryan

Respostas:

73

O Pythoscope faz isso nos casos de teste que gera automaticamente, assim como a ferramenta 2to3 para o python 2.6 (converte a fonte python 2.x na fonte python 3.x).

Ambas as ferramentas usam a biblioteca lib2to3 , que é uma implementação do mecanismo de analisador / compilador python que pode preservar comentários na fonte quando ela faz ida e volta da fonte -> AST -> fonte.

O projeto de corda pode atender às suas necessidades se você quiser fazer mais refatorações como transformações.

O módulo ast é sua outra opção, e há um exemplo mais antigo de como "desparalisar" as árvores de sintaxe de volta ao código (usando o módulo analisador). Mas o astmódulo é mais útil ao fazer uma transformação AST no código que é transformado em um objeto de código.

O projeto redbaron também pode ser um bom ajuste (ht Xavier Combelle)

Ryan
fonte
5
o exemplo unparse ainda é mantida, aqui é a versão Py3k atualização: hg.python.org/cpython/log/tip/Tools/parser/unparse.py
Janus Troelsen
2
Com relação ao unparse.pyscript - pode ser realmente complicado usá-lo em outro script. Mas, existe um pacote chamado astunparse ( no github , no pypi ) que é basicamente uma versão do pacote adequadamente unparse.py.
Mbdevpl 18/05/19
Você poderia atualizar sua resposta adicionando parso como a opção preferida? É muito bom e atualizado.
encaixotado
59

O módulo ast embutido não parece ter um método para converter de volta à fonte. No entanto, o módulo codegen aqui fornece uma impressora bonita para o ast que permitiria fazer isso. por exemplo.

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

Isso imprimirá:

def foo():
    return 42

Observe que você pode perder a formatação exata e os comentários, pois eles não são preservados.

No entanto, você pode não precisar. Se tudo o que você precisa é executar o AST substituído, basta fazê-lo chamando compile () no ast e executando o objeto de código resultante.

Brian
fonte
20
Apenas para quem usa isso no futuro, o codegen está desatualizado e possui alguns bugs. Eu consertei alguns deles; Eu tenho isso como uma essência no github: gist.github.com/791312
mattbasta
Observe que o codegen mais recente é atualizado em 2012, após o comentário acima, então acho que o codegen é atualizado. @mattbasta
zjffdu
4
Astor parece ser um sucessor mantida para CodeGen
medmunds
20

Em uma resposta diferente, sugeri o uso do astorpacote, mas encontrei um pacote de análise de análise AST mais atualizado chamado astunparse:

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


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

Eu testei isso no Python 3.5.

argentpepper
fonte
19

Pode não ser necessário gerar novamente o código fonte. É um pouco perigoso para mim dizer, é claro, já que você realmente não explicou por que acha que precisa gerar um arquivo .py cheio de código; mas:

  • Se você deseja gerar um arquivo .py que as pessoas realmente usem, talvez para que eles possam preencher um formulário e obter um arquivo .py útil para inserir em seu projeto, não será necessário transformá-lo em um AST e de volta porque você perderá toda a formatação (pense nas linhas em branco que tornam o Python tão legível ao agrupar conjuntos de linhas relacionados) (comentários de nós linenoe col_offsetatributos ). Em vez disso, você provavelmente desejará usar um mecanismo de modelagem (a linguagem de modelo do Django , por exemplo, foi projetada para facilitar a modelagem de arquivos de texto) para personalizar o arquivo .py ou usar a extensão MetaPython de Rick Copeland .

  • Se você estiver tentando fazer uma alteração durante a compilação de um módulo, observe que não é necessário voltar ao texto; você pode compilar o AST diretamente, em vez de transformá-lo novamente em um arquivo .py.

  • Mas em quase todo e qualquer caso, você provavelmente está tentando fazer algo dinâmico que uma linguagem como o Python realmente facilita muito, sem escrever novos arquivos .py! Se você expandir sua pergunta para nos informar o que realmente deseja realizar, os novos arquivos .py provavelmente não estarão envolvidos na resposta; Eu já vi centenas de projetos Python fazendo centenas de coisas do mundo real, e nenhum deles precisava criar um arquivo .py. Então, devo admitir, sou um pouco cético que você tenha encontrado o primeiro bom caso de uso. :-)

Atualização: agora que você explicou o que está tentando fazer, eu ficaria tentado a operar no AST de qualquer maneira. Você deseja fazer a mutação removendo, não as linhas de um arquivo (o que poderia resultar em meias instruções que simplesmente morrem com um SyntaxError), mas em instruções inteiras - e que melhor lugar para fazer isso do que no AST?

Brandon Rhodes
fonte
Boa visão geral de possíveis soluções e possíveis alternativas.
21909 Ryan
1
Caso de uso no mundo real para geração de código: Kid e Genshi (acredito) geram Python a partir de modelos XML para renderização rápida de páginas dinâmicas.
21411 Rick Copeland
10

Analisar e modificar a estrutura do código é certamente possível com a ajuda do astmódulo e mostrarei em um exemplo em um momento. No entanto, a gravação do código-fonte modificado não é possível apenas com o astmódulo. Existem outros módulos disponíveis para este trabalho, como um aqui .

NOTA: O exemplo abaixo pode ser tratado como um tutorial introdutório sobre o uso do astmódulo, mas um guia mais abrangente sobre o uso do astmódulo está disponível aqui no tutorial do Green Tree snakes e na documentação oficial do astmódulo .

Introdução a ast:

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

Você pode analisar o código python (representado na string) simplesmente chamando a API ast.parse(). Isso retorna o identificador para a estrutura Abstract Syntax Tree (AST). Curiosamente, você pode compilar novamente essa estrutura e executá-la como mostrado acima.

Outra API muito útil é ast.dump()que despeja todo o AST em uma forma de string. Ele pode ser usado para inspecionar a estrutura da árvore e é muito útil na depuração. Por exemplo,

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

No 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 a diferença na sintaxe da declaração de impressão no Python 2.7 vs. o Python 3.5 e a diferença no tipo de nó AST nas respectivas árvores.


Como modificar o código usando ast:

Agora, vamos dar uma olhada em um exemplo de modificação do código python por astmódulo. A principal ferramenta para modificar a estrutura AST é a ast.NodeTransformerclasse. Sempre que é necessário modificar o AST, ele precisa subclassificar e escrever a (s) Transformação (s) de Nó de acordo.

No nosso exemplo, vamos tentar escrever um utilitário simples que transforma as instruções de impressão Python 2 em chamadas de função do Python 3.

Imprima a declaração no utilitário conversor de chamadas 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])

Este utilitário pode ser experimentado em um pequeno arquivo de exemplo, como o abaixo, e deve funcionar bem.

Arquivo de entrada de teste: 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()

Observe que a transformação acima é apenas para astfins de tutorial e, no caso real, será necessário analisar todos os cenários diferentes, como print " x is %s" % ("Hello Python").

ViFI
fonte
6

Eu criei recentemente um código bastante estável (o núcleo é realmente bem testado) e extensível que gera código a partir da astárvore: https://github.com/paluh/code-formatter .

Estou usando meu projeto como base para um pequeno plug-in vim (que uso todos os dias), então meu objetivo é gerar um código python realmente agradável e legível.

PS: Eu tentei estender, codegenmas sua arquitetura é baseada na ast.NodeVisitorinterface, então formatadores ( visitor_métodos) são apenas funções. Achei essa estrutura bastante limitadora e difícil de otimizar (no caso de expressões longas e aninhadas, é mais fácil manter os objetos em árvore e armazenar em cache alguns resultados parciais - de outra forma, você pode atingir complexidade exponencial se quiser procurar o melhor layout). MAS, codegen como todas as peças do trabalho de mitsuhiko (que eu li) são muito bem escritas e concisas.

paluh
fonte
4

Uma das outras respostas recomenda codegen, que parece ter sido substituída por astor. A versão do astorPyPI (versão 0.5 até a presente data) também parece um pouco desatualizada, portanto você pode instalar a versão de desenvolvimento da astorseguinte forma.

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

Em seguida, você pode usar astor.to_sourcepara converter um código-fonte Python AST em código-fonte Python legível por humanos:

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

Eu testei isso no Python 3.5.

argentpepper
fonte
4

Se você estiver olhando para isso em 2019, poderá usar este pacote libcst . Possui sintaxe semelhante ao ast. Isso funciona como um encanto e preserva a estrutura do código. É basicamente útil para o projeto em que você deve preservar comentários, espaço em branco, nova linha etc.

Se você não precisa se preocupar com os comentários de preservação, espaços em branco e outros, a combinação de ast e astor funciona bem.

Saurav Gharti
fonte
2

Tínhamos uma necessidade semelhante, que não foi resolvida por outras respostas aqui. Por isso, criamos uma biblioteca para isso, o ASTTokens , que pega uma árvore AST produzida com os módulos ast ou astroid e a marca com os intervalos de texto no código-fonte original.

Ele não faz modificações diretamente no código, mas isso não é difícil de adicionar, pois indica o intervalo de texto que você precisa modificar.

Por exemplo, isso envolve uma chamada de função WRAP(...), preservando comentários e tudo o mais:

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

Produz:

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

Espero que isto ajude!

DS.
fonte
1

Um Sistema de Transformação de Programa é uma ferramenta que analisa o texto de origem, constrói ASTs, permite modificá-lo usando transformações de fonte para fonte ("se você vir esse padrão, substitua-o por esse padrão"). Essas ferramentas são ideais para a mutação de códigos-fonte existentes, que são apenas "se você vê esse padrão, substitua por uma variante de padrão".

Obviamente, você precisa de um mecanismo de transformação de programa que possa analisar o idioma de seu interesse e ainda fazer as transformações direcionadas a padrões. Nosso DMS Software Reengineering Toolkit é um sistema que pode fazer isso e lida com Python e uma variedade de outros idiomas.

Veja esta resposta do SO para obter um exemplo de AST para Python analisado por DMS, capturando comentários com precisão. O DMS pode fazer alterações no AST e gerar novamente um texto válido, incluindo os comentários. Você pode solicitar a impressão bonita do AST, usando suas próprias convenções de formatação (você pode alterá-las) ou fazer "impressão de fidelidade", que usa as informações originais de linha e coluna para preservar ao máximo o layout original (algumas alterações no layout onde o novo código está inserido é inevitável).

Para implementar uma regra de "mutação" para Python com DMS, você pode escrever o seguinte:

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

Esta regra substitui "+" por "-" de uma maneira sintaticamente correta; Ele opera no AST e, portanto, não toca em seqüências ou comentários que parecem corretos. A condição extra em "mutate_this_place" é permitir que você controle com que frequência isso ocorre; você não deseja alterar todos os lugares do programa.

Obviamente, você deseja várias regras como essa que detectam várias estruturas de código e as substituem pelas versões mutadas. O DMS tem prazer em aplicar um conjunto de regras. O AST mutado é então bastante impresso.

Ira Baxter
fonte
Não vejo essa resposta há quatro anos. Uau, ele foi rebaixado várias vezes. Isso é realmente impressionante, pois responde diretamente à pergunta do OP e até mostra como fazer as mutações que ele deseja fazer. Suponho que nenhum dos que recusaram se importaria em explicar por que votaram abaixo.
Ira Baxter
4
Porque promove uma ferramenta de código fechado muito cara.
Zoran Pavlovic
@ZoranPavlovic: Então você não se opõe a nenhuma de sua precisão ou utilidade técnica?
Ira Baxter
2
@Zoran: Ele não disse que tinha uma biblioteca de código aberto. Ele disse que queria modificar o código fonte do Python (usando ASTs), e as soluções que conseguiu encontrar não o fizeram. Essa é uma solução. Você não acha que as pessoas usam ferramentas comerciais em programas escritos em linguagens como Python em Java?
Ira Baxter
1
Eu não sou um eleitor descendente, mas o post parece um anúncio. Para melhorar a resposta, você pode divulgar que é afiliado ao produto
wim 15/03
0

Eu costumava usar o barão para isso, mas agora mudei para o parso porque está atualizado com o python moderno. Isso funciona muito bem.

Eu também precisava disso para um testador de mutação. É realmente muito simples criar um com parso, confira meu código em https://github.com/boxed/mutmut

encaixotado
fonte