解析一个.py文件,读取AST,对其进行修改,然后写回修改后的源代码


168

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

有多种方法可以使用标准python模块(例如ast或)来解析/编译python源代码compiler。但是,我认为它们都不支持修改源代码(例如删除此函数声明)然后写回修改后的python源代码的方法。

更新:我要这样做的原因是我想为python 编写一个Mutation测试库,主要是通过删除语句/表达式,重新运行测试并查看中断。


4
从2.6版开始不推荐使用:编译器软件包已在Python 3.0中删除。
dfa

1
您无法编辑什么内容?你为什么不能写一个装饰器?
S.Lott

3
天啊!我想使用相同的技术(特别是创建一个鼻子插件)为python创建一个突变测试器,您是否打算开源?
Ryan

2
@Ryan是的,我将开源任何我创建的东西。我们应该保持联系
Rory

1
当然,我通过启动板给您发送了电子邮件。
Ryan

Answers:


73

Pythoscope会对自动生成的测试用例执行此操作,就像python 2.6 的2to3工具一样(它将python 2.x源转换为python 3.x源)。

这两个工具都使用lib2to3库,该库是python解析器/编译器机制的实现,当从源-> AST->源往返时,可以在源中保留注释。

绳项目,如果你想要做的更像变换重构可满足您的需求。

AST模块是你的其他选择,并有一个如何“unparse”语法树放回代码旧的例子(使用解析器模块)。但是,ast当对代码进行AST转换,然后将其转换为代码对象时,该模块更有用。

redbaron项目也可能是一个不错的选择(HT泽维尔Combelle)


5
未解析的示例仍保持不变,以下是更新的py3k版本:hg.python.org/cpython/log/tip/Tools/parser/unparse.py
Janus Troelsen

2
关于unparse.py脚本-从另一个脚本中使用它可能真的很麻烦。但是,有一个名为astunparse的软件包(在github上在pypi上),基本上是的正确打包的版本unparse.py
mbdevpl

您是否可以通过添加parso作为首选选项来更新您的答案?非常好,已经更新。
盒装

59

内置的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()并执行生成的代码对象即可。


20
对于将来使用此功能的任何人,codegen基本上已经过时并且存在一些错误。我已经修复了其中的一些;我在github上有一个要点:gist.github.com/791312
mattbasta 2011年

请注意,在上述注释之后,最新的代码生成器已于2012年更新,因此我想代码生成器已更新。@mattbasta
zjffdu

4
astor似乎是
Codegen的

20

在一个不同的答案中,我建议使用该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上进行了测试。


19

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

  • 如果您想生成一个供人们实际使用的.py文件,也许以便他们可以填写表格并获得一个有用的.py文件以插入其项目中,那么您就不想将其更改为AST和返回,因为您将丢失所有格式设置(想像一下通过将相关的行集合在一起使Python易于阅读的空白行)ast节点具有linenocol_offset属性)注释。相反,您可能需要使用模板引擎(例如,Django模板语言旨在简化模板文本文件)来自定义.py文件,或者使用Rick Copeland的MetaPython扩展。

  • 如果要在模块编译期间进行更改,请注意,您不必一直回到文本;您可以直接编译AST,而不必将其重新转换为.py文件。

  • 但是在几乎所有情况下,您可能都在尝试做一些动态的事情,像Python这样的语言实际上很容易,而无需编写新的.py文件!如果您扩展问题以使我们知道您实际要完成的工作,那么答案中可能根本不会涉及新的.py文件;我已经看到数百个Python项目在做数百个现实世界的事情,而编写一个.py文件并不需要它们中的任何一个。因此,我必须承认,我有点怀疑您已经找到了第一个好的用例。:-)

更新:既然您已经解释了您要做什么,那么无论如何我都会很想直接在AST上进行操作。您将希望通过删除而不是删除文件的行来进行更改(这可能导致半语句仅因SyntaxError而死),而是通过整个语句来进行更改,那么与AST相比,还有什么更好的地方呢?


很好地概述了可能的解决方案和可能的替代方案。
Ryan

1
现实世界中用于代码生成的用例:Kid和Genshi(我相信)从XML模板生成Python,以快速呈现动态页面。
里克·科普兰

10

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")


6

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

我将我的项目用作小vim插件的基础(我每天都在使用),所以我的目标是生成非常好的可读性python代码。

PS我已经尝试扩展,codegen但是它的体系结构是基于ast.NodeVisitor接口的,所以格式化程序(visitor_方法)只是功能。我发现这种结构相当局限且难以优化(在长且嵌套的表达式的情况下,保留对象树并缓存部分结果更容易-如果您要搜索最佳布局,则可以用其他方式达到指数复杂性)。但是, codegen由于光彦的每件作品(我读过的作品)都写得很简洁。


4

建议的其他答案之一codegen,似乎已被取代astorastorPyPI的版本(撰写本文时为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上进行了测试。


4

如果您在2019年查看此内容,则可以使用此libcs​​t 软件包。它的语法类似于ast。这就像一个魅力,并保留了代码结构。它对于必须保留注释,空格,换行符等的项目基本上是有帮助的。

如果您不需要关心保留的注释,空格和其他内容,则ast和astor的组合效果很好。


2

我们有类似的需求,但这里没有其他答案可以解决。因此,我们为此创建了一个库ASTTokens,该库使用由astastroid生成的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

希望这可以帮助!


1

一个程序变换系统是一个工具,解析源文本,建立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进行漂亮打印。


我已经有4年没有看这个答案了。哇,它被否决了好几次。这真是惊人的,因为它可以直接回答OP的问题,甚至可以显示如何进行他想做的变异。我不认为任何拒绝投票的人都愿意解释为什么他们拒绝投票。
Ira Baxter 2014年

4
因为它促进了非常昂贵的封闭源工具。
Zoran Pavlovic 2015年

@ZoranPavlovic:所以您不反对其技术准确性或实用性吗?
Ira Baxter

2
@佐兰:他没有说他有一个开源库。他说他想修改Python源代码(使用AST),但他找到的解决方案没有做到这一点。这是一个解决方案。您不认为人们在用Java等Python语言编写的程序上使用商业工具吗?
伊拉·巴克斯特

1
我不是选民,但帖子的内容有点像广告。为了改善答案,您可以披露您与该产品相关联
2016年

0

我曾经为此使用男爵,但现在切换到parso,因为它与现代python保持同步。效果很好。

对于突变测试仪,我也需要它。用parso制作一个非常简单,请在https://github.com/boxed/mutmut上查看我的代码

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.