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

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

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

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

5 ответов


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


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

этот вопрос естественно возникает при написании любого вида кода Python beautifier, PEP-8 checker и т. д. В таких случаях, вы are выполнение преобразований "Источник-Источник", вы do ожидайте, что входные данные будут написаны человеком и не только хотят, чтобы выходные данные были удобочитаемыми, но и, кроме того, ожидают:

  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): вернуть написание строки в данном узле.

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

короче, этот код работает хорошо, но немного неуклюжий.

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

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


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

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

  • парсер: токены, грамматика и т. д.
  • пройдохи: библиотека преобразований
  • рефакторинг инструменты: применяет фиксаторы к анализируемому АСТ
  • командная строка: выберите исправления для применения и запустите их параллельно с помощью multiprocessing

Ниже приводится краткое введение в использование 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()

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

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

если ваша цель-собрать данные, но не преобразовать их, то вам нужно сделать немного больше работы. Вот рецепт I взбитые до использования 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')]

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

наши инструментарий реинжиниринга программного обеспечения DMS С передняя часть Python будет анализировать Python и создавать ASTs, которые захватывают все комментарии (см. Этот так пример). Передняя часть Python включает prettyprinter, который может регенерировать код Python (с комментариями!) непосредственно из АСТ. DMS сама обеспечивает низкоуровневый механизм синтаксического анализа и возможность преобразования "Источник-Источник", которые работают с шаблонами, написанными с использованием синтаксиса поверхности целевого языка (например, Python).