Разобрать файл .py, прочитать AST, изменить его, а затем записать измененный исходный код

168

Я хочу программно редактировать исходный код Python. По сути, я хочу прочитать .pyфайл, сгенерировать AST , а затем записать обратно измененный исходный код Python (т. Е. Другой .pyфайл).

Существуют способы синтаксического анализа / компиляции исходного кода Python с использованием стандартных модулей Python, таких как astили compiler. Однако я не думаю, что кто-либо из них поддерживает способы изменения исходного кода (например, удаление объявления этой функции), а затем записывает обратно модифицирующий исходный код Python.

ОБНОВЛЕНИЕ: причина, по которой я хочу это сделать, заключается в том, что я хотел бы написать библиотеку тестирования мутаций для python, в основном путем удаления операторов / выражений, повторного запуска тестов и просмотра ошибок.

Рори
источник
4
Устаревший с версии 2.6: пакет компилятора был удален в Python 3.0.
DFA
1
Что вы не можете редактировать источник? Почему ты не можешь написать декоратор?
S.Lott
3
Святая корова! Я хотел сделать тестер мутаций для python, используя ту же технику (в частности, создавая плагин для носа), вы планируете открыть его?
Райан
2
@Ryan Да, я с открытым исходным кодом все, что я создаю. Мы должны поддерживать связь по этому вопросу
Рори
1
Определенно, я отправил вам письмо через Launchpad.
Райан

Ответы:

73

Pythoscope делает это с тестовыми примерами , которые он генерирует автоматически, как и инструмент 2to3 для python 2.6 (он конвертирует исходный код python 2.x в исходный код python 3.x).

Оба эти инструмента используют библиотеку lib2to3, которая является реализацией механизма синтаксического анализатора / компилятора python, который может сохранять комментарии в источнике, когда он округляется из источника -> AST -> источник.

Проект веревки может удовлетворить ваши потребности , если вы хотите сделать больше рефакторинга , как преобразования.

Модуль ast - это ваша другая опция, и есть более старый пример того, как «разбирать» синтаксические деревья обратно в код (используя модуль синтаксического анализа). Но astмодуль более полезен при выполнении преобразования AST для кода, который затем преобразуется в объект кода.

Проект RedBaron также может подойти (Ксавье Комбель)

Райан
источник
5
пример unparse все еще поддерживается, вот обновленная версия py3k
Янус Троелсен
2
Что касается unparse.pyскрипта - это может быть очень неудобно использовать из другого скрипта. Но есть пакет под названием astunparse ( на github , на pypi ), который в основном представляет собой правильно упакованную версию unparse.py.
mbdevpl
Не могли бы вы обновить свой ответ, добавив parso в качестве предпочтительного варианта? Это очень хорошо и обновлено.
штучной упаковке
59

Кажется, что встроенный модуль ast не имеет метода для преобразования обратно в исходный код. Тем не менее, модуль codegen здесь предоставляет симпатичный принтер для ast, который позволит вам сделать это. например.

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

Это напечатает:

def foo():
    return 42

Обратите внимание, что вы можете потерять точное форматирование и комментарии, так как они не сохраняются.

Однако вам может и не понадобиться. Если все, что вам требуется, это выполнить замененный AST, вы можете сделать это, просто вызвав compile () в ast и выполнив полученный объект кода.

Брайан
источник
20
Просто для тех, кто использует это в будущем, codegen в значительной степени устарел и имеет несколько ошибок. Я исправил пару из них; У меня есть это как gistub
mattbasta
Обратите внимание, что последняя версия codegen была обновлена ​​в 2012 году, то есть после вышеприведенного комментария, поэтому я думаю, что codegen обновлен. @mattbasta
zjffdu
4
astor, кажется, является продолженным преемником
codegen
20

В другом ответе я предложил использовать astorпакет, но с тех пор я нашел более современный пакет для анализа AST, который называется astunparse:

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


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

Я проверил это на Python 3.5.

argentpepper
источник
19

Возможно, вам не нужно повторно генерировать исходный код. Конечно, мне немного опасно говорить, поскольку вы на самом деле не объяснили, почему вы думаете, что вам нужно сгенерировать файл .py, полный кода; но:

  • Если вы хотите сгенерировать файл .py, который люди фактически будут использовать, возможно, чтобы они могли заполнить форму и получить полезный файл .py для вставки в свой проект, то вам не нужно менять его на AST и назад , потому что вы потеряете все форматирование (вспомнит пустые строки , которые делают Python так читаемыми группировки связанных наборов линий вместе) ( AST узлы имеют linenoи col_offsetатрибуты ) комментарии. Вместо этого вы, вероятно, захотите использовать шаблонизатор ( например, язык шаблонов Django предназначен для упрощения шаблонизации даже текстовых файлов) для настройки файла .py или использовать расширение MetaPython Рика Копленда .

  • Если вы пытаетесь внести изменения во время компиляции модуля, обратите внимание, что вам не нужно возвращаться к тексту; Вы можете просто скомпилировать AST напрямую, а не превращать его обратно в файл .py.

  • Но почти в любом случае вы, вероятно, пытаетесь сделать что-то динамическое, что на самом деле делает такой язык, как Python, без написания новых файлов .py! Если вы расширите свой вопрос, чтобы сообщить нам, чего вы на самом деле хотите достичь, новые файлы .py, вероятно, вообще не будут участвовать в ответе; Я видел сотни проектов Python, выполняющих сотни реальных вещей, и ни один из них не нуждался в написании файла .py. Итак, я должен признать, я немного скептик, что вы нашли первый хороший вариант использования. :-)

Обновление: теперь, когда вы объяснили, что вы пытаетесь сделать, я все равно хотел бы просто поработать с AST. Вы захотите изменить его, удалив не строки файла (что может привести к полу-операторам, которые просто умирают с SyntaxError), а целые операторы - и что может быть лучше для этого, чем в AST?

Брэндон Роудс
источник
Хороший обзор возможных решений и возможных альтернатив.
Райан
1
Реальный пример использования для генерации кода: Кид и Генши (я полагаю) генерируют Python из шаблонов XML для быстрой визуализации динамических страниц.
Рик Коупленд
10

Разбор и изменение структуры кода, безусловно, возможны с помощью astмодуля, и я покажу это на примере ниже. Однако запись измененного исходного кода невозможна только с astодним модулем. Для этой работы доступны и другие модули, например, один здесь .

ПРИМЕЧАНИЕ. Пример, приведенный ниже, можно рассматривать как вводное руководство по использованию astмодуля, но более полное руководство по использованию astмодуля доступно здесь, в руководстве Green Tree snakes и официальной документации по astмодулю .

Введение в ast:

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

Вы можете проанализировать код Python (представленный в виде строки), просто вызвав API ast.parse(). Это возвращает дескриптор в структуру абстрактного синтаксического дерева (AST). Интересно, что вы можете скомпилировать эту структуру и выполнить ее, как показано выше.

Другой очень полезный API - ast.dump()это дамп всего AST в виде строки. Он может использоваться для проверки древовидной структуры и очень полезен при отладке. Например,

На 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)])"

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

Обратите внимание на разницу в синтаксисе для оператора печати в Python 2.7 по сравнению с Python 3.5 и разницу в типе узла AST в соответствующих деревьях.


Как изменить код, используя ast:

Теперь давайте рассмотрим пример модификации кода Python astмодулем. Основным инструментом для изменения структуры AST является ast.NodeTransformerкласс. Всякий раз, когда нужно модифицировать AST, ему / ей нужно подклассы из него и написать Node Transformation соответственно.

Для нашего примера давайте попробуем написать простую утилиту, которая преобразует Python 2, операторы print в вызовы функций Python 3.

Вывести оператор в утилиту конвертирования вызовов 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])

Эту утилиту можно попробовать на небольшом примере файла, например, приведенном ниже, и она должна работать нормально.

Тестовый входной файл: 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()

Обратите внимание, что приведенное выше преобразование предназначено только для astцелей обучения, и в реальном случае нужно будет рассмотреть все различные сценарии, такие как print " x is %s" % ("Hello Python").

ViFI
источник
6

Недавно я создал довольно стабильный (ядро действительно хорошо протестировано) и расширяемый кусок кода, который генерирует код из astдерева: https://github.com/paluh/code-formatter .

Я использую свой проект в качестве основы для небольшого плагина vim (который я использую каждый день), поэтому моя цель - генерировать действительно хороший и читаемый код на Python.

PS Я пытался расширить, codegenно его архитектура основана на ast.NodeVisitorинтерфейсе, поэтому средства форматирования ( visitor_методы) - это просто функции. Я обнаружил, что эта структура довольно ограничена и ее трудно оптимизировать (в случае длинных и вложенных выражений легче сохранять дерево объектов и кэшировать некоторые частичные результаты - иначе вы можете столкнуться с экспоненциальной сложностью, если хотите найти лучший макет). НО codegen как каждая часть работы Мицухико (которую я прочитал) очень хорошо написана и лаконична.

paluh
источник
4

Один из других ответов рекомендует codegen, который, кажется, был заменен astor. Версия astorPyPI (версия 0.5 на момент написания статьи) также выглядит несколько устаревшей, поэтому вы можете установить версию для разработки astorследующим образом.

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

Затем вы можете использовать astor.to_sourceдля преобразования Python AST в читаемый исходный код Python:

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

Я проверил это на Python 3.5.

argentpepper
источник
4

Если вы посмотрите на это в 2019 году, то вы можете использовать этот пакет libcst . Синтаксис похож на аст. Это работает как очарование и сохраняет структуру кода. Это в основном полезно для проекта, где вы должны сохранить комментарии, пробелы, перевод строки и т. Д.

Если вам не нужно заботиться о сохранении комментариев, пробелов и прочего, тогда комбинация ast и astor работает хорошо.

Саурав Гарти
источник
2

У нас была похожая потребность, которая не была решена другими ответами здесь. Поэтому мы создали для этого библиотеку ASTTokens , которая берет дерево AST, созданное с помощью модулей ast или astroid , и отмечает его диапазонами текста в исходном исходном коде.

Он не выполняет модификации кода напрямую, но его нетрудно добавить сверху, так как он говорит вам диапазон текста, который вам нужно изменить.

Например, это оборачивает вызов функции WRAP(...), сохраняя комментарии и все остальное:

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

Производит:

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

Надеюсь это поможет!

DS.
источник
1

Система преобразования программ - это инструмент, который анализирует исходный текст, создает AST, позволяет изменять их с помощью преобразований источника в источник («если вы видите этот шаблон, замените его этим шаблоном»). Такие инструменты идеально подходят для мутации существующих исходных кодов, которые просто «если вы видите этот шаблон, замените его вариантом».

Конечно, вам нужен механизм программной трансформации, который может анализировать интересующий вас язык и при этом выполнять преобразования, ориентированные на шаблоны. Наш инструментарий реинжиниринга программного обеспечения DMS - это система, которая может сделать это и обрабатывает Python и множество других языков.

Посмотрите этот SO-ответ для примера разбора DMS AST для Python для точного захвата комментариев . DMS может вносить изменения в AST и восстанавливать действительный текст, включая комментарии. Вы можете попросить его полностью распечатать AST, используя свои собственные правила форматирования (вы можете изменить их), или выполнить «печать верности», которая использует исходную информацию о строках и столбцах для максимального сохранения исходного макета (некоторые изменения в макете, где новый код) вставлено неизбежно).

Чтобы реализовать правило «мутации» для Python с DMS, вы можете написать следующее:

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

Это правило заменяет "+" на "-" синтаксически правильным образом; он работает с AST и поэтому не затрагивает строки или комментарии, которые выглядят правильно. Дополнительное условие для mutate_this_place - это возможность контролировать, как часто это происходит; Вы не хотите видоизменять каждое место в программе.

Очевидно, что вам нужно больше таких правил, которые бы определяли различные структуры кода и заменяли их мутированными версиями. DMS с удовольствием применяет ряд правил. Мутировавший AST затем довольно печатается.

Ира Бакстер
источник
Я не смотрел на этот ответ в течение 4 лет. Вау, это было понижено несколько раз. Это действительно потрясающе, так как он отвечает на вопрос OP напрямую и даже показывает, как делать мутации, которые он хочет сделать. Я не думаю, что кто-либо из downvoters хотел бы объяснить, почему они понизили.
Ира Бакстер
4
Потому что это продвигает очень дорогой инструмент с закрытым исходным кодом.
Зоран Павлович
@ZoranPavlovic: То есть вы не возражаете против его технической точности или полезности?
Ира Бакстер
2
@Zoran: Он не сказал, что у него есть библиотека с открытым исходным кодом. Он сказал, что хочет изменить исходный код Python (используя AST), и решения, которые он может найти, этого не делают. Это такое решение. Вы не думаете, что люди используют коммерческие инструменты в программах, написанных на таких языках, как Python на Java?
Ира Бакстер
1
Я не голосующий вниз, но пост читается немного как реклама. Чтобы улучшить ответ, вы можете раскрыть, что вы связаны с продуктом
Вим
0

Раньше я использовал для этого барон, но теперь перешел на парсо, потому что он соответствует современным питонам. Работает отлично.

Я также нуждался в этом для тестера мутации. Это действительно довольно просто сделать с парсо, проверьте мой код на https://github.com/boxed/mutmut

коробочный
источник