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

168

Chcę programowo edytować kod źródłowy Pythona. Zasadniczo chcę przeczytać .pyplik, wygenerować AST , a następnie zapisać z powrotem zmodyfikowany kod źródłowy Pythona (tj. Inny .pyplik).

Istnieją sposoby analizowania / kompilowania kodu źródłowego języka Python przy użyciu standardowych modułów języka Python, takich jak astlub compiler. Jednak nie sądzę, aby którykolwiek z nich obsługiwał sposoby modyfikacji kodu źródłowego (np. Usunięcie tej deklaracji funkcji), a następnie zapisanie modyfikującego kodu źródłowego Pythona.

AKTUALIZACJA: Powodem, dla którego chcę to zrobić, jest to, że chciałbym napisać bibliotekę testującą mutacje dla Pythona, głównie poprzez usuwanie instrukcji / wyrażeń, ponowne uruchamianie testów i sprawdzanie, co się psuje.

Rory
źródło
4
Przestarzałe od wersji 2.6: pakiet kompilatora został usunięty w Pythonie 3.0.
dfa
1
Czego nie możesz edytować źródła? Dlaczego nie możesz napisać dekoratora?
S.Lott
3
Święta krowa! Chciałem stworzyć tester mutacji dla Pythona przy użyciu tej samej techniki (konkretnie tworząc wtyczkę do nosa), czy planujesz go pozyskać z otwartego źródła?
Ryan
2
@Ryan Yeah, będę otwierać wszystko, co stworzę. Powinniśmy pozostać w kontakcie w tej sprawie
Rory
1
Zdecydowanie wysłałem Ci wiadomość e-mail za pośrednictwem Launchpada.
Ryan

Odpowiedzi:

73

Pythoscope robi to na przypadkach testowych, które generuje automatycznie, podobnie jak narzędzie 2to3 dla Pythona 2.6 (konwertuje źródło pythona 2.x na źródło python 3.x).

Oba te narzędzia wykorzystują bibliotekę lib2to3, która jest implementacją parsera / kompilatora języka Python, który może zachować komentarze w źródle, gdy jest w obie strony ze źródła -> AST -> źródło.

Projekt liny może zaspokoić Twoje potrzeby, jeśli chcesz wykonać więcej refaktoryzacji, np. Transformacje.

Ast moduł to inna opcja, a nie starszy przykład jak „unparse” drzew składniowych powrotem do kodu (za pomocą modułu analizatora). Ale astmoduł jest bardziej przydatny podczas wykonywania transformacji AST na kodzie, który jest następnie przekształcany w obiekt kodu.

Projekt Redbaron również może być dobrym rozwiązaniem (ht Xavier Combelle)

Ryan
źródło
5
nieprecyzyjny przykład jest nadal utrzymywany, oto zaktualizowana wersja py3k
Janus Troelsen
2
Jeśli chodzi o unparse.pyskrypt - użycie go z innego skryptu może być naprawdę uciążliwe. Ale istnieje pakiet o nazwie astunparse ( na github , na pypi ), który jest właściwie spakowaną wersją unparse.py.
mbdevpl
Czy mógłbyś zaktualizować swoją odpowiedź, dodając parso jako preferowaną opcję? Jest bardzo dobry i zaktualizowany.
zapakowany
59

Wydaje się, że wbudowany moduł AST nie ma metody konwersji z powrotem do źródła. Jednak moduł codegen zapewnia ładną drukarkę dla ast, która pozwoli ci to zrobić. na przykład.

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

Spowoduje to wydrukowanie:

def foo():
    return 42

Pamiętaj, że możesz utracić dokładne formatowanie i komentarze, ponieważ nie są one zachowywane.

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

Brian
źródło
20
Tylko dla każdego, kto użyje tego w przyszłości, codegen jest w dużej mierze nieaktualny i ma kilka błędów. Naprawiłem kilka z nich; Mam to jako sedno na github: gist.github.com/791312
mattbasta
Zauważ, że najnowszy codegen został zaktualizowany w 2012 r., Czyli po powyższym komentarzu, więc myślę, że codegen jest zaktualizowany. @mattbasta
zjffdu
4
astor wydaje się być zachowanym następcą codegen
medmunds.
20

W innej odpowiedzi zasugerowałem użycie tego astorpakietu, ale od tego czasu znalazłem bardziej aktualny pakiet do un-parsowania AST o nazwie astunparse:

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


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

Przetestowałem to w Pythonie 3.5.

argentpepper
źródło
19

Może nie być konieczne ponowne generowanie kodu źródłowego. Oczywiście to trochę niebezpieczne, ponieważ nie wyjaśniłeś, dlaczego myślisz, że musisz wygenerować plik .py pełen kodu; ale:

  • Jeśli chcesz wygenerować plik .py, którego ludzie będą faktycznie używać, na przykład po to, aby mogli wypełnić formularz i uzyskać przydatny plik .py do wstawienia do swojego projektu, nie chcesz go zamieniać na AST i z powrotem, ponieważ stracisz całe formatowanie (pomyśl o pustych wierszach, które sprawiają, że Python jest tak czytelny dzięki grupowaniu powiązanych zestawów wierszy) ( węzły ast mają linenoi col_offsetatrybuty ) komentarze. Zamiast tego prawdopodobnie będziesz chciał użyć silnika tworzenia szablonów ( na przykład język szablonów Django został zaprojektowany tak, aby ułatwić tworzenie szablonów nawet plików tekstowych), aby dostosować plik .py lub użyć rozszerzenia MetaPython Ricka Copelanda .

  • Jeśli próbujesz dokonać zmiany podczas kompilacji modułu, pamiętaj, ż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 w rzeczywistości ułatwia, bez pisania nowych plików .py! Jeśli rozszerzysz swoje pytanie, aby poinformować nas, co tak naprawdę chcesz osiągnąć, nowe pliki .py prawdopodobnie nie będą w ogóle zaangażowane w odpowiedź; Widziałem setki projektów Pythona wykonujących setki rzeczy w prawdziwym świecie i żaden z nich nie był potrzebny do napisania pliku .py. Muszę więc przyznać, że jestem trochę sceptykiem, że znalazłeś pierwszy dobry przypadek użycia. :-)

Aktualizacja: teraz, gdy wyjaśniłeś, co próbujesz zrobić, i tak bym się kusił, aby po prostu operować na AST. Będziesz chciał mutować, usuwając nie wiersze pliku (co może skutkować połowicznymi instrukcjami, które po prostu umierają z błędem SyntaxError), ale całymi instrukcjami - a jakie jest lepsze miejsce do zrobienia tego niż w AST?

Brandon Rhodes
źródło
Dobry przegląd możliwych rozwiązań i prawdopodobnych alternatyw.
Ryan
1
Prawdziwy przypadek użycia do generowania kodu: Kid i Genshi (wierzę) generują Python z szablonów XML w celu szybkiego renderowania dynamicznych stron.
Rick Copeland
10

Przetwarzanie i modyfikowanie struktury kodu jest na pewno możliwe przy pomocy astmodułu i za chwilę pokażę to na przykładzie. Jednak zapis zmodyfikowanego kodu źródłowego nie jest możliwy w przypadku astsamego modułu. Istnieją inne moduły dostępne do tego zadania, takie jak jeden tutaj .

UWAGA: Poniższy przykład może być traktowany jako wprowadzenie do korzystania z astmodułu, ale bardziej obszerny przewodnik na temat korzystania z astmodułu jest dostępny tutaj w samouczku Green Tree snakes i oficjalnej dokumentacji astmodułu .

Wprowadzenie do ast:

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

Możesz przeanalizować kod Pythona (przedstawiony w postaci ciągu znaków), po prostu wywołując interfejs API ast.parse(). To zwraca uchwyt do struktury abstrakcyjnego drzewa składni (AST). Co ciekawe, możesz skompilować tę strukturę i wykonać ją, jak pokazano powyżej.

Innym bardzo przydatnym interfejsem API jest ast.dump()zrzucanie całego AST w postaci łańcucha. Może być używany do sprawdzania struktury drzewa i jest bardzo pomocny w debugowaniu. Na przykład,

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

W 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 i różnicę w typie węzła AST w odpowiednich drzewach.


Jak zmodyfikować kod za pomocą ast:

Przyjrzyjmy się teraz przykładowi modyfikacji kodu w Pythonie według astmodułu. Głównym narzędziem do modyfikowania struktury AST jest ast.NodeTransformerklasa. Ilekroć trzeba zmodyfikować AST, musi utworzyć podklasę z niej i odpowiednio napisać transformację (e) węzła.

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

Wydrukuj instrukcję do narzędzia konwertera połączeń 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])

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

Plik wejściowy testu: 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 w celach astsamouczkowych iw prawdziwym przypadku trzeba będzie spojrzeć na wszystkie różne scenariusze, takie jak print " x is %s" % ("Hello Python").

ViFI
źródło
6

Stworzyłem ostatnio całkiem stabilny (rdzeń jest naprawdę dobrze przetestowany) i rozszerzalny fragment kodu, który generuje kod z astdrzewa: 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 w Pythonie.

PS Próbowałem rozszerzyć, codegenale jego architektura jest oparta na ast.NodeVisitorinterfejsie, więc elementy formatujące ( visitor_metody) to tylko funkcje. Uważam, że ta struktura jest dość ograniczona i trudna do optymalizacji (w przypadku długich i zagnieżdżonych wyrażeń łatwiej jest zachować drzewo obiektów i buforować niektóre częściowe wyniki - w inny sposób można osiągnąć wykładniczą złożoność, jeśli chcesz wyszukać najlepszy układ). ALE codegen jak każda praca mitsuhiko (którą przeczytałem) jest bardzo dobrze napisana i zwięzła.

paluh
źródło
4

Jedna z pozostałych odpowiedzi zaleca codegen, która wydaje się być zastąpiona przez astor. Wersja astorna PyPI (wersja 0.5 w chwili pisania tego tekstu) również wydaje się być nieco przestarzała, więc możesz zainstalować wersję rozwojową w astornastępujący sposób.

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

Następnie możesz użyć astor.to_sourcedo przekonwertowania Pythona AST 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

Przetestowałem to w Pythonie 3.5.

argentpepper
źródło
4

Jeśli patrzysz na to w 2019 roku, możesz użyć tego pakietu libcst . Ma składnię podobną do ast. Działa to jak urok i pozwala zachować strukturę kodu. Zasadniczo jest to pomocne w przypadku projektu, w którym musisz zachować komentarze, spacje, nową linię itp.

Jeśli nie musisz przejmować się zachowywaniem komentarzy, spacji i innych, dobrze działa kombinacja ast i astor .

Saurav Gharti
źródło
2

Mieliśmy podobną potrzebę, której nie rozwiązały inne odpowiedzi tutaj. Dlatego stworzyliśmy do tego bibliotekę, ASTTokens , która pobiera drzewo AST utworzone za pomocą modułów ast lub astroid i oznacza je zakresami tekstu w oryginalnym kodzie źródłowym.

Nie modyfikuje kodu bezpośrednio, ale nie jest to trudne do dodania, ponieważ informuje o zakresie tekstu, który musisz zmodyfikować.

Na przykład otacza wywołanie funkcji 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!

DS.
źródło
1

Transformacji ustrojowej Program jest narzędziem, które tekst źródłowy parsowań buduje ASTs, pozwala modyfikować je za źródło-źródło przemiany ( „jeśli widzisz ten wzór, wymień go do wzorca”). Takie narzędzia są idealne do wykonywania mutacji istniejących kodów źródłowych, które są po prostu „jeśli widzisz ten wzorzec, zastąp go wariantem wzorca”.

Oczywiście potrzebujesz mechanizmu transformacji programu, który może analizować język, który Cię interesuje, i nadal wykonywać transformacje sterowane wzorcem. Nasz DMS Software Reengineering Toolkit to system, który może to zrobić i obsługuje Python i wiele innych języków.

Zobacz tę odpowiedź SO, aby zobaczyć przykład przetworzonej przez DMS AST do przechwytywania komentarzy w Pythonie dokładnie . DMS może wprowadzać zmiany w AST i ponownie generować prawidłowy tekst, w tym komentarze. Możesz poprosić go o ładne wydrukowanie AST, używając jego własnych konwencji formatowania (możesz je zmienić) lub zrobić "drukowanie wierne", które wykorzystuje oryginalne informacje o wierszu i kolumnie, aby maksymalnie zachować oryginalny układ (pewna zmiana w układzie, gdy nowy kod jest włożona jest nieunikniona).

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 zamienia „+” na „-” w poprawny składniowo sposób; działa na AST i dlatego nie dotyka napisów ani komentarzy, które wyglądają dobrze. Dodatkowym warunkiem na „mutate_this_place” jest umożliwienie kontroli, jak często to się dzieje; nie chcesz zmieniać każdego miejsca w programie.

Oczywiście chciałbyś mieć więcej reguł, takich jak ta, które wykrywają różne struktury kodu i zastępują je zmutowanymi wersjami. DMS z przyjemnością stosuje zestaw zasad. Zmutowane AST jest następnie ładnie wydrukowane.

Ira Baxter
źródło
Nie patrzyłem na tę odpowiedź od 4 lat. Wow, zostało to kilkakrotnie odrzucone. To naprawdę oszałamiające, ponieważ bezpośrednio odpowiada na pytanie OP, a nawet pokazuje, jak robić mutacje, które chce zrobić. Nie sądzę, żeby którykolwiek z tych, którzy go odrzucili, miałby ochotę wyjaśnić, dlaczego przegłosowali.
Ira Baxter
4
Ponieważ promuje bardzo drogie narzędzie o zamkniętym kodzie źródłowym.
Zoran Pavlovic
@ZoranPavlovic: Więc nie sprzeciwiasz się żadnej technicznej poprawności ani użyteczności?
Ira Baxter
2
@Zoran: Nie powiedział, że ma bibliotekę open source. Powiedział, że chce zmodyfikować kod źródłowy Pythona (przy użyciu AST), a rozwiązania, które mógł znaleźć, tego nie zrobiły. To jest takie rozwiązanie. Nie sądzisz, że ludzie używają komercyjnych narzędzi w programach napisanych w językach takich jak Python w Javie?
Ira Baxter
1
Nie jestem zwolennikiem osłabienia, ale post brzmi trochę jak reklama. Aby poprawić odpowiedź, możesz ujawnić, że jesteś powiązany z produktem
wim
0

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

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

zapakowane
źródło