解析.py文件,读取AST,修改它,然后写回修改后的源代码

我想以编程方式编辑python源代码。基本上我想读取一个.py文件,生成AST,然后写回修改后的python源代码(即另一个.py文件)。

有一些方法可以使用标准的python模块来解析/编译python源代码,比如astcompiler。但是,我认为它们都不支持修改源代码的方法(例如删除这个函数声明),然后再写回修改的python源代码。

更新:我想这样做的原因是我想为python编写突变测试库,主要是通过删除语句/表达式,重新运行测试,看看有什么破坏。

67999 次浏览

您可能不需要重新生成源代码。当然,这对我来说有点危险,因为您实际上并没有解释为什么您认为需要生成一个充满代码的.py文件;但是:

  • 如果你想生成一个人们实际使用的。py文件,也许这样他们就可以填写一个表单,并获得一个有用的。py文件插入到他们的项目中,然后你不想把它变成AST,因为你会失去所有的格式(想想那些空行,通过将相关的行集分组在一起,使Python如此可读) ( AST节点有linenocol_offset属性)注释。相反,你可能会想使用模板引擎(Django模板语言,例如,被设计为使模板甚至文本文件容易)来定制.py文件,或者使用Rick Copeland的MetaPython扩展名。

  • 如果您试图在编译模块期间进行更改,请注意,您不必一直返回到文本;你可以直接编译AST,而不是把它转换回.py文件。

  • 但在几乎所有情况下,您可能都在尝试做一些动态的事情,而像Python这样的语言实际上非常容易做到,而不需要编写新的.py文件!如果你扩大你的问题,让我们知道你真正想要完成什么,新的.py文件可能根本不会涉及到答案;我见过成百上千的Python项目做了成百上千的实际工作,没有一个项目需要编写.py文件。所以,我必须承认,我有点怀疑你已经找到了第一个好的用例。: -)

更新:既然你已经解释了你要做的事情,我想无论如何都要对AST进行操作。您可能希望通过删除整个语句而不是删除文件中的行(这可能导致半条语句,并简单地使用SyntaxError终止)来进行更改—还有什么地方比在AST中更好地做到这一点呢?

Pythoscope对它自动生成的测试用例执行此操作,正如python 2.6的版本2工具所做的那样(它将python 2。X源代码转换为python 3。x源)。

这两个工具都使用lib2to3库,它是python解析器/编译器机制的实现,可以在从source ->AST→源。

如果你想做更多的重构,比如转换,绳的项目可以满足你的需求。

ast模块是你的另一个选择,还有有一个关于如何“unparse_”的老例子。语法树返回到代码中(使用解析器模块)。但是ast模块在对随后转换为代码对象的代码进行AST转换时更有用。

redbaron项目也可能是一个很好的选择(ht Xavier Combelle)

内置ast模块似乎没有转换回源代码的方法。然而,这里的codegen模块为ast提供了一个漂亮的打印机,使您能够这样做。 如:< / p >
import ast
import codegen


expr="""
def foo():
print("hello world")
"""
p=ast.parse(expr)


p.body[0].body = [ ast.parse("return 42").body[0] ] # Replace function body with "return 42"


print(codegen.to_source(p))

这将打印:

def foo():
return 42

请注意,您可能会丢失确切的格式和注释,因为这些没有保留。

但是,您可能不需要这样做。如果您所需要的只是执行替换的AST,那么只需在AST上调用compile()并执行结果代码对象即可。

程序转换系统是一个解析源文本、构建ast的工具,允许你使用源到源转换来修改它们(“如果你看到这个模式,就用那个模式替换它”)。这样的工具非常适合对现有源代码进行突变,即“如果看到此模式,请用模式变体替换”。

当然,您需要一个程序转换引擎,它可以解析您感兴趣的语言,并仍然执行面向模式的转换。我们的DMS软件再造工具包是一个可以做到这一点的系统,并处理Python和其他各种语言。

准确地看到这个所以回答一个用于捕获注释的dms解析AST的例子。DMS可以对AST进行更改,并重新生成有效文本,包括注释。您可以要求它使用自己的格式约定(您可以更改这些约定)对AST进行美化打印,或者进行“保真打印”,即使用原始的行和列信息来最大限度地保留原始布局(插入新代码时不可避免地会对布局进行一些更改)。

要用DMS实现Python的“突变”规则,您可以编写以下代码:

rule mutate_addition(s:sum, p:product):sum->sum =
" \s + \p " -> " \s - \p"
if mutate_this_place(s);

该规则以语法正确的方式将“+”替换为“-”;它对AST进行操作,因此不会触及碰巧看起来正确的字符串或注释。“mutate_this_place”上的额外条件是让您控制这种情况发生的频率;你不希望在程序中改变每一个的位置。

显然,您需要更多这样的规则来检测各种代码结构,并将它们替换为变异的版本。DMS乐于应用一组规则。突变的AST随后被漂亮地打印出来。

我最近创建了相当稳定(核心是经过良好测试的)和可扩展的代码段,它从ast树生成代码:https://github.com/paluh/code-formatter

我正在使用我的项目作为一个小vim插件的基础(我每天都在使用),所以我的目标是生成非常漂亮和可读的python代码。

< p >注: 我试图扩展codegen,但它的架构是基于ast.NodeVisitor接口,所以格式化器(visitor_方法)只是函数。我发现这种结构非常有限,很难优化(在长嵌套表达式的情况下,更容易保持对象树和缓存一些部分结果-如果你想搜索最佳布局,另一种方式你可以达到指数级的复杂性)。 codegen,因为mitsuhiko的每一篇作品(我读过)都写得很好,很简洁

另一个答案推荐codegen,它似乎已经被astor所取代。PyPI上的astor的版本(写这篇文章时是0.5版本)似乎也有点过时,所以你可以按照下面的方法安装astor的开发版本。

pip install git+https://github.com/berkerpeksag/astor.git#egg=astor

然后你可以使用astor.to_source将Python AST转换为人类可读的Python源代码:

>>> import ast
>>> import astor
>>> print(astor.to_source(ast.parse('def foo(x): return 2 * x')))
def foo(x):
return 2 * x

我已经在Python 3.5上进行了测试。

在另一个答案中,我建议使用astor包,但我后来发现了一个名为astunparse的最新AST非解析包:

>>> import ast
>>> import astunparse
>>> print(astunparse.unparse(ast.parse('def foo(x): return 2 * x')))




def foo(x):
return (2 * x)

我已经在Python 3.5上进行了测试。

我们也有类似的需求,这里的其他答案并没有解决这个问题。因此,我们为此创建了一个库ASTTokens,它接受由ast星状的模块生成的AST树,并用原始源代码中的文本范围标记它。

它不直接修改代码,但在上面添加代码并不难,因为它会告诉您需要修改的文本范围。

例如,它将函数调用包装在WRAP(...)中,保留注释和其他内容:

example = """
def foo(): # Test
'''My func'''
log("hello world")  # Print
"""


import ast, asttokens
atok = asttokens.ASTTokens(example, parse=True)


call = next(n for n in ast.walk(atok.tree) if isinstance(n, ast.Call))
start, end = atok.get_text_range(call)
print(atok.text[:start] + ('WRAP(%s)' % atok.text[start:end])  + atok.text[end:])

生产:

def foo(): # Test
'''My func'''
WRAP(log("hello world"))  # Print

希望这能有所帮助!

ast模块的帮助下,解析和修改代码结构当然是可能的,我将在稍后的示例中展示它。然而,仅使用ast模块是不可能写回修改后的源代码的。还有其他模块可用于此工作,例如在这里

注意:下面的例子可以作为使用ast模块的入门教程,但是更全面的使用ast模块的指南可以在绿树蛇教程ast模块的官方文档找到。

ast简介:

>>> import ast
>>> tree = ast.parse("print 'Hello Python!!'")
>>> exec(compile(tree, filename="<ast>", mode="exec"))
Hello Python!!

你可以通过调用API ast.parse()来解析python代码(以字符串表示)。它返回抽象语法树(AST)结构的句柄。有趣的是,您可以编译回这个结构并执行它,如上面所示。

另一个非常有用的API是ast.dump(),它将整个AST转储为字符串形式。它可以用来检查树形结构,在调试中有很大的帮助。例如,

在Python 2.7中:

>>> import ast
>>> tree = ast.parse("print 'Hello Python!!'")
>>> ast.dump(tree)
"Module(body=[Print(dest=None, values=[Str(s='Hello Python!!')], nl=True)])"

在Python 3.5上:

>>> import ast
>>> tree = ast.parse("print ('Hello Python!!')")
>>> ast.dump(tree)
"Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hello Python!!')], keywords=[]))])"

请注意Python 2.7和Python 3.5中print语句的语法差异,以及各自树中AST节点类型的差异。


如何使用ast修改代码:

现在,让我们看一个通过ast模块修改python代码的例子。修改AST结构的主要工具是ast.NodeTransformer类。每当一个人需要修改AST时,他/她需要从它继承子类并相应地编写节点转换。

对于我们的例子,让我们试着编写一个简单的实用程序,它可以将python2, print语句转换为python3函数调用。

打印语句到Fun调用转换工具:print2to3.py

#!/usr/bin/env python
'''
This utility converts the python (2.7) statements to Python 3 alike function calls before running the code.


USAGE:
python print2to3.py <filename>
'''
import ast
import sys


class P2to3(ast.NodeTransformer):
def visit_Print(self, node):
new_node = ast.Expr(value=ast.Call(func=ast.Name(id='print', ctx=ast.Load()),
args=node.values,
keywords=[], starargs=None, kwargs=None))
ast.copy_location(new_node, node)
return new_node


def main(filename=None):
if not filename:
return


with open(filename, 'r') as fp:
data = fp.readlines()
data = ''.join(data)
tree = ast.parse(data)


print "Converting python 2 print statements to Python 3 function calls"
print "-" * 35
P2to3().visit(tree)
ast.fix_missing_locations(tree)
# print ast.dump(tree)


exec(compile(tree, filename="p23", mode="exec"))


if __name__ == '__main__':
if len(sys.argv) <=1:
print ("\nUSAGE:\n\t print2to3.py <filename>")
sys.exit(1)
else:
main(sys.argv[1])

这个实用程序可以在一个小的示例文件上尝试,比如下面的一个,它应该可以正常工作。

输入文件:py2.py

class A(object):
def __init__(self):
pass


def good():
print "I am good"


main = good


if __name__ == '__main__':
print "I am in main"
main()

请注意,上面的转换仅用于ast教程的目的,在实际情况下,必须查看所有不同的场景,如print " x is %s" % ("Hello Python")

我以前使用baron,但现在已经切换到parso,因为它是现代python的最新版本。效果很好。

我还需要这个做变异测试。用parso做一个真的很简单,看看我在https://github.com/boxed/mutmut的代码

如果你在2019年看这个,那么你可以使用这个libcst 包中。它的语法类似ast。这就像一个魅力,并保留了代码结构。它基本上对你必须保留注释、空格、换行符等的项目很有帮助

如果你不需要关心保留注释、空格和其他,那么ast和阿斯特的组合工作得很好。

花了一段时间,但Python 3.9有这个: https://docs.python.org/3.9/whatsnew/3.9.html#ast https://docs.python.org/3.9/library/ast.html#ast.unparse < / p >
ast.unparse(ast_obj)

取消解析ast.AST对象并生成一个字符串,如果使用ast.parse()进行解析,该字符串将生成一个等效的ast.AST对象。

不幸的是,上面的答案实际上没有一个同时满足这两个条件

  • 保持周围源代码的语法完整性(例如保留注释,其他类型的代码格式)
  • 实际上使用AST(而不是CST)。

我最近写了一个小工具包来做纯基于AST的重构,叫做重构。例如,如果你想用42替换所有的__abc0,你可以简单地像这样写一个规则;

class Replace(Rule):
    

def match(self, node):
assert isinstance(node, ast.Name)
assert node.id == 'placeholder'
        

replacement = ast.Constant(42)
return ReplacementAction(node, replacement)

它会找到所有可接受的节点,用新节点替换它们并生成最终的表单;

--- test_file.py
+++ test_file.py


@@ -1,11 +1,11 @@


def main():
-    print(placeholder * 3 + 2)
-    print(2 +               placeholder      + 3)
+    print(42 * 3 + 2)
+    print(2 +               42      + 3)
# some commments
-    placeholder # maybe other comments
+    42 # maybe other comments
if something:
other_thing
-    print(placeholder)
+    print(42)
 

if __name__ == "__main__":
main()