Parse a.py plik, przeczytaj AST, zmodyfikuj go, a następnie Odpisz zmodyfikowany kod źródłowy

Chcę programowo edytować kod źródłowy Pythona. Zasadniczo chcę przeczytać plik .py, wygenerować AST , a następnie napisać zmodyfikowany kod źródłowy Pythona (tj. inny plik .py).

Istnieją sposoby parsowania / kompilacji kodu źródłowego Pythona przy użyciu standardowych modułów Pythona, takich jak ast lub compiler. Nie sądzę jednak, aby któryś z nich wspierał sposoby modyfikacji kodu źródłowego (np. delete this function declaration) a następnie odpisywania modyfikacji kod źródłowy Pythona.

UPDATE: powodem, dla którego chcę to zrobić, jest to, że chciałbym napisać bibliotekę testowania mutacji dla Pythona, głównie przez usuwanie wyrażeń / wyrażeń, ponowne uruchamianie testów i sprawdzanie, co się psuje.

Author: Jonathan Leffler, 2009-04-20

10 answers

Pythoscope robi to dla przypadków testowych, które automatycznie generuje, podobnie jak narzędzie 2to3 dla Pythona 2.6 (konwertuje Pythona 2.źródło X do Pythona 3.x źródło).

Oba te narzędzia wykorzystują bibliotekę lib2to3 , która jest implementacją parsera/kompilatora Pythona, który może zachować komentarze w źródle, gdy jest okrągły potknięty z source -> AST -> source.

Projekt liny może zaspokoić Twoje potrzeby, jeśli chcesz zrobić więcej refaktoryzacja jak transformacja.

The Ast module is your other option, and there 's an older example of how to "unparse" syntax trees back into code (using the parser module). Ale moduł ast jest bardziej przydatny podczas transformacji AST na kodzie, który jest następnie przekształcany w obiekt kodu.

Projekt redbaron również może być dobrym dopasowaniem (http: / / ]}

 65
Author: Ryan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-18 22:31:23

Wbudowany Moduł ast nie wydaje się mieć metody konwersji z powrotem do źródła. Jednak moduł codegen zapewnia ładną drukarkę dla ast, która umożliwi Ci to. np.

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

To wydrukuje:

def foo():
    return 42

Zauważ, że możesz utracić dokładne formatowanie i komentarze, ponieważ nie są one zachowane.

Jednak może nie trzeba. Jeśli wszystko czego potrzebujesz to wykonanie zastąpionego AST, możesz to zrobić po prostu wywołując compile () na ast i wykonanie wynikowego obiektu kodu.

 55
Author: Brian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-23 18:10:15

Nie musisz ponownie generować kodu źródłowego. To trochę niebezpieczne dla mnie, aby powiedzieć, oczywiście, ponieważ tak naprawdę nie wyjaśniłeś, dlaczego uważasz, że musisz wygenerować plik .py pełen kodu; ale: {]}

  • Jeśli chcesz wygenerować plik. py, którego ludzie będą faktycznie używać, może po to, aby mogli wypełnić formularz i uzyskać przydatny plik. py do wstawienia do swojego projektu, to nie chcesz go zmieniać na AST i z powrotem, ponieważ stracisz {7]} wszystkie formatowanie (pomyśl o tym, jak puste linie, które sprawiają, że Python jest tak czytelny, grupując powiązane zestawy wierszy razem) (węzły ast posiadają atrybuty lineno i col_offset ). Zamiast tego prawdopodobnie będziesz chciał użyć silnika szablonów (język szablonów Django , na przykład, jest zaprojektowany, aby ułatwić tworzenie szablonów nawet plików tekstowych), aby dostosować plik .py, lub użyć rozszerzenia MetaPython MetaPython Ricka Copelanda.

  • Jeśli próbujesz dokonać zmiany podczas kompilacji modułu, zauważ, że nie musisz wracać do tekstu; możesz po prostu skompilować AST bezpośrednio zamiast zamieniać go z powrotem w plik. py.

  • Ale w prawie każdym przypadku prawdopodobnie próbujesz zrobić coś dynamicznego, co język taki jak Python naprawdę ułatwia, bez pisania nowych plików .py! Jeśli rozszerzysz swoje pytanie, aby dać nam znać, co naprawdę chcesz osiągnąć, nowe pliki. py prawdopodobnie nie będą w ogóle zaangażowane w odpowiedź; widziałem setki Python projects robi setki rzeczywistych rzeczy, a żadna z nich nie jest potrzebna do napisania pliku. py. Muszę przyznać, że jestem trochę sceptyczny, że znalazłeś pierwszy dobry przypadek użycia. :-)

Update: teraz, gdy już wyjaśniłeś, co próbujesz zrobić, i tak bym się skusił, aby po prostu operować na AST. Będziesz chciał zmutować usuwając, a nie linie pliku (co może spowodować pół-wypowiedzi, które po prostu umrą z błędem składniowym), ale całe wypowiedzi - a co lepsze miejsce na to niż w AST?

 20
Author: Brandon Rhodes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-21 21:41:45

W innej odpowiedzi zaproponowałem użycie pakietu astor, ale od tego czasu znalazłem bardziej aktualny pakiet AST un-parsing o nazwie astunparse:

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


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

Testowałem to na Pythonie 3.5.

 14
Author: argentpepper,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-09 13:52:09

Stworzyłem ostatnio dość stabilny (core jest naprawdę dobrze przetestowany) i rozszerzalny fragment kodu, który generuje kod z drzewa ast: https://github.com/paluh/code-formatter .

Używam mojego projektu jako bazy dla małej wtyczki vim (której używam na co dzień), więc moim celem jest wygenerowanie naprawdę ładnego i czytelnego kodu Pythona.

P. S. Próbowałem rozszerzyć codegen, ale jego architektura opiera się na interfejsie ast.NodeVisitor, więc formatery (metodyvisitor_) to tylko funkcje. Ja struktura ta jest dość ograniczająca i trudna do optymalizacji (w przypadku długich i zagnieżdżonych wyrażeń łatwiej jest zachować drzewo obiektów i buforować częściowe wyniki-w inny sposób można uzyskać wykładniczą złożoność, jeśli chcesz wyszukać najlepszy układ). Ale codegen Jak każdy fragment pracy mitsuhiko (który przeczytałem) jest bardzo dobrze napisany i zwięzły.

 6
Author: paluh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-08 12:52:16

Parsowanie i modyfikowanie struktury kodu jest z pewnością możliwe za pomocą modułu ast i pokażę to w przykładzie za chwilę. Jednak zapisanie zmodyfikowanego kodu źródłowego nie jest możliwe tylko za pomocą modułu ast. Dostępne są inne moduły do tego zadania, takie jak jeden tutaj .

Uwaga: poniższy przykład może być traktowany jako wstępny samouczek na temat korzystania z modułu ast, ale bardziej wyczerpujący przewodnik na temat korzystania z modułu ast jest dostępny tutaj Green Tree snakes tutorial i oficjalna dokumentacja ast modułu.

Wprowadzenie do ast:

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

Możesz przetworzyć kod Pythona (reprezentowany w łańcuchu znaków) po prostu wywołując API ast.parse(). Zwraca uchwyt do struktury drzewa abstrakcyjnej składni (AST). Co ciekawe, możesz skompilować tę strukturę i wykonać ją jak pokazano powyżej.

Innym bardzo użytecznym API jest ast.dump(), który zrzuca cały AST w postaci ciągu znaków. Może służy do sprawdzania struktury drzewa i jest bardzo pomocny w debugowaniu. Na przykład,

Na Pythonie 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)])"

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

Zwróć uwagę na różnicę w składni instrukcji print w Pythonie 2.7 w porównaniu z Pythonem 3.5 oraz różnicę w typie węzła AST w odpowiednich drzewach.


Jak zmodyfikować kod za pomocą ast:

Teraz rzućmy okiem na przykład modyfikacji kodu Pythona przez moduł ast. Głównym narzędziem modyfikacji struktury AST jest klasa ast.NodeTransformer. Ilekroć trzeba zmodyfikować AST, trzeba z niego podklasować i odpowiednio zapisać transformację węzła.

W naszym przykładzie spróbujmy napisać proste narzędzie, które przekształca polecenia Python 2 , print na wywołania funkcji Python 3.

Wydrukuj oświadczenie do Narzędzia Fun call converter: 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])

To narzędzie można wypróbować na małym przykładowym pliku, takim jak jeden poniżej, i powinno działać dobrze.

Testowy plik wejściowy : 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()

Należy pamiętać, że powyższa transformacja jest tylko dla ast w celu tutoriala i w prawdziwym przypadku trzeba będzie przyjrzeć się wszystkim różnym scenariuszom, takim jak print " x is %s" % ("Hello Python").

 6
Author: ViFI,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-23 08:38:57

Jedna z innych odpowiedzi poleca codegen, która wydaje się być zastąpiona przez astor. Wersja astor Na PyPI (Wersja 0.5 W chwili pisania tego tekstu) wydaje się być trochę nieaktualna, więc możesz zainstalować wersję rozwojową astor w następujący sposób.

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

Następnie możesz użyć astor.to_source, Aby przekonwertować AST Pythona na czytelny dla człowieka kod źródłowy Pythona:

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

Testowałem to na Pythonie 3.5.

 3
Author: argentpepper,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:02:47

A Program Transformation System jest narzędziem, które analizuje tekst źródłowy, buduje ASTs, pozwala modyfikować je za pomocą transformacji source-to-source ("jeśli widzisz ten wzorzec, zamień go na ten wzorzec"). Takie narzędzia są idealne do wykonywania mutacji istniejących kodów źródłowych, które są po prostu "jeśli widzisz ten wzór, zastąp go wariantem wzorca".

Oczywiście potrzebujesz silnika transformacji programu, który może analizować interesujący Cię język i nadal wykonywać transformacje kierowane wzorem. Nasz DMS Software Reengineering Toolkit jest systemem, który może to zrobić i obsługuje Python i wiele innych języków.

Zobacz to więc odpowiedz na przykład DMS-parsed AST dla Pythona przechwytywania komentarzy dokładnie. DMS może wprowadzać zmiany w AST i regenerować poprawny tekst, w tym Komentarze. Możesz poprosić go o wydrukowanie AST, używając własnych Konwencji formatowania (możesz je zmienić), lub zrobić " wierność printing", który wykorzystuje informacje o oryginalnej linii i kolumnie, aby maksymalnie zachować oryginalny układ (pewne zmiany w układzie, w którym wstawiany jest nowy kod, są nieuniknione).

Aby zaimplementować regułę "mutacji" dla Pythona z DMS, możesz napisać:

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

Ta reguła zastępuje " + " na " - " w poprawny składniowo sposób; działa na AST i dlatego nie dotyka ciągów lub komentarzy, które wyglądają poprawnie. Dodatkowym warunkiem "mutate_this_place" jest umożliwienie kontroluj jak często to występuje; nie chcesz mutować Co miejsce w programie.

Oczywiście chciałbyś mieć więcej takich reguł, które wykrywają różne struktury kodu i zastępują je zmutowanymi wersjami. DMS chętnie stosuje zestaw reguł. Zmutowany AST jest następnie prettyprinted.

 2
Author: Ira Baxter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:02:47

Mieliśmy podobną potrzebę, która nie została rozwiązana przez inne odpowiedzi tutaj. Dlatego stworzyliśmy bibliotekę ASTTokens , która pobiera drzewo AST wytworzone z modułów ast lub astroid i oznacza je zakresami tekstu w oryginalnym kodzie źródłowym.

Nie dokonuje modyfikacji kodu bezpośrednio, ale nie jest to trudne do dodania na górze, ponieważ mówi ci zakres tekstu, który musisz zmodyfikować.

Na przykład, to zawija wywołanie funkcji w WRAP(...), zachowując komentarze i wszystko inne:

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

Produkuje:

def foo(): # Test
  '''My func'''
  WRAP(log("hello world"))  # Print
Mam nadzieję, że to pomoże!
 2
Author: DS.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-14 02:39:21

Kiedyś używałem do tego barona, ale teraz przełączyłem się na parso, ponieważ jest on aktualny z nowoczesnym Pythonem. Działa świetnie.

Potrzebowałem tego również do testera mutacji. To naprawdę bardzo proste, aby jeden z parso, sprawdź mój kod na https://github.com/boxed/mutmut

 0
Author: boxed,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-11 05:22:58