Подтвердить что ты не робот

Как получить lineno из "конца заявления" в Python ast

Я пытаюсь работать с script, который управляет другим script в Python, script, который должен быть изменен, имеет структуру вроде:

class SomethingRecord(Record):
    description = 'This records something'
    author = 'john smith'

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

Теперь единственной проблемой является description, иногда это многострочная строка, например

    description = ('line 1'
                   'line 2'
                   'line 3')

или

    description = 'line 1' \
        'line 2' \
        'line 3'

и я имею только номер строки первой строки, а не следующие строки. Поэтому мой однострочный заменитель выполнит

    description = 'new value'
        'line 2' \
        'line 3'

и код сломан. Я понял, что если я знаю как lineno начала и конца/количества строк назначения description, я мог бы восстановить свой код, чтобы справиться с такой ситуацией. Как получить такую ​​информацию в стандартной библиотеке Python?

4b9b3361

Ответ 1

Я посмотрел на другие ответы; кажется, что люди делают backflips, чтобы обойти проблемы вычисления номеров линий, когда ваша настоящая проблема - это изменение кода. Это говорит о том, что базовый механизм не помогает вам, как вам действительно нужно.

Если вы используете систему трансформации программ (PTS), вы можете избежать этой бессмыслицы.

Хорошая PTS проанализирует ваш исходный код в AST, а затем позволит вам применить правила перезаписи исходного кода для изменения AST и, наконец, преобразовать измененный AST обратно в исходный текст. Обычно PTSes принимают правила преобразования по существу этой формы:

   if you see *this*, replace it by *that*

[Парсер, который строит AST, НЕ является PTS. Они не допускают таких правил; вы можете написать ad hoc-код, чтобы взломать дерево, но это обычно довольно неудобно. Не делают они АСТ для регенерации исходного текста.]

(Мой PTS, см. био, называется) DMS - это PTS, которая может это сделать. Пример OP будет легко выполнен с использованием следующего правила перезаписи:

 source domain Python; -- tell DMS the syntax of pattern left hand sides
 target domain Python; -- tell DMS the syntax of pattern right hand sides

 rule replace_description(e: expression): statement -> statement =
     " description = \e "
  ->
     " description = ('line 1'
                      'line 2'
                      'line 3')";

Правилу единственного преобразования присваивается имя replace_description, чтобы отличить его от всего другого правила, которое мы могли бы определить. Параметры правила (e: выражение) указывают, что шаблон позволит произвольное выражение, определяемое исходным языком. statement- > означает, что правило отображает инструкцию на исходном языке, на утверждение на целевом языке; мы могли бы использовать любую другую синтаксическую категорию из грамматики Python, предоставленной DMS. Используемый здесь " - это метаовота, используемая для выделения синтаксиса языка правил из синтаксиса языка субъекта. Вторая разделяет шаблон источника с целевой шаблон, который.

Вы заметите, что нет необходимости упоминать номера строк. PTS преобразует синтаксис поверхности правила в соответствующие AST, фактически анализируя шаблоны с тем же синтаксическим анализатором, который используется для анализа исходного файла. АСТ, созданный для шаблонов, используется для создания соответствия/замены шаблона. Поскольку это происходит из AST, фактическая компоновка исходного кода (интервал, linebreaks, comments) не влияет на способность DMS сопоставлять или заменять. Комментарии не являются проблемой для сопоставления, поскольку они привязаны к узлам дерева, а не к узлам дерева; они сохраняются в преобразованной программе. DMS действительно фиксирует строку и точную информацию столбца для всех элементов дерева; просто не требуется для реализации преобразований. Кодовая компоновка также сохраняется на выходе DMS, используя эту информацию о строках/столбцах.

Другие PTS предлагают в целом аналогичные возможности.

Ответ 2

В качестве обходного пути вы можете изменить:

    description = 'line 1' \
              'line 2' \
              'line 3'

в

    description = 'new value'; tmp = 'line 1' \
              'line 2' \
              'line 3'

и т.д..

Это простое изменение, но действительно уродливый код.

Ответ 3

В самом деле, необходимая информация не сохраняется в ast. Я не знаю подробностей о том, что вам нужно, но похоже, что вы можете использовать модуль tokenize из стандартной библиотеки. Идея состоит в том, что каждый логический оператор Python заканчивается токеном NEWLINE (также он может быть точкой с запятой, но, как я понимаю, это не ваш случай). Я тестировал этот подход с таким файлом:

# first comment
class SomethingRecord:
    description = ('line 1'
                   'line 2'
                   'line 3')

class SomethingRecord2:
    description = ('line 1',
                   'line 2',
                   # comment in the middle

                   'line 3')

class SomethingRecord3:
    description = 'line 1' \
                  'line 2' \
                  'line 3'
    whatever = 'line'

class SomethingRecord3:
    description = 'line 1', \
                  'line 2', \
                  'line 3'
                  # last comment

И вот что я предлагаю сделать:

import tokenize
from io import BytesIO
from collections import defaultdict

with tokenize.open('testmod.py') as f:
    code = f.read()
    enc = f.encoding

rl = BytesIO(code.encode(enc)).readline
tokens = list(tokenize.tokenize(rl))

token_table = defaultdict(list)  # mapping line numbers to token numbers
for i, tok in enumerate(tokens):
    token_table[tok.start[0]].append(i)

def find_end(start):
    i = token_table[start][-1]  # last token number on the start line
    while tokens[i].exact_type != tokenize.NEWLINE:
        i += 1
    return tokens[i].start[0]

print(find_end(3))
print(find_end(8))
print(find_end(15))
print(find_end(21))

Это выдает:

5
12
17
23

Это кажется правильным, вы можете настроить этот подход в зависимости от того, что именно вам нужно. tokenize более подробный, чем ast, но также более гибкий. Разумеется, лучший подход - использовать их как для разных частей вашей задачи.


EDIT: Я пробовал это в Python 3.4, но я думаю, что он также должен работать в других версиях.

Ответ 4

Мое решение имеет другой путь: когда мне пришлось менять код в другом файле, я открыл файл, нашел строку и получил все следующие строки с более глубоким отступом, чем первый, и вернул номер строки для первой строки которая не является более глубокой. Я возвращаю None, None, если не могу найти текст, который я искал. Это, конечно, неполное, но я думаю, что этого достаточно, чтобы вы прошли через:)

def get_all_indented(text_lines, text_in_first_line):
    first_line = None
    indent = None
    for line_num in range(len(text_lines)):
        if indent is not None and first_line is not None:
            if not text_lines[line_num].startswith(indent):
                return first_line, line_num     # First and last lines
        if text_in_first_line in text_lines[line_num]:
            first_line = line_num
            indent = text_lines[line_num][:text_lines[line_num].index(text_in_first_line)] + ' '  # At least 1 more space.
    return None, None

Ответ 5

Существует новая библиотека asttokens, которая хорошо справляется с этим: https://github.com/gristlabs/asttokens

import ast, asttokens

code = '''
class SomethingRecord(object):
    desc1 = 'This records something'
    desc2 = ('line 1'
             'line 2'
             'line 3')
    desc3 = 'line 1' \
            'line 2' \
            'line 3'
    author = 'john smith'
'''

atok = asttokens.ASTTokens(code, parse=True)
assign_values = [n.value for n in ast.walk(atok.tree) if isinstance(n, ast.Assign)]

replacements = [atok.get_text_range(n) + ("'new value'",) for n in assign_values]
print(asttokens.util.replace(atok.text, replacements))

производит

class SomethingRecord(object):
    desc1 = 'new value'
    desc2 = ('new value')
    desc3 = 'new value'
    author = 'new value'