Проанализируйте файл .py, прочитайте AST, измените его, а затем запишите измененный исходный код - PullRequest
149 голосов
/ 20 апреля 2009

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

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

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

Ответы [ 10 ]

67 голосов
/ 20 апреля 2009

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

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

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

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

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

55 голосов
/ 20 апреля 2009

Кажется, что встроенный модуль 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 голосов
/ 20 апреля 2009

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

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

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

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

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

17 голосов
/ 09 сентября 2016

В другом ответе я предложил использовать пакет 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.

7 голосов
/ 23 января 2017

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

ПРИМЕЧАНИЕ. Приведенный ниже пример можно рассматривать как вводное руководство по использованию модуля ast, но более полное руководство по использованию модуля ast доступно здесь: Руководство по змеям Green Tree и официальная документация по 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=[]))])"

Обратите внимание на разницу в синтаксисе оператора print в 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").

6 голосов
/ 22 сентября 2013

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

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

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

4 голосов
/ 17 августа 2016

Один из других ответов рекомендует codegen, который, кажется, был заменен astor. Версия astor на PyPI (версия 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.

2 голосов
/ 14 декабря 2016

У нас была похожая потребность, которая не была решена другими ответами здесь. Для этого мы создали библиотеку 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

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

2 голосов
/ 09 марта 2010

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

Конечно, вам нужен механизм программной трансформации, который может анализировать интересующий вас язык и при этом выполнять преобразования, ориентированные на шаблоны. Наш DMS Software Reengineering Toolkit - это система, которая может это делать, обрабатывать 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 довольно печатается.

0 голосов
/ 11 августа 2018

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

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

...