it-roy-ru.com

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

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

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

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

145
Rory

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

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

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

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

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

66
Ryan

Кажется, что встроенный модуль 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 и выполнив полученный объект кода.

55
Brian

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

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

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

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

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

20
Brandon Rhodes

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

16
argentpepper

Разбор и изменение структуры кода, безусловно, возможны с помощью модуля 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 __== '__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 __== '__main__':
    print "I am in main"
    main()

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

6
ViFI

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

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

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

6
paluh

Один из других ответов рекомендует 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.

3
argentpepper

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

2
Ira Baxter

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

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

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

0
boxed