Python AST с сохраненными комментариями - PullRequest
16 голосов
/ 17 сентября 2011

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

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

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

Ответы [ 6 ]

11 голосов
/ 17 сентября 2011

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

9 голосов
/ 03 сентября 2015

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())
5 голосов
/ 17 марта 2016

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

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

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

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

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

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 голосов
/ 08 ноября 2017

Несколько человек уже упомянули 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()

Run 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')]
2 голосов
/ 11 января 2019

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

https://pybowler.io/

0 голосов
/ 06 октября 2011

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

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

...