问题:解析一个.py文件,读取AST,对其进行修改,然后写回修改后的源代码
我想以编程方式编辑python源代码。基本上,我想读取一个.py
文件,生成AST,然后写回修改后的python源代码(即另一个.py
文件)。
有多种方法可以使用标准python模块(例如ast
或)来解析/编译python源代码compiler
。但是,我认为它们都不支持修改源代码(例如删除此函数声明)然后写回修改后的python源代码的方法。
更新:我要这样做的原因是我想为python 编写一个Mutation测试库,主要是通过删除语句/表达式,重新运行测试并查看中断。
回答 0
Pythoscope会对自动生成的测试用例执行此操作,就像python 2.6 的2to3工具一样(它将python 2.x源转换为python 3.x源)。
这两个工具都使用lib2to3库,该库是python解析器/编译器机制的实现,当从源-> AST->源往返时,可以在源中保留注释。
该绳项目,如果你想要做的更像变换重构可满足您的需求。
该AST模块是你的其他选择,并有一个如何“unparse”语法树放回代码旧的例子(使用解析器模块)。但是,ast
当对代码进行AST转换,然后将其转换为代码对象时,该模块更有用。
该redbaron项目也可能是一个不错的选择(HT泽维尔Combelle)
回答 1
内置的ast模块似乎没有方法可以转换回源代码。但是,这里的codegen模块为ast提供了一台漂亮的打印机,使您能够这样做。例如。
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()并执行生成的代码对象即可。
回答 2
在一个不同的答案中,我建议使用该astor
程序包,但此后我发现了一个名为AST的最新的非解析程序包astunparse
:
>>> import ast
>>> import astunparse
>>> print(astunparse.unparse(ast.parse('def foo(x): return 2 * x')))
def foo(x):
return (2 * x)
我已经在Python 3.5上进行了测试。
回答 3
您可能不需要重新生成源代码。当然,这对我来说有点危险,因为您尚未真正解释为什么您认为需要生成一个充满代码的.py文件。但:
如果您想生成一个供人们实际使用的.py文件,也许以便他们可以填写表格并获得一个有用的.py文件以插入其项目中,那么您就不想将其更改为AST和返回,因为您将丢失
所有格式设置(想像一下通过将相关的行集合在一起使Python易于阅读的空白行)(ast节点具有lineno
和col_offset
属性)注释。相反,您可能需要使用模板引擎(例如,Django模板语言旨在简化模板文本文件)来自定义.py文件,或者使用Rick Copeland的MetaPython扩展。如果要在模块编译期间进行更改,请注意,您不必一直回到文本;您可以直接编译AST,而不必将其重新转换为.py文件。
但是在几乎所有情况下,您可能都在尝试做一些动态的事情,像Python这样的语言实际上很容易,而无需编写新的.py文件!如果您扩展问题以使我们知道您实际要完成的工作,那么答案中可能根本不会涉及新的.py文件;我已经看到数百个Python项目在做数百个现实世界的事情,而编写一个.py文件并不需要它们中的任何一个。因此,我必须承认,我有点怀疑您已经找到了第一个好的用例。:-)
更新:既然您已经解释了您要做什么,那么无论如何我都会很想直接在AST上进行操作。您将希望通过删除而不是删除文件的行来进行更改(这可能导致半语句仅因SyntaxError而死),而是通过整个语句来进行更改,那么与AST相比,还有什么更好的地方呢?
回答 4
在ast
模块的帮助下,解析和修改代码结构当然是可能的,我将在稍后的示例中进行演示。但是,ast
单独使用模块无法写回修改后的源代码。还有其他可用于此工作的模块,例如此处的一个。
注意:以下示例可被视为有关ast
模块用法的入门教程,但是有关使用ast
模块的更全面指南,可从Green Tree snakes教程和有关ast
模块的官方文档中获得。
简介ast
:
>>> import ast
>>> tree = ast.parse("print 'Hello Python!!'")
>>> exec(compile(tree, filename="<ast>", mode="exec"))
Hello Python!!
您可以通过简单地调用API来解析python代码(以字符串表示)ast.parse()
。这将句柄返回到抽象语法树(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时,他/她都需要从AST中继承子类并相应地编写Node Transformation。
对于我们的示例,让我们尝试编写一个简单的实用程序,将Python 2的print语句转换为Python 3函数调用。
打印语句到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")
。
回答 5
我最近创建了相当稳定的(核心真的经过了很好的测试)和可扩展的代码,这些代码从ast
树中生成了代码:https : //github.com/paluh/code-formatter。
我将我的项目用作小vim插件的基础(我每天都在使用),所以我的目标是生成非常好的可读性python代码。
PS我已经尝试扩展,codegen
但是它的体系结构是基于ast.NodeVisitor
接口的,所以格式化程序(visitor_
方法)只是功能。我发现这种结构相当局限且难以优化(在长且嵌套的表达式的情况下,保留对象树并缓存部分结果更容易-如果您要搜索最佳布局,则可以用其他方式达到指数复杂性)。但是, codegen
由于光彦的每件作品(我读过的作品)都写得很简洁。
回答 6
建议的其他答案之一codegen
,似乎已被取代astor
。astor
PyPI的版本(在撰写本文时为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上进行了测试。
回答 7
如果您在2019年查看此内容,则可以使用此libcst 软件包。它的语法类似于ast。这就像一个魅力,并保留了代码结构。它对于必须保留注释,空格,换行符等的项目基本上是有帮助的。
如果您不需要关心保留的注释,空格和其他内容,则ast和astor的组合效果很好。
回答 8
我们有类似的需求,但这里没有其他答案可以解决。因此,我们为此创建了一个库ASTTokens,该库使用由ast或astroid生成的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
希望这可以帮助!
回答 9
一个程序变换系统是一个工具,解析源文本,建立AST的,允许您使用源到源转换(“如果你看到这个模式,通过该模式取代它”)对其进行修改。此类工具非常适合对现有源代码进行变异,这些变异只是“如果您看到此模式,请替换为模式变体”。
当然,您需要一个程序转换引擎,该引擎可以解析您感兴趣的语言,并且仍然进行模式导向的转换。我们的DMS软件再造工具包是一个可以执行此操作的系统,可以处理Python和多种其他语言。
请参阅此SO答案,以获取DMS解析的AST的示例,该AST用于Python准确捕获注释。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进行漂亮打印。
回答 10
我曾经为此使用男爵,但现在切换到parso,因为它与现代python保持同步。效果很好。
对于突变测试仪,我也需要它。用parso制作一个非常简单,请在https://github.com/boxed/mutmut上查看我的代码