191

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

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

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

4

13 回答 13

80

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

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

如果您想进行更多重构(如转换),rope 项目可能会满足您的需求

ast模块是您的另一个选择,并且有一个较旧的示例说明如何将语法树“解析”回代码(使用解析器模块)。但是该ast模块在对代码进行 AST 转换然后转换为代码对象时更有用。

redbaron项目也可能很合适(ht Xavier Combelle

于 2009-04-20T17:04:21.707 回答
60

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

于 2009-04-20T17:05:41.967 回答
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 上对此进行了测试。

于 2016-09-09T13:52:09.240 回答
20

花了一段时间,但 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

ast.unparse(ast_obj)

反解析一个 ast.AST 对象并生成一个字符串,其代码如果用 ast.parse() 解析回来,将产生一个等效的 ast.AST 对象。

于 2020-08-02T04:00:50.213 回答
19

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

  • 如果您想生成人们将实际使用的 .py 文件,也许这样他们就可以填写表格并获得有用的 .py 文件以插入到他们的项目中,那么您不想将其更改为 AST 并且回来,因为你会失去所有的格式(想想那些通过将相关的行组合在一起使 Python 如此可读的空行)ast 节点有linenocol_offset属性)注释。相反,您可能希望使用模板引擎(例如Django 模板语言,旨在使模板甚至文本文件变得容易)来自定义 .py 文件,或者使用 Rick Copeland 的MetaPython扩展。

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

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

更新:既然您已经解释了您要做什么,我还是很想对 AST 进行操作。您将希望通过删除而不是文件的行来进行变异(这可能导致半语句简单地因 SyntaxError 而死),而是整个语句——还有什么比在 AST 中更好的地方来做到这一点?

于 2009-04-20T16:44:53.280 回答
12

在模块的帮助下,解析和修改代码结构当然是可能的,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 时,他/她需要从中继承子类并相应地编写节点转换。

对于我们的示例,让我们尝试编写一个简单的实用程序,将 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").

于 2017-01-23T08:14:03.070 回答
6

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

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

PS 我尝试过扩展codegen,但它的架构是基于ast.NodeVisitor接口的,所以格式化程序(visitor_方法)只是函数。我发现这种结构非常有限且难以优化(在长且嵌套的表达式的情况下,保留对象树并缓存一些部分结果更容易 - 以其他方式,如果您想搜索最佳布局,您可以达到指数复杂性)。但是 codegen,mitsuhiko 的每一部作品(我读过)都写得非常好,简洁。

于 2013-09-21T21:22:17.983 回答
6

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

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

于 2019-09-12T15:45:26.237 回答
5

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

于 2016-08-17T15:50:17.897 回答
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

希望这可以帮助!

于 2016-12-14T02:39:21.973 回答
2

不幸的是,上面的答案都没有真正满足这两个条件

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

我最近编写了一个小工具包来进行纯 AST 的重构,称为refactor。例如,如果您想将所有placeholders替换为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()
于 2021-07-30T18:55:48.633 回答
1

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

当然,您需要一个程序转换引擎,它可以解析您感兴趣的语言,并且仍然进行模式导向的转换。我们的DMS Software Reengineering Toolkit是一个可以做到这一点的系统,它可以处理 Python 和各种其他语言。

有关 Python准确捕获评论的 DMS 解析 AST 示例,请参阅此SO 答案。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 进行漂亮打印。

于 2010-03-09T04:51:50.723 回答
1

我曾经为此使用 baron,但现在已切换到 parso,因为它与现代 python 是最新的。它工作得很好。

我还需要这个用于突变测试仪。用 parso 制作一个真的很简单,在https://github.com/boxed/mutmut查看我的代码

于 2018-08-11T05:22:58.897 回答