如何在Python中获得“end-of-statement”的lineno

时间:2016-09-29 20:35:58

标签: python abstract-syntax-tree

我正在尝试处理在Python中操作另一个脚本的脚本,要修改的脚本具有如下结构:

tuple_it

我使用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,我可以修复我的代码来处理这种情况。如何使用Python标准库获取此类信息?

6 个答案:

答案 0 :(得分:7)

我看了其他的答案;当你真正的问题是修改代码时,似乎人们正在做后空翻来解决计算行号的问题。这表明基线机制并没有以你真正需要的方式帮助你。

如果你使用program transformation system (PTS),你可以避免这么多废话。

一个好的PTS会将您的源代码解析为AST,然后让您应用源级重写规则来修改AST,并最终将修改后的AST转换回源文本。通常,PTS接受基本上这种形式的转换规则:

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

[构建AST的解析器不是PTS。他们不允许这样的规则;你可以编写临时代码来破解树,但这通常很尴尬。他们不做AST来源文本再生。]

(我的PTS,见bio,叫做)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-> statement 表示规则将源语言中的语句映射到目标语言中的语句;我们可以使用提供给DMS的Python语法中的任何其他语法类别。这里使用的" metaquote ,用于区分规则语言的语法,从主语言的语法。第二个 - > 将源模式与目标模式分开

您会注意到没有必要提及行号。 PTS通过使用用于解析源文件的相同解析器实际解析模式,将规则表面语法转换为相应的AST。为模式生成的AST用于实现模式匹配/替换。因为这是由AST驱动的,所以原始代码的实际布局(间距,换行符,注释)不会影响DMS匹配或替换的能力。注释不是匹配的问题,因为它们附加到树节点而不是树节点;它们保存在改造后的程序中。 DMS确实捕获所有树元素的行和精确列信息;只是不需要实现转换。使用该行/列信息,DMS在输出中也保留代码布局。

其他PTS提供的功能大致类似。

答案 1 :(得分:6)

作为一种解决方法,您可以更改:

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

为:

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

等。

这是一个简单的改变,但确实产生了丑陋的代码。

答案 2 :(得分:2)

实际上,您需要的信息不会存储在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

这似乎是正确的,您可以根据您的具体需要调整此方法。 tokenizeast更冗长,但也更灵活。当然,最好的方法是将它们用于任务的不同部分。

编辑:我在Python 3.4中试过这个,但我认为它也可以在其他版本中使用。

答案 3 :(得分:1)

我的解决方案采用了不同的路径:当我必须更改另一个文件中的代码时,我打开文件,找到该行并获得所有下一行比第一行更深的缩进并返回第一行的行号哪个不深。 如果我找不到我要找的文字,我会返回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

答案 4 :(得分:1)

有一个新的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'

答案 5 :(得分:0)

现在可以通过end_lineno since Python 3.8来获得。