我想以编程方式编辑python源代码。基本上我想读取一个.py文件,生成AST,然后写回修改后的python源代码(即另一个.py文件)。
.py
有一些方法可以使用标准的python模块来解析/编译python源代码,比如ast或compiler。但是,我认为它们都不支持修改源代码的方法(例如删除这个函数声明),然后再写回修改的python源代码。
ast
compiler
更新:我想这样做的原因是我想为python编写突变测试库,主要是通过删除语句/表达式,重新运行测试,看看有什么破坏。
您可能不需要重新生成源代码。当然,这对我来说有点危险,因为您实际上并没有解释为什么您认为需要生成一个充满代码的.py文件;但是:
如果你想生成一个人们实际使用的。py文件,也许这样他们就可以填写一个表单,并获得一个有用的。py文件插入到他们的项目中,然后你不想把它变成AST,因为你会失去所有的格式(想想那些空行,通过将相关的行集分组在一起,使Python如此可读) ( AST节点有lineno和col_offset属性)注释。相反,你可能会想使用模板引擎(Django模板语言,例如,被设计为使模板甚至文本文件容易)来定制.py文件,或者使用Rick Copeland的MetaPython扩展名。
lineno
col_offset
如果您试图在编译模块期间进行更改,请注意,您不必一直返回到文本;你可以直接编译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)
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代码。
codegen
ast.NodeVisitor
visitor_
另一个答案推荐codegen,它似乎已经被astor所取代。PyPI上的astor的版本(写这篇文章时是0.5版本)似乎也有点过时,所以你可以按照下面的方法安装astor的开发版本。
astor
pip install git+https://github.com/berkerpeksag/astor.git#egg=astor
然后你可以使用astor.to_source将Python AST转换为人类可读的Python源代码:
astor.to_source
>>> 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非解析包:
astunparse
>>> import ast >>> import astunparse >>> print(astunparse.unparse(ast.parse('def foo(x): return 2 * x'))) def foo(x): return (2 * x)
我们也有类似的需求,这里的其他答案并没有解决这个问题。因此,我们为此创建了一个库ASTTokens,它接受由ast或星状的模块生成的AST树,并用原始源代码中的文本范围标记它。
它不直接修改代码,但在上面添加代码并不难,因为它会告诉您需要修改的文本范围。
例如,它将函数调用包装在WRAP(...)中,保留注释和其他内容:
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)结构的句柄。有趣的是,您可以编译回这个结构并执行它,如上面所示。
ast.parse()
另一个非常有用的API是ast.dump(),它将整个AST转储为字符串形式。它可以用来检查树形结构,在调试中有很大的帮助。例如,
ast.dump()
在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时,他/她需要从它继承子类并相应地编写节点转换。
ast.NodeTransformer
对于我们的例子,让我们试着编写一个简单的实用程序,它可以将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")。
print " x is %s" % ("Hello Python")
我以前使用baron,但现在已经切换到parso,因为它是现代python的最新版本。效果很好。
我还需要这个做变异测试。用parso做一个真的很简单,看看我在https://github.com/boxed/mutmut的代码
如果你在2019年看这个,那么你可以使用这个libcst 包中。它的语法类似ast。这就像一个魅力,并保留了代码结构。它基本上对你必须保留注释、空格、换行符等的项目很有帮助
如果你不需要关心保留注释、空格和其他,那么ast和阿斯特的组合工作得很好。
ast.unparse(ast_obj)
取消解析ast.AST对象并生成一个字符串,如果使用ast.parse()进行解析,该字符串将生成一个等效的ast.AST对象。
不幸的是,上面的答案实际上没有一个同时满足这两个条件
我最近写了一个小工具包来做纯基于AST的重构,叫做重构。例如,如果你想用42替换所有的__abc0,你可以简单地像这样写一个规则;
42
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()