Python AST с сохраненными комментариями

Я могу получить AST без комментариев, используя

import ast
module = ast.parse(open('/path/to/module.py').read())

Не могли бы вы показать пример получения AST с сохраненными комментариями (и пробелами)?

Ответ 1

Модуль ast не содержит комментариев. Модуль tokenize может давать вам комментарии, но не предоставляет другую структуру программы.

Ответ 2

AST, который хранит информацию о форматировании, комментариях и т.д., Называется Полным синтаксическим деревом.

Redbaron может сделать это. Установите с помощью pip install redbaron и попробуйте следующий код.

import redbaron

with open("/path/to/module.py", "r") as source_code:
    red = redbaron.RedBaron(source_code.read())

print (red.fst())

Ответ 3

Этот вопрос естественным образом возникает при написании любого вида кода на языке Python, средства проверки pep-8 и т.д. В таких случаях вы выполняете преобразования источник-источник, вы ожидаете, что ввод будет написан человеком, а не только хотите, чтобы вывод должен быть удобочитаемым, но, кроме того, ожидайте, что он:

  1. включите все комментарии, где именно они появляются в оригинале.
  2. выведите точное написание строк, включая строки документов, как в оригинале.

Это далеко не просто сделать с модулем ast. Вы можете назвать это дырой в API, но, кажется, нет простого способа расширить API, чтобы легко выполнить 1 и 2.

Предложение Андрея использовать вместе ast и tokenize - это замечательный обходной путь. Идея пришла ко мне и при написании конвертера Python в Coffeescript, но код далеко не тривиален.

Класс TokenSync (ts), начинающийся со строки 1305 в py2cs.py, координирует связь между данными на основе токенов и прохождением ast. Учитывая строковый s, то TokenSync размечает класс s и inits внутренних структур данных, которые поддерживают несколько методов интерфейса:

ts.leading_lines(node): возвращает список предыдущего комментария и пустые строки.

ts.trailing_comment(node): возвращает строку, содержащую завершающий комментарий для узла, если таковой имеется.

ts.sync_string(node): вернуть написание строки в данном узле.

Для настоящих посетителей использовать эти методы довольно просто, но немного неуклюже. Вот несколько примеров из класса CoffeeScriptTraverser (cst) в py2cs.py:

def do_Str(self, node):
    '''A string constant, including docstrings.'''
    if hasattr(node, 'lineno'):
        return self.sync_string(node)

Это работает при условии, что узлы ast.Str посещаются в порядке их появления в источниках. Это происходит естественно в большинстве обходов.

Вот аст. Если посетитель. Здесь показано, как использовать ts.leading_lines и ts.trailing_comment:

def do_If(self, node):

    result = self.leading_lines(node)
    tail = self.trailing_comment(node)
    s = 'if %s:%s' % (self.visit(node.test), tail)
    result.append(self.indent(s))
    for z in node.body:
        self.level += 1
        result.append(self.visit(z))
        self.level -= 1
    if node.orelse:
        tail = self.tail_after_body(node.body, node.orelse, result)
        result.append(self.indent('else:' + tail))
        for z in node.orelse:
            self.level += 1
            result.append(self.visit(z))
            self.level -= 1
    return ''.join(result)

Метод ts.tail_after_body компенсирует тот факт, что нет узлов ast, представляющих предложения else. Это не ракетостроение, но это не красиво:

def tail_after_body(self, body, aList, result):
    '''
    Return the tail of the 'else' or 'finally' statement following the given body.
    aList is the node.orelse or node.finalbody list.
    '''
    node = self.last_node(body)
    if node:
        max_n = node.lineno
        leading = self.leading_lines(aList[0])
        if leading:
            result.extend(leading)
            max_n += len(leading)
        tail = self.trailing_comment_at_lineno(max_n + 1)
    else:
        tail = '\n'
    return tail

Обратите внимание, что cst.tail_after_body просто вызывает ts.tail_after_body.

Резюме

Класс TokenSync инкапсулирует большинство сложностей, связанных с тем, чтобы сделать ориентированные на токены данные доступными для кода обхода ast. Использовать класс TokenSync просто, но посетители ast для всех операторов Python (и ast.Str) должны включать вызовы ts.leading_lines, ts.trailing_comment и ts.sync_string. Кроме того, хак ts.tail_after_body необходим для обработки "отсутствующих" аст-узлов.

Короче говоря, код работает хорошо, но немного неуклюже.

@Андрей: ваш короткий ответ может означать, что вы знаете более элегантный способ. Если это так, я бы хотел увидеть это.

Эдвард К. Реам

Ответ 4

Несколько человек уже упомянули lib2to3, но я хотел создать более полный ответ, потому что этот инструмент недооценен. Не беспокойся о redbaron.

lib2to3 состоит из нескольких частей:

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

Ниже приведено краткое введение в использование lib2to3 для преобразований и очистки данных (т.е. извлечения).

Трансформации

Если вы хотите преобразовать файлы Python (то есть сложный поиск/замена), CLI, предоставляемый lib2to3 является полнофункциональным и может преобразовывать файлы параллельно.

Чтобы использовать его, создайте пакет Python, в котором каждый его подмодуль содержит отдельный подкласс lib2to3.fixer_base.BaseFix. Смотрите lib2to3.fixes для множества примеров.

Затем создайте свой исполняемый скрипт (заменив "myfixes" на имя вашего пакета):

import sys
import lib2to3.main

def main(args=None):
    sys.exit(lib2to3.main.main("myfixes", args=args))

if __name__ == '__main__':
    main()

Запустите yourscript -h чтобы увидеть варианты.

выскабливание

Если ваша цель - собирать данные, а не преобразовывать их, вам нужно проделать еще немного работы. Вот рецепт, который я использовал, чтобы использовать lib2to3 для очистки данных:

# file: basescraper.py
from __future__ import absolute_import, print_function

from lib2to3.pgen2 import token
from lib2to3.pgen2.parse import ParseError
from lib2to3.pygram import python_grammar
from lib2to3.refactor import RefactoringTool
from lib2to3 import fixer_base


def symbol_name(number):
    """
    Get a human-friendly name from a token or symbol

    Very handy for debugging.
    """
    try:
        return token.tok_name[number]
    except KeyError:
        return python_grammar.number2symbol[number]


class SimpleRefactoringTool(RefactoringTool):
    def __init__(self, scraper_classes, options=None, explicit=None):
        self.fixers = None
        self.scraper_classes = scraper_classes
        # first argument is a list of fixer paths, as strings. we override
        # get_fixers, so we don't need it.
        super(SimpleRefactoringTool, self).__init__(None, options, explicit)

    def get_fixers(self):
        """
        Override base method to get fixers from passed fixers classes instead
        of via dotted-module-paths.
        """
        self.fixers = [cls(self.options, self.fixer_log)
                       for cls in self.scraper_classes]
        return (self.fixers, [])

    def get_results(self):
        """
        Get the scraped results returned from 'scraper_classes'
        """
        return {type(fixer): fixer.results for fixer in self.fixers}


class BaseScraper(fixer_base.BaseFix):
    """
    Base class for a fixer that stores results.

    lib2to3 was designed with transformation in mind, but if you just want
    to scrape results, you need a way to pass data back to the caller.
    """
    BM_compatible = True

    def __init__(self, options, log):
        self.results = []
        super(BaseScraper, self).__init__(options, log)

    def scrape(self, node, match):
        raise NotImplementedError

    def transform(self, node, match):
        result = self.scrape(node, match)
        if result is not None:
            self.results.append(result)


def scrape(code, scraper):
    """
    Simple interface when you have a single scraper class.
    """
    tool = SimpleRefactoringTool([scraper])
    tool.refactor_string(code, '<test.py>')
    return tool.get_results()[scraper]

А вот простой скребок, который находит первый комментарий после функции def:

# file: commentscraper.py
from basescraper import scrape, BaseScraper, ParseError

class FindComments(BaseScraper):

    PATTERN = """ 
    funcdef< 'def' name=any parameters< '(' [any] ')' >
           ['->' any] ':' suite=any+ >
    """

    def scrape(self, node, results):
        suite = results["suite"]
        name = results["name"]

        if suite[0].children[1].type == token.INDENT:
            indent_node = suite[0].children[1]
            return (str(name), indent_node.prefix.strip())
        else:
            # e.g. "def foo(...): x = 5; y = 7"
            # nothing to save
            return

# example usage:

code = '''\

@decorator
def foobar():
    # type: comment goes here
    """
    docstring
    """
    pass

'''
comments = scrape(code, FindTypeComments)
assert comments == [('foobar', '# type: comment goes here')]

Ответ 5

Другие эксперты, похоже, считают, что модуль Python AST разделяет комментарии, поэтому это означает, что маршрут просто не будет работать для вас.

Наш DMS Software Reengineering Toolkit с его Python front end будет анализировать Python и строить АСТ, которые фиксируют все комментарии (см. этот пример SO). Передняя часть Python включает симпатичный принтер, который может регенерировать код Python (с комментариями!) Непосредственно из AST. Сам DMS предоставляет низкоуровневое синтаксическое моделирование и возможность преобразования источника в источник, которые работают с шаблонами, написанными с использованием синтаксиса поверхности целевого языка (например, Python).

Ответ 6

Если вы используете Python 3, вы можете использовать bowler, который основан на lib2to3, но предоставляет гораздо более приятный API и CLI для создания сценариев преобразования.

https://pybowler.io/