xis*_*xis 39 python abstract-syntax-tree
我正在尝试在Python中操作另一个脚本的脚本,要修改的脚本具有如下结构:
class SomethingRecord(Record):
description = 'This records something'
author = 'john smith'
Run Code Online (Sandbox Code Playgroud)
我ast
用来找到description
行号,并使用一些代码来更改原始文件,其中新的描述字符串基于行号.到现在为止还挺好.
现在唯一的问题description
偶尔是一个多行字符串,例如
description = ('line 1'
'line 2'
'line 3')
Run Code Online (Sandbox Code Playgroud)
要么
description = 'line 1' \
'line 2' \
'line 3'
Run Code Online (Sandbox Code Playgroud)
我只有第一行的行号,而不是以下行.所以我的单行替代品会这样做
description = 'new value'
'line 2' \
'line 3'
Run Code Online (Sandbox Code Playgroud)
并且代码被破坏了.我想如果我知道开始和结束的行号/ description
赋值行数我可以修复我的代码来处理这种情况.如何使用Python标准库获取此类信息?
我看了其他的答案; 当你真正的问题是修改代码时,似乎人们正在做后空翻来解决计算行号的问题.这表明基线机制并没有以您真正需要的方式帮助您.
如果你使用程序转换系统(PTS),你可以避免很多这些废话.
一个好的PTS会将您的源代码解析为AST,然后让您应用源级重写规则来修改AST,并最终将修改后的AST转换回源文本.通常PTS接受基本上这种形式的转换规则:
if you see *this*, replace it by *that*
Run Code Online (Sandbox Code Playgroud)
[构建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')";
Run Code Online (Sandbox Code Playgroud)
一个转换规则被赋予一个名称replace_description,以区别于我们可能定义的所有其他规则.规则参数(e:表达式)表示模式将允许源语言定义的任意表达式. statement-> statement表示规则将源语言中的语句映射到目标语言中的语句; 我们可以使用提供给DMS的Python语法中的任何其他语法类别.的"这里使用的是一个metaquote,用来区分规则语言的语法形成被摄体语言的语法的第二个.- > 分隔的源极图案此从目标图案那个.
您会注意到没有必要提及行号.PTS通过使用用于解析源文件的相同解析器实际解析模式,将规则表面语法转换为相应的AST.为模式生成的AST用于实现模式匹配/替换.因为这是由AST驱动的,所以原始代码的实际布局(间距,换行符,注释)不会影响DMS的匹配或替换能力.注释不是匹配的问题,因为它们附加到树节点而不是树节点; 它们保存在改造后的程序中.DMS确实捕获所有树元素的行和精确列信息; 只是不需要实现转换.使用该行/列信息,DMS在输出中也保留代码布局.
其他PTS提供了大致相似的功能.
作为解决方法,您可以更改:
description = 'line 1' \
'line 2' \
'line 3'
Run Code Online (Sandbox Code Playgroud)
至:
description = 'new value'; tmp = 'line 1' \
'line 2' \
'line 3'
Run Code Online (Sandbox Code Playgroud)
等等
这是一个简单的改变,但确实产生了丑陋的代码.
end_lineno
从 Python 3.8开始可用。
事实上,您需要的信息并未存储在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
Run Code Online (Sandbox Code Playgroud)
这就是我建议要做的:
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))
Run Code Online (Sandbox Code Playgroud)
这打印出:
5
12
17
23
Run Code Online (Sandbox Code Playgroud)
这似乎是正确的,您可以根据您的具体需要调整此方法。tokenize
比 更详细,ast
但也更灵活。当然,最好的方法是将它们用于任务的不同部分。
编辑:我在Python 3.4中尝试过这个,但我认为它也应该适用于其他版本。