Analizzare un file .py, leggere l'AST, modificarlo, quindi riscrivere il codice sorgente modificato


168

Voglio modificare a livello di codice il codice sorgente di Python. Fondamentalmente voglio leggere un .pyfile, generare l' AST e quindi riscrivere il codice sorgente di Python modificato (ovvero un altro .pyfile).

Esistono modi per analizzare / compilare il codice sorgente di Python usando moduli standard di Python, come asto compiler. Tuttavia, non credo che nessuno di essi supporti i modi per modificare il codice sorgente (ad esempio eliminare questa dichiarazione di funzione) e quindi riscrivere il codice sorgente di Python modificante.

AGGIORNAMENTO: Il motivo per cui voglio fare questo è che vorrei scrivere una libreria di test di mutazione per Python, principalmente cancellando le dichiarazioni / espressioni, rieseguendo i test e vedendo cosa si rompe.


4
Obsoleto dalla versione 2.6: il pacchetto del compilatore è stato rimosso in Python 3.0.
dfa,

1
Cosa non puoi modificare la fonte? Perché non riesci a scrivere un decoratore?
S.Lott

3
Mucca sacra! Volevo creare un tester di mutazione per Python usando la stessa tecnica (in particolare la creazione di un plug-in nose), hai intenzione di farlo open source?
Ryan,

2
@Ryan Sì, aprirò tutto ciò che creo. Dovremmo tenerci in contatto su questo
Rory,

1
Sicuramente, ti ho inviato un'e-mail tramite Launchpad.
Ryan,

Risposte:


73

Pythoscope fa questo ai casi di test che genera automaticamente così come il 2to3 strumento per python 2.6 (converte il sorgente python 2.x in sorgente python 3.x).

Entrambi questi strumenti usano la libreria lib2to3 che è un'implementazione del macchinario Python Parson / Compiler che può conservare i commenti nella fonte quando è tondo sganciato dalla fonte -> AST -> fonte.

Il progetto corda può soddisfare le tue esigenze se vuoi fare più refactoring come trasformazioni.

Il modulo ast è la tua altra opzione e c'è un vecchio esempio di come "analizzare" gli alberi di sintassi nel codice (usando il modulo parser). Ma il astmodulo è più utile quando si esegue una trasformazione AST su codice che viene quindi trasformato in un oggetto codice.

Anche il progetto redbaron potrebbe essere adatto (ht Xavier Combelle)


5
l'esempio unparse è ancora mantenuto, ecco la versione di py3k aggiornata: hg.python.org/cpython/log/tip/Tools/parser/unparse.py
Janus Troelsen,

2
Per quanto riguarda la unparse.pysceneggiatura, può essere davvero complicato usarla da un'altra sceneggiatura. Ma esiste un pacchetto chiamato astunparse ( su github , su pypi ) che è fondamentalmente una versione di unparse.py.
mbdevpl,

Potresti forse aggiornare la tua risposta aggiungendo parso come opzione preferita? È molto buono e aggiornato.
scatola il

59

Il modulo ast incorporato non sembra avere un metodo per riconvertire in sorgente. Tuttavia, il modulo codegen qui fornisce una stampante carina per l'ast che ti consentirebbe di farlo. per esempio.

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

Questo stamperà:

def foo():
    return 42

Tieni presente che potresti perdere la formattazione e i commenti esatti, poiché questi non vengono conservati.

Tuttavia, potrebbe non essere necessario. Se tutto ciò che serve è eseguire l'AST sostituito, è possibile farlo semplicemente chiamando compile () sull'ast ed eseguendo l'oggetto codice risultante.


20
Solo per chiunque lo usi in futuro, codegen è in gran parte obsoleto e presenta alcuni bug. Ne ho riparati un paio; Ho questo come un riassunto
mattbasta

Si noti che l'ultimo codegen è stato aggiornato nel 2012, dopo il commento sopra, quindi immagino che codegen sia stato aggiornato. @mattbasta
zjffdu,

4
astor sembra essere un successore mantenuto di
codegen

20

In una risposta diversa, ho suggerito di utilizzare il astorpacchetto, ma da allora ho trovato un pacchetto di analisi non aggiornato AST chiamato astunparse:

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


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

Ho provato questo su Python 3.5.


19

Potrebbe non essere necessario rigenerare il codice sorgente. Questo è un po 'pericoloso per me, ovviamente, dal momento che non hai ancora spiegato perché pensi di dover generare un file .py pieno di codice; ma:

  • Se vuoi generare un file .py che le persone useranno effettivamente, forse in modo che possano compilare un modulo e ottenere un utile file .py da inserire nel loro progetto, allora non vuoi cambiarlo in un AST e indietro perché perderai tutta la formattazione (pensa alle righe vuote che rendono Python così leggibile raggruppando insieme set di linee correlate) (i nodi ast hanno linenoe col_offsetattributi ) commenti. Invece, probabilmente vorrai usare un motore di template (il linguaggio dei template di Django , per esempio, è progettato per rendere semplice il template anche dei file di testo) per personalizzare il file .py, oppure usare l' estensione MetaPython di Rick Copeland .

  • Se si sta tentando di apportare una modifica durante la compilazione di un modulo, tenere presente che non è necessario tornare completamente al testo; puoi semplicemente compilare direttamente l'AST invece di trasformarlo in un file .py.

  • Ma in quasi tutti i casi, probabilmente stai provando a fare qualcosa di dinamico che un linguaggio come Python rende davvero molto semplice, senza scrivere nuovi file .py! Se espandi la tua domanda per farci sapere cosa vuoi veramente realizzare, i nuovi file .py probabilmente non saranno coinvolti nella risposta; Ho visto centinaia di progetti Python che eseguono centinaia di cose nel mondo reale, e nessuno di loro ha mai avuto bisogno di scrivere un file .py. Quindi, devo ammettere, sono un po 'scettico che hai trovato il primo buon caso d'uso. :-)

Aggiornamento: ora che hai spiegato cosa stai cercando di fare, sarei tentato di operare comunque sull'AST. Vorrai mutare rimuovendo, non le righe di un file (che potrebbero tradursi in mezze dichiarazioni che muoiono semplicemente con un SyntaxError), ma intere dichiarazioni - e quale posto migliore per farlo che nell'AST?


Buona panoramica della possibile soluzione e delle possibili alternative.
Ryan,

1
Caso d'uso reale per la generazione di codice: Kid e Genshi (credo) generano Python da modelli XML per il rendering rapido di pagine dinamiche.
Rick Copeland,

10

L'analisi e la modifica della struttura del codice è certamente possibile con l'aiuto del astmodulo e lo mostrerò in un esempio tra un momento. Tuttavia, non è possibile riscrivere il codice sorgente modificato con il astsolo modulo. Ci sono altri moduli disponibili per questo lavoro come uno qui .

NOTA: L'esempio che segue può essere trattato come un tutorial introduttivo sull'uso del astmodulo, ma una guida più completa sull'uso del astmodulo è disponibile qui nel tutorial sui serpenti Green Tree e nella documentazione ufficiale sul astmodulo .

Introduzione a ast:

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

Puoi analizzare il codice Python (rappresentato in stringa) semplicemente chiamando l'API ast.parse(). Ciò restituisce l'handle alla struttura AST (Abstract Syntax Tree). È interessante notare che è possibile compilare nuovamente questa struttura ed eseguirla come mostrato sopra.

Un'altra API molto utile è quella ast.dump()che scarica l'intero AST in una forma di stringa. Può essere utilizzato per ispezionare la struttura ad albero ed è molto utile nel debug. Per esempio,

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

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

Notare la differenza nella sintassi per l'istruzione print in Python 2.7 rispetto a Python 3.5 e la differenza nel tipo di nodo AST nei rispettivi alberi.


Come modificare il codice usando ast:

Ora diamo un'occhiata a un esempio di modifica del codice Python per astmodulo. Lo strumento principale per modificare la struttura AST è la ast.NodeTransformerclasse. Ogni volta che è necessario modificare l'AST, è necessario sottoclassarlo e scrivere di conseguenza la trasformazione o le trasformazioni dei nodi.

Per il nostro esempio, proviamo a scrivere una semplice utility che trasforma Python 2, stampa le istruzioni in chiamate di funzione Python 3.

Stampa l'istruzione sull'utilità di conversione delle chiamate divertenti: 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])

Questa utility può essere provata su un piccolo file di esempio, come quello qui sotto, e dovrebbe funzionare bene.

Test file di input: 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()

Si noti che la trasformazione di cui sopra è solo a astscopo di esercitazione e nel caso reale si dovrà esaminare tutti i diversi scenari come print " x is %s" % ("Hello Python").


6

Di recente ho creato un pezzo di codice abbastanza stabile (il core è davvero ben testato) ed estensibile che genera codice astdall'albero: https://github.com/paluh/code-formatter .

Sto usando il mio progetto come base per un piccolo plugin vim (che sto usando tutti i giorni), quindi il mio obiettivo è generare codice Python davvero piacevole e leggibile.

PS Ho provato ad estenderlo codegenma la sua architettura si basa ast.NodeVisitorsull'interfaccia, quindi i formattatori ( visitor_metodi) sono solo funzioni. Ho trovato questa struttura abbastanza limitante e difficile da ottimizzare (in caso di espressioni lunghe e nidificate è più facile mantenere l'albero degli oggetti e memorizzare alcuni risultati parziali - in altro modo puoi colpire la complessità esponenziale se vuoi cercare il miglior layout). MA codegen come ogni opera di mitsuhiko (che ho letto) è molto ben scritta e concisa.


4

Una delle altre risposte raccomanda codegen, che sembra essere stata sostituita da astor. La versione di astorsu PyPI (versione 0.5 al momento della stesura di questo documento) sembra essere anche un po 'datata, quindi puoi installare la versione di sviluppo astorcome segue.

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

Quindi puoi usare astor.to_sourceper convertire un AST Python in codice sorgente Python leggibile dall'uomo:

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

Ho provato questo su Python 3.5.


4

Se lo stai guardando nel 2019, puoi usare questo pacchetto libcst . Ha una sintassi simile a ast. Funziona come un incantesimo e preserva la struttura del codice. È sostanzialmente utile per il progetto in cui devi conservare commenti, spazi bianchi, newline ecc.

Se non hai bisogno di preoccuparti dei commenti conservativi, degli spazi bianchi e di altri, allora la combinazione di ast e astor funziona bene.


2

Avevamo un bisogno simile, che non è stato risolto da altre risposte qui. Quindi abbiamo creato una libreria per questo, ASTTokens , che prende un albero AST prodotto con i moduli ast o astroid e lo contrassegna con gli intervalli di testo nel codice sorgente originale.

Non modifica direttamente il codice, ma non è difficile aggiungere in cima, poiché ti dice l'intervallo di testo che devi modificare.

Ad esempio, questo avvolge una chiamata di funzione WRAP(...), preservando i commenti e tutto il resto:

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

Spero che questo ti aiuti!


1

Un sistema di trasformazione del programma è uno strumento che analizza il testo di origine, crea AST, consente di modificarli utilizzando trasformazioni da origine a fonte ("se vedi questo modello, sostituiscilo con quel modello"). Tali strumenti sono ideali per eseguire la mutazione dei codici sorgente esistenti, che sono solo "se vedi questo modello, sostituiscilo con una variante di modello".

Ovviamente, hai bisogno di un motore di trasformazione del programma in grado di analizzare il linguaggio che ti interessa e fare comunque le trasformazioni dirette al modello. Il nostro DMS Reengineering Toolkit è un sistema in grado di farlo e gestisce Python e una varietà di altre lingue.

Guarda questo risposta SO per un esempio di un AST analizzato da DMS per Python che acquisisce i commenti con precisione. DMS può apportare modifiche all'AST e rigenerare testo valido, inclusi i commenti. Puoi chiedergli di stampare piuttosto l'AST, usando le sue convenzioni di formattazione (puoi cambiarle), o fare "stampa fedele", che usa le informazioni sulla linea e la colonna originali per preservare al massimo il layout originale (alcune modifiche al layout in cui il nuovo codice è inserito è inevitabile).

Per implementare una regola di "mutazione" per Python con DMS, è possibile scrivere quanto segue:

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

Questa regola sostituisce "+" con "-" in modo sintatticamente corretto; funziona su AST e quindi non toccherà stringhe o commenti che sembrano avere ragione. La condizione aggiuntiva su "mutate_this_place" è quella di consentire di controllare la frequenza con cui ciò si verifica; non vuoi mutare ogni posto nel programma.

Ovviamente vorresti un sacco di regole come questa che rilevano varie strutture di codice e le sostituiscono con le versioni mutate. DMS è felice di applicare un insieme di regole. L'AST mutato è quindi piuttosto stampato.


Non vedo questa risposta da 4 anni. Wow, è stato sottoposto a downgrade più volte. È davvero sorprendente, dal momento che risponde direttamente alla domanda di OP, e mostra persino come fare le mutazioni che vuole fare. Immagino che nessuno dei downvoter si preoccuperebbe di spiegare il motivo per cui ha effettuato il downgrade.
Ira Baxter,

4
Perché promuove uno strumento molto costoso e di origine chiusa.
Zoran Pavlovic,

@ZoranPavlovic: Quindi non ti stai opponendo alla sua precisione tecnica o utilità?
Ira Baxter,

2
@Zoran: non ha detto di avere una libreria open source. Ha detto che voleva modificare il codice sorgente di Python (usando gli AST) e che le soluzioni che riusciva a trovare non lo facevano. Questa è una soluzione del genere. Non pensi che le persone utilizzino strumenti commerciali su programmi scritti in lingue come Python su Java?
Ira Baxter,

1
Non sono un votante negativo, ma il post è un po 'come un annuncio pubblicitario. Per migliorare la risposta, potresti rivelare che sei affiliato al prodotto
entro il

0

Usavo il barone per questo, ma ora sono passato a Parso perché è aggiornato con il moderno Python. Funziona benissimo.

Ne avevo bisogno anche per un tester di mutazione. È davvero abbastanza semplice crearne uno con parso, controlla il mio codice su https://github.com/boxed/mutmut

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.