Как получить исходный код, соответствующий узлу Python AST?

Узлы Python AST имеютlineno а такжеcol_offset атрибуты, которые указывают начало соответствующего диапазона кода. Есть ли простой способ получить конец диапазона кода? Сторонняя библиотека?

 DS.10 дек. 2016 г., 08:35
Это'здесьgithub.com/gristlabs/asttokens, но я также добавил, с примером, в качестве отдельного ответа.
 Aivar07 дек. 2016 г., 16:18
@DS. Некоторые проблемные случаи здесь:bitbucket.org/plas/thonny/issues?component=ast+analysis
 DS.10 дек. 2016 г., 08:13
ОК, у меня есть библиотека! @ Авиар, твои тесты были очень полезными. Это чувствует себя довольно крепким сейчас.
 Aivar06 дек. 2016 г., 11:56
@DS I 'Я не доволен моим решением, потому что оноВ настоящее время неполные и иногда появляются ошибки. Но я нене вижу другого хорошего решения. Одной из альтернатив будет написание нового парсера, который собирает больше информации, но яЯ не готов сделать это сам. Отдельный пакет был бы действительно хорош - есть несколько проектов, которые могли бы извлечь из этого пользу. Посмотрите, например, эту идею, ребята:stackoverflow.com/questions/40639652/...
 DS.05 дек. 2016 г., 22:15
Мне также нужен способ аннотировать узлы информацией о смещении конца (например, ваше решение), а также с поддержкой Python2. Я думаю о создании отдельного модуля, который это делает. Это будет интересно? @ Айвар, ты доволен своим подходом?
 DS.07 дек. 2016 г., 15:34
Я пытаюсь подход, который кажется многообещающим до сих пор, который связывает каждый узел с токенами (из модуля токенизации). Не могли бы вы поделиться некоторыми примерами, которые вызывают проблемы?

Ответы на вопрос(3)

я знаю, что очень поздно, но я думаю, что это то, что вы ищете, я делаю анализ только для определения функций в модуле. Этим методом мы можем получить первую и последнюю строку узла ast. Таким образом, строки исходного кода определения функции могут быть получены путем анализа исходного файла путем чтения только тех строк, которые нам нужны. Это очень простой пример,

st='def foo():\n    print "hello" \n\ndef bla():\n    a = 1\n    b = 2\n  
c= a+b\n    print c'

import ast 
tree = ast.parse(st)
for function in tree.body:
    if isinstance(function,ast.FunctionDef):
        # Just in case if there are loops in the definition
        lastBody = func.body[-1]
        while isinstance (lastBody,(ast.For,ast.While,ast.If)):
            lastBody = lastBody.Body[-1]
        lastLine = lastBody.lineno
        print "Name of the function is ",function.name
        print "firstLine of the function is ",function.lineno
        print "LastLine of the function is ",lastLine
        print "the source lines are "
        if isinstance(st,str):
            st = st.split("\n")
        for i , line in enumerate(st,1):
            if i in range(function.lineno,lastLine+1):
                print line
 Aivar24 нояб. 2015 г., 14:00
Тот'верно! Я хочу выделить соответствующий диапазон кода, когда в AST выбрано произвольное выражение или узел оператора
 True if u_believe else False24 нояб. 2015 г., 13:33
о, мне действительно очень жаль Я буду искать это и держать вас в курсе.
 True if u_believe else False24 нояб. 2015 г., 13:35
но вам нужен конец диапазона кода, верно?
 True if u_believe else False24 нояб. 2015 г., 14:52
я думаю, что вам нужна только первая и последняя строка узла. Я тоже делаю что-то подобное. пожалуйста, проверьте мои изменения в ответе, если это все равно поможет.
 Aivar24 нояб. 2015 г., 13:27
Спасибо! К сожалению это немне не поможет Мне нужны местоположения для всех узлов и не только номеров строк, но и столбцов.

asttokens библиотека для этого. Он поддерживает источник как в текстовом, так и в токенизированном виде и помечает узлы AST информацией о токене, из которой текст также легко доступен.

Работает с Python 2 и 3 (проверено с 2.7 и 3.5). Например:

import ast, asttokens
st='''
def greet(a):
  say("hello") if a else say("bye")
'''
atok = asttokens.ASTTokens(st, parse=True)
for node in ast.walk(atok.tree):
  if hasattr(node, 'lineno'):
    print atok.get_text_range(node), node.__class__.__name__, atok.get_text(node)

Печать

(1, 50) FunctionDef def greet(a):
  say("hello") if a else say("bye")
(17, 50) Expr say("hello") if a else say("bye")
(11, 12) Name a
(17, 50) IfExp say("hello") if a else say("bye")
(33, 34) Name a
(17, 29) Call say("hello")
(40, 50) Call say("bye")
(17, 20) Name say
(21, 28) Str "hello"
(40, 43) Name say
(44, 49) Str "bye"
 Glycerine11 февр. 2017 г., 00:24
Быстрая и простая установка. Это действительно отличная библиотека.
 Aivar10 дек. 2016 г., 14:25
Очень круто! Я'попробую скоро.
Решение Вопроса

РЕДАКТИРОВАТЬ: последний код (протестирован в Python 3.5-3.7) здесь:https://bitbucket.org/plas/thonny/src/master/thonny/ast_utils.py

Как я ненайти легкий путь, здесьЭто сложный (и, вероятно, не оптимальный) способ. Может произойти сбой и / или работать некорректно, если в синтаксическом анализаторе Python больше ошибок lineno / col_offset, чем упомянутых (и обходных) в коде. Протестировано в Python 3.3:

def mark_code_ranges(node, source):
    """
    Node is an AST, source is corresponding source as string.
    Function adds recursively attributes end_lineno and end_col_offset to each node
    which has attributes lineno and col_offset.
    """

    NON_VALUE_KEYWORDS = set(keyword.kwlist) - {'False', 'True', 'None'}


    def _get_ordered_child_nodes(node):
        if isinstance(node, ast.Dict):
            children = []
            for i in range(len(node.keys)):
                children.append(node.keys[i])
                children.append(node.values[i])
            return children
        elif isinstance(node, ast.Call):
            children = [node.func] + node.args

            for kw in node.keywords:
                children.append(kw.value)

            if node.starargs != None:
                children.append(node.starargs)
            if node.kwargs != None:
                children.append(node.kwargs)

            children.sort(key=lambda x: (x.lineno, x.col_offset))
            return children
        else:
            return ast.iter_child_nodes(node)    

    def _fix_triple_quote_positions(root, all_tokens):
        """
        http://bugs.python.org/issue18370
        """
        string_tokens = list(filter(lambda tok: tok.type == token.STRING, all_tokens))

        def _fix_str_nodes(node):
            if isinstance(node, ast.Str):
                tok = string_tokens.pop(0)
                node.lineno, node.col_offset = tok.start

            for child in _get_ordered_child_nodes(node):
                _fix_str_nodes(child)

        _fix_str_nodes(root)

        # fix their erroneous Expr parents   
        for node in ast.walk(root):
            if ((isinstance(node, ast.Expr) or isinstance(node, ast.Attribute))
                and isinstance(node.value, ast.Str)):
                node.lineno, node.col_offset = node.value.lineno, node.value.col_offset

    def _fix_binop_positions(node):
        """
        http://bugs.python.org/issue18374
        """
        for child in ast.iter_child_nodes(node):
            _fix_binop_positions(child)

        if isinstance(node, ast.BinOp):
            node.lineno = node.left.lineno
            node.col_offset = node.left.col_offset


    def _extract_tokens(tokens, lineno, col_offset, end_lineno, end_col_offset):
        return list(filter((lambda tok: tok.start[0] >= lineno
                                   and (tok.start[1] >= col_offset or tok.start[0] > lineno)
                                   and tok.end[0] <= end_lineno
                                   and (tok.end[1] <= end_col_offset or tok.end[0] < end_lineno)
                                   and tok.string != ''),
                           tokens))



    def _mark_code_ranges_rec(node, tokens, prelim_end_lineno, prelim_end_col_offset):
        """
        Returns the earliest starting position found in given tree, 
        this is convenient for internal handling of the siblings
        """

        # set end markers to this node
        if "lineno" in node._attributes and "col_offset" in node._attributes:
            tokens = _extract_tokens(tokens, node.lineno, node.col_offset, prelim_end_lineno, prelim_end_col_offset)
            #tokens = 
            _set_real_end(node, tokens, prelim_end_lineno, prelim_end_col_offset)

        # mark its children, starting from last one
        # NB! need to sort children because eg. in dict literal all keys come first and then all values
        children = list(_get_ordered_child_nodes(node))
        for child in reversed(children):
            (prelim_end_lineno, prelim_end_col_offset) = \
                _mark_code_ranges_rec(child, tokens, prelim_end_lineno, prelim_end_col_offset)

        if "lineno" in node._attributes and "col_offset" in node._attributes:
            # new "front" is beginning of this node
            prelim_end_lineno = node.lineno
            prelim_end_col_offset = node.col_offset

        return (prelim_end_lineno, prelim_end_col_offset)

    def _strip_trailing_junk_from_expressions(tokens):
        while (tokens[-1].type not in (token.RBRACE, token.RPAR, token.RSQB,
                                      token.NAME, token.NUMBER, token.STRING, 
                                      token.ELLIPSIS)
                    and tokens[-1].string not in ")}]"
                    or tokens[-1].string in NON_VALUE_KEYWORDS):
            del tokens[-1]

    def _strip_trailing_extra_closers(tokens, remove_naked_comma):
        level = 0
        for i in range(len(tokens)):
            if tokens[i].string in "({[":
                level += 1
            elif tokens[i].string in ")}]":
                level -= 1

            if level == 0 and tokens[i].string == "," and remove_naked_comma:
                tokens[:] = tokens[0:i]
                return

            if level < 0:
                tokens[:] = tokens[0:i]
                return   

    def _set_real_end(node, tokens, prelim_end_lineno, prelim_end_col_offset):
        # prelim_end_lineno and prelim_end_col_offset are the start of 
        # next positioned node or end of source, ie. the suffix of given
        # range may contain keywords, commas and other stuff not belonging to current node

        # Function returns the list of tokens which cover all its children


        if isinstance(node, _ast.stmt):
            # remove empty trailing lines
            while (tokens[-1].type in (tokenize.NL, tokenize.COMMENT, token.NEWLINE, token.INDENT)
                   or tokens[-1].string in (":", "else", "elif", "finally", "except")):
                del tokens[-1]

        else:
            _strip_trailing_extra_closers(tokens, not isinstance(node, ast.Tuple))
            _strip_trailing_junk_from_expressions(tokens)

        # set the end markers of this node
        node.end_lineno = tokens[-1].end[0]
        node.end_col_offset = tokens[-1].end[1]

        # Try to peel off more tokens to give better estimate for children
        # Empty parens would confuse the children of no argument Call
        if ((isinstance(node, ast.Call)) 
            and not (node.args or node.keywords or node.starargs or node.kwargs)):
            assert tokens[-1].string == ')'
            del tokens[-1]
            _strip_trailing_junk_from_expressions(tokens)
        # attribute name would confuse the "value" of Attribute
        elif isinstance(node, ast.Attribute):
            if tokens[-1].type == token.NAME:
                del tokens[-1]
                _strip_trailing_junk_from_expressions(tokens)
            else:
                raise AssertionError("Expected token.NAME, got " + str(tokens[-1]))
                #import sys
                #print("Expected token.NAME, got " + str(tokens[-1]), file=sys.stderr)

        return tokens

    all_tokens = list(tokenize.tokenize(io.BytesIO(source.encode('utf-8')).readline))
    _fix_triple_quote_positions(node, all_tokens)
    _fix_binop_positions(node)
    source_lines = source.split("\n") 
    prelim_end_lineno = len(source_lines)
    prelim_end_col_offset = len(source_lines[len(source_lines)-1])
    _mark_code_ranges_rec(node, all_tokens, prelim_end_lineno, prelim_end_col_offset)
 Aivar24 нояб. 2015 г., 13:29
На самом деле все оказалось сложнее. Новый код здесь:bitbucket.org/plas/thonny/src (ищите ast_utils.py в пакете под названием "thonny ")
 eyquem07 июл. 2013 г., 21:37
Я ничего не понимаю, но я поддерживаю работу и разделение работы.

Ваш ответ на вопрос