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

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

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

UPDATE: Причина, по которой я хочу это сделать, - это написать библиотеку тестирования мутаций для python, в основном путем удаления утверждений/выражения, повторные тесты и видя, что происходит.

Ответ 1

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

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

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

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

Проект redbaron также может быть хорошо подходит (ht Xavier Combelle)

Ответ 2

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

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

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

Ответ 5

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

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

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

Ответ 6

Анализ и изменение структуры кода, безусловно, возможно с помощью модуля 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, ему/ей необходимо подклассу из него и соответствующим образом изменять Преобразование узлов.

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

Заявление на печать в утилите конвертера Fun call: 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").

Ответ 7

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

Ответ 8

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

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

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

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

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

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

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

Ответ 9

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

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

Ответ 10

Я использовал барон для этого, но теперь переключился на parso, потому что он обновлен с современным python. Он отлично работает.

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