如何在Python中获得“语句结束”的行号

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

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

我使用ast来定位description行号,并且使用一些代码来根据行号更改原始文件和新描述string。 到现在为止还挺好。

现在唯一的问题是偶尔的description是一个多行string,例如

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

要么

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

而且我只有第一行的行号,而不是下面的行。 所以我的单线替代品就可以做到了

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

代码被破坏 我认为,如果我知道description任务的开始和结束/行数的行号,我可以修复我的代码来处理这种情况。 如何使用Python标准库获取这些信息?

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

如果你使用程序转换系统(PTS) ,你可以避免很多这样的废话。

一个好的PTS会将你的源代码parsing为一个AST,然后让你应用源代码级的重写规则来修改AST,并最终将修改后的AST转换回源文本。 一般来说PTS接受本质上这种forms的转换规则:

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

[构buildAST的parsing器不是PTS。 他们不允许这样的规则; 你可以编写专门的代码来破解这个树,但这通常很尴尬。 他们没有做AST来源文本再生。]

(我的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:expression式)表示该模式将允许由源语言定义的任意expression式。 statement->语句表示规则将源语言中的语句映射到目标语言中的语句; 我们可以使用提供给DMS的Python语法中的任何其他语法类别。 这里使用的是一个元语言 ,用来区分规则语言的语法forms和主题语言的语法;第二把源语言模式和目标语言模式分开。

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

其他PTS通常提供类似的function。

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

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

至:

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

等等

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

事实上,你需要的信息并不存储在ast 。 我不知道你需要什么细节,但看起来你可以使用标准库中的tokenize模块。 这个想法是每个逻辑Python语句都以一个NEWLINE标记结束(也可以是一个分号,但据我所知,这不是你的情况)。 我用这样的文件testing了这个方法:

 # 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中试过,但我认为它也应该在其他版本中工作。

我的解决scheme采取了不同的path:当我不得不改变另一个文件中的代码时,我打开文件,find该行,并获得了比第一个更深的缩进的所有下一行,并返回第一行的行号,更深。 如果我找不到我正在寻找的文本,我将返回无,无。 这当然是不完整的,但我认为这足以让你通过:)

 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 

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