Analysieren Sie eine .py-Datei, lesen Sie den AST, ändern Sie ihn und schreiben Sie den geänderten Quellcode zurück

167

Ich möchte den Python-Quellcode programmgesteuert bearbeiten. Grundsätzlich möchte ich eine .pyDatei lesen , den AST generieren und dann den geänderten Python-Quellcode (dh eine andere .pyDatei) zurückschreiben .

Es gibt Möglichkeiten, Python-Quellcode mithilfe von Standard-Python-Modulen wie astoder zu analysieren / kompilieren compiler. Ich glaube jedoch nicht, dass einer von ihnen Möglichkeiten unterstützt, den Quellcode zu ändern (z. B. diese Funktionsdeklaration zu löschen) und dann den modifizierenden Python-Quellcode zurückzuschreiben.

UPDATE: Der Grund, warum ich dies tun möchte, ist, dass ich eine Mutationstestbibliothek für Python schreiben möchte , hauptsächlich indem ich Anweisungen / Ausdrücke lösche, Tests erneut ausführe und sehe, was kaputt geht.

Rory
quelle
4
Veraltet seit Version 2.6: Das Compiler-Paket wurde in Python 3.0 entfernt.
dfa
1
Was können Sie die Quelle nicht bearbeiten? Warum kannst du keinen Dekorateur schreiben?
S.Lott
3
Heiliger Bimbam! Ich wollte einen Mutationstester für Python mit der gleichen Technik erstellen (speziell ein Nasen-Plugin erstellen). Planen Sie Open Sourcing?
Ryan
2
@ Ryan Ja, ich werde alles, was ich erstelle, als Open Source anbieten. Wir sollten in Kontakt bleiben
Rory
1
Auf jeden Fall habe ich Ihnen eine E-Mail über Launchpad gesendet.
Ryan

Antworten:

73

Pythoscope führt dies für die Testfälle aus, die automatisch generiert werden, ebenso wie das 2to3- Tool für Python 2.6 (es konvertiert die Python 2.x-Quelle in die Python 3.x-Quelle).

Beide Tools verwenden die lib2to3- Bibliothek, eine Implementierung der Python-Parser / Compiler-Maschinerie, mit der Kommentare in der Quelle beibehalten werden können, wenn sie aus der Quelle -> AST -> Quelle ausgelöst wird.

Das Seilprojekt kann Ihren Anforderungen entsprechen, wenn Sie mehr Refactoring wie Transformationen durchführen möchten.

Das ast- Modul ist Ihre andere Option, und es gibt ein älteres Beispiel dafür, wie Syntaxbäume (mithilfe des Parser-Moduls) wieder in Code "analysiert" werden . Das astModul ist jedoch nützlicher, wenn eine AST-Transformation für Code durchgeführt wird, der dann in ein Codeobjekt umgewandelt wird.

Das Redbaron- Projekt könnte auch gut passen (ht Xavier Combelle)

Ryan
quelle
5
Das unparse Beispiel wird immer noch beibehalten. Hier ist die aktualisierte py3k-Version: hg.python.org/cpython/log/tip/Tools/parser/unparse.py
Janus Troelsen
2
In Bezug auf das unparse.pySkript kann es sehr umständlich sein, es aus einem anderen Skript zu verwenden. Aber es gibt ein Paket namens Astunparse ( auf Github , auf Pypi ), das im Grunde eine richtig verpackte Version von ist unparse.py.
mbdevpl
Könnten Sie Ihre Antwort möglicherweise aktualisieren, indem Sie parso als bevorzugte Option hinzufügen? Es ist sehr gut und aktualisiert.
Boxed
59

Das eingebaute Ast-Modul scheint keine Methode zum Zurückkonvertieren in die Quelle zu haben. Das Codegen- Modul hier bietet jedoch einen hübschen Drucker für den Ast, mit dem Sie dies tun können. z.B.

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

Dies wird gedruckt:

def foo():
    return 42

Beachten Sie, dass Sie möglicherweise die genaue Formatierung und die Kommentare verlieren, da diese nicht erhalten bleiben.

Möglicherweise müssen Sie dies jedoch nicht. Wenn Sie lediglich den ersetzten AST ausführen müssen, können Sie dies einfach tun, indem Sie compile () auf dem ast aufrufen und das resultierende Codeobjekt ausführen.

Brian
quelle
20
Nur für alle, die dies in Zukunft verwenden, ist Codegen weitgehend veraltet und weist einige Fehler auf. Ich habe ein paar davon repariert; Ich habe dies als Kern auf Github: gist.github.com/791312
Mattbasta
Beachten Sie, dass der neueste Codegen 2012 aktualisiert wird, und zwar nach dem obigen Kommentar. Ich denke, Codegen wird aktualisiert. @ Mattbasta
Zjffdu
3
astor scheint ein gepflegter Nachfolger von codegen
medmunds
19

Möglicherweise müssen Sie den Quellcode nicht neu generieren. Das ist natürlich ein bisschen gefährlich für mich, da Sie nicht erklärt haben, warum Sie denken, dass Sie eine .py-Datei voller Code generieren müssen. aber:

  • Wenn Sie eine .py-Datei generieren möchten, die tatsächlich verwendet wird, damit sie möglicherweise ein Formular ausfüllen und eine nützliche .py-Datei zum Einfügen in ihr Projekt erhalten können, möchten Sie sie nicht in eine AST- und eine AST-Datei ändern zurück, weil Sie alle Formatierungen verlieren (denken Sie an die Leerzeilen, die Python so lesbar machen, indem Sie verwandte Zeilensätze zusammen gruppieren) ( Astknoten haben linenound col_offsetAttribute ). Stattdessen möchten Sie wahrscheinlich eine Vorlagen- Engine verwenden (die Django-Vorlagensprache soll beispielsweise das Vorlagen - Erstellen von Textdateien vereinfachen), um die .py-Datei anzupassen, oder die MetaPython- Erweiterung von Rick Copeland verwenden .

  • Wenn Sie versuchen, während der Kompilierung eines Moduls Änderungen vorzunehmen, müssen Sie nicht vollständig zum Text zurückkehren. Sie können den AST einfach direkt kompilieren, anstatt ihn wieder in eine .py-Datei umzuwandeln.

  • Aber in fast jedem Fall versuchen Sie wahrscheinlich, etwas Dynamisches zu tun, das eine Sprache wie Python tatsächlich sehr einfach macht, ohne neue .py-Dateien zu schreiben! Wenn Sie Ihre Frage erweitern, um uns mitzuteilen, was Sie tatsächlich erreichen möchten, werden neue .py-Dateien wahrscheinlich überhaupt nicht in die Antwort einbezogen. Ich habe Hunderte von Python-Projekten gesehen, die Hunderte von realen Dingen ausgeführt haben, und kein einziges von ihnen musste jemals eine .py-Datei schreiben. Ich muss zugeben, ich bin ein bisschen skeptisch, dass Sie den ersten guten Anwendungsfall gefunden haben. :-)

Update: Nachdem Sie erklärt haben, was Sie tun möchten, wäre ich ohnehin versucht, nur mit dem AST zu arbeiten. Sie möchten mutieren, indem Sie nicht Zeilen einer Datei entfernen (was zu Halbanweisungen führen kann, die einfach mit einem SyntaxError sterben), sondern ganze Anweisungen - und wo kann man das besser tun als im AST?

Brandon Rhodes
quelle
Guter Überblick über mögliche Lösungen und wahrscheinliche Alternativen.
Ryan
1
Realer Anwendungsfall für die Codegenerierung: Kid und Genshi (glaube ich) generieren Python aus XML-Vorlagen für das schnelle Rendern dynamischer Seiten.
Rick Copeland
19

In einer anderen Antwort schlug ich vor, das astorPaket zu verwenden, aber seitdem habe ich ein aktuelleres AST-Entparsing-Paket mit dem Namen gefunden astunparse:

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


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

Ich habe dies auf Python 3.5 getestet.

Argentpepper
quelle
10

Das Parsen und Ändern der Codestruktur ist sicherlich mit Hilfe des astModuls möglich, und ich werde es gleich in einem Beispiel zeigen. Das Zurückschreiben des geänderten Quellcodes ist jedoch nicht nur mit dem astModul möglich . Für diesen Job stehen andere Module zur Verfügung, z. B. eines hier .

HINWEIS: Das folgende Beispiel kann als einführendes Tutorial zur Verwendung des astModuls behandelt werden. Eine umfassendere Anleitung zur Verwendung des astModuls finden Sie hier im Tutorial zu Green Tree Snakes und in der offiziellen Dokumentation zum astModul .

Einführung in ast:

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

Sie können den Python-Code (in Zeichenfolge dargestellt) analysieren, indem Sie einfach die API aufrufen ast.parse(). Dadurch wird das Handle an die AST-Struktur (Abstract Syntax Tree) zurückgegeben. Interessanterweise können Sie diese Struktur zurückkompilieren und wie oben gezeigt ausführen.

Eine weitere sehr nützliche API ist die, ast.dump()die den gesamten AST in einer Zeichenfolgenform ausgibt. Es kann zur Überprüfung der Baumstruktur verwendet werden und ist beim Debuggen sehr hilfreich. Beispielsweise,

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

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

Beachten Sie den Unterschied in der Syntax für die Druckanweisung in Python 2.7 gegenüber Python 3.5 und den Unterschied im Typ des AST-Knotens in den jeweiligen Bäumen.


So ändern Sie Code mithilfe von ast:

Schauen wir uns nun ein Beispiel für die Änderung des Python-Codes nach astModulen an. Das Hauptwerkzeug zum Ändern der AST-Struktur ist ast.NodeTransformerclass. Wann immer jemand den AST ändern muss, muss er eine Unterklasse daraus erstellen und die Knotenumwandlung (en) entsprechend schreiben.

In unserem Beispiel versuchen wir, ein einfaches Dienstprogramm zu schreiben, das die Python 2-Druckanweisungen in Python 3-Funktionsaufrufe umwandelt.

Dienstprogramm Print to Fun Call Converter drucken: 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])

Dieses Dienstprogramm kann für kleine Beispieldateien wie die folgende ausprobiert werden und sollte einwandfrei funktionieren.

Testeingabedatei: 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()

Bitte beachten Sie, dass die obige Transformation nur zu astLernzwecken dient und im realen Fall alle verschiedenen Szenarien betrachtet werden müssen, wie z print " x is %s" % ("Hello Python").

ViFI
quelle
6

Ich habe kürzlich einen ziemlich stabilen (Kern ist wirklich gut getestet) und erweiterbaren Code erstellt, der Code aus dem astBaum generiert : https://github.com/paluh/code-formatter .

Ich verwende mein Projekt als Basis für ein kleines Vim-Plugin (das ich jeden Tag verwende), daher ist es mein Ziel, wirklich schönen und lesbaren Python-Code zu generieren.

PS Ich habe versucht zu erweitern, codegenaber die Architektur basiert auf der ast.NodeVisitorSchnittstelle, daher sind Formatierer ( visitor_Methoden) nur Funktionen. Ich fand diese Struktur ziemlich einschränkend und schwer zu optimieren (bei langen und verschachtelten Ausdrücken ist es einfacher, den Objektbaum beizubehalten und einige Teilergebnisse zwischenzuspeichern - auf andere Weise können Sie die exponentielle Komplexität erreichen, wenn Sie nach dem besten Layout suchen möchten). ABER codegen da jedes Stück von Mitsuhikos Werk (das ich gelesen habe) sehr gut geschrieben und prägnant ist.

paluh
quelle
4

Eine der anderen Antworten empfiehlt codegen, die von abgelöst worden zu sein scheint astor. Die Version von astoron PyPI (Version 0.5 zum Zeitpunkt dieses Schreibens) scheint ebenfalls etwas veraltet zu sein, sodass Sie die Entwicklungsversion von astorwie folgt installieren können .

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

Anschließend können Sie astor.to_sourceeinen Python AST in lesbaren Python-Quellcode konvertieren:

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

Ich habe dies auf Python 3.5 getestet.

Argentpepper
quelle
4

Wenn Sie sich dies 2019 ansehen, können Sie dieses libcst- Paket verwenden. Es hat eine ähnliche Syntax wie ast. Dies funktioniert wie ein Zauber und behält die Codestruktur bei. Es ist grundsätzlich hilfreich für das Projekt, bei dem Sie Kommentare, Leerzeichen, Zeilenumbrüche usw. beibehalten müssen.

Wenn Sie sich nicht um die Beibehaltung von Kommentaren, Leerzeichen und anderen kümmern müssen, funktioniert die Kombination von Ast und Astor gut.

Saurav Gharti
quelle
2

Wir hatten ein ähnliches Bedürfnis, das durch andere Antworten hier nicht gelöst wurde. Deshalb haben wir hierfür eine Bibliothek erstellt, ASTTokens , die einen mit den Ast- oder Astroid- Modulen erstellten AST-Baum verwendet und ihn mit den Textbereichen im ursprünglichen Quellcode markiert.

Code wird nicht direkt geändert, aber das ist nicht schwer hinzuzufügen, da es Ihnen den Textbereich angibt, den Sie ändern müssen.

Dies schließt beispielsweise einen Funktionsaufruf ein WRAP(...), wobei Kommentare und alles andere erhalten bleiben:

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

Produziert:

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

Hoffe das hilft!

DS.
quelle
1

Ein Programmtransformationssystem ist ein Tool, das Quelltext analysiert, ASTs erstellt und es Ihnen ermöglicht, sie mithilfe von Quell-zu-Quell-Transformationen zu ändern ("Wenn Sie dieses Muster sehen, ersetzen Sie es durch dieses Muster"). Solche Tools sind ideal für die Mutation vorhandener Quellcodes, die nur "Wenn Sie dieses Muster sehen, durch eine Mustervariante ersetzen" sind.

Natürlich benötigen Sie eine Programmtransformations-Engine, die die für Sie interessante Sprache analysieren und dennoch die mustergesteuerten Transformationen durchführen kann. Unser DMS Software Reengineering Toolkit ist ein System, das dies kann und Python und eine Vielzahl anderer Sprachen verarbeitet.

In dieser SO-Antwort finden Sie ein Beispiel für einen DMS-analysierten AST für Python, der Kommentare genau erfasst . DMS kann Änderungen am AST vornehmen und gültigen Text einschließlich der Kommentare neu generieren. Sie können ihn bitten, den AST mit seinen eigenen Formatierungskonventionen zu drucken (Sie können diese ändern) oder "Fidelity Printing" ausführen, bei dem die ursprünglichen Zeilen- und Spalteninformationen verwendet werden, um das ursprüngliche Layout maximal beizubehalten (einige Änderungen im Layout, wenn neuer Code verwendet wird) eingefügt ist unvermeidlich).

Um eine "Mutations" -Regel für Python mit DMS zu implementieren, können Sie Folgendes schreiben:

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

Diese Regel ersetzt "+" syntaktisch korrekt durch "-". Es arbeitet mit dem AST und berührt daher keine Zeichenfolgen oder Kommentare, die zufällig richtig aussehen. Die zusätzliche Bedingung für "mutate_this_place" besteht darin, dass Sie steuern können, wie oft dies auftritt. Sie möchten nicht jeden Ort im Programm mutieren .

Offensichtlich möchten Sie eine Reihe weiterer Regeln wie diese, die verschiedene Codestrukturen erkennen und durch mutierte Versionen ersetzen. DMS wendet gerne eine Reihe von Regeln an. Der mutierte AST wird dann hübsch gedruckt.

Ira Baxter
quelle
Ich habe mir diese Antwort seit 4 Jahren nicht mehr angesehen. Wow, es wurde mehrmals herabgestimmt. Das ist wirklich erstaunlich, da es die Frage von OP direkt beantwortet und sogar zeigt, wie man die Mutationen macht, die er machen möchte. Ich nehme an, keiner der Downvoter würde gerne erklären, warum er Downvoting gemacht hat.
Ira Baxter
4
Weil es ein sehr teures Closed-Source-Tool fördert.
Zoran Pavlovic
@ZoranPavlovic: Sie haben also keine Einwände gegen die technische Genauigkeit oder Nützlichkeit?
Ira Baxter
2
@ Zoran: Er hat nicht gesagt, dass er eine Open Source Bibliothek hat. Er sagte, er wolle den Python-Quellcode (mithilfe von ASTs) ändern, und die Lösungen, die er finden konnte, taten dies nicht. Dies ist eine solche Lösung. Sie glauben nicht, dass Leute kommerzielle Tools für Programme verwenden, die in Sprachen wie Python auf Java geschrieben sind?
Ira Baxter
1
Ich bin kein Down-Voter, aber der Beitrag liest sich ein bisschen wie eine Werbung. Um die Antwort zu verbessern, können Sie offenlegen, dass Sie mit dem Produkt verbunden sind
wim
0

Ich habe früher Baron dafür verwendet, bin jetzt aber zu Parso gewechselt, weil es mit modernem Python auf dem neuesten Stand ist. Es funktioniert großartig.

Ich brauchte das auch für einen Mutationstester. Es ist wirklich ganz einfach, eine mit parso zu erstellen. Schauen Sie sich meinen Code unter https://github.com/boxed/mutmut an

verpackt
quelle