使用Python分析Python代码

2020-08-20 21:45:52

作为一种职业,软件工程的一个特殊之处就是可能从事富有诗意的工作:我们工作的一部分是制造针对我们自己工作的工具;也许世界各地的一些外科医生可以设计和融合他们自己的手术刀,但对于软件工程师来说,构建我们自己的工具是每天的现实。

最近,我一直在迁移要用Bazel构建的大型代码库。为了正确地做到这一点,我必须生成超过100个不同的构建文件。手工操作会很慢,容易出错,而且会让人精疲力竭,所以我选择了编写一个自动为我做这件事的工具。

在描述Bazel的构建模块时,必须将每个测试文件定义为*_test规则,该规则显式定义它所具有的所有依赖项。例如,为了让Bazel运行Python测试,可以编写类似以下内容的代码:

Py_test(name=";test_context";,srcs=[";test_context.py";],main=";test_context.py";,tag=[],deps=[";:context";,";//libs/config";,],)。

要编写将为存储库中的每个测试文件生成此块的程序,第一步是找出存储库中的哪些文件是测试文件。在我正在处理的存储库上下文中,测试文件可以定义为:

听起来很简单,我们当然可以用一些正则表达式来做一些事情,对吗?嗯,我们可以,但是有很多边缘情况需要考虑,如果我们可以用Python解释器看到代码的相同方式来看待代码,那不是很棒吗?

事实证明有一种简单的方法可以做到这一点,Python标准库包含一个整洁的小包,名为AST-Abstract SynTax Tree的缩写。维基百科将AST定义为:

在计算机科学中,抽象语法树(AST),或者仅仅是语法树,是用编程语言编写的源代码的抽象语法结构的树表示。树的每个节点表示源代码中出现的一个构造。

AST模块帮助Python应用程序处理Python抽象语法树。抽象语法本身可能会随每个Python发行版而改变;该模块有助于以编程方式找出当前语法是什么样子。

在软件工程中,构建程序来分析另一个程序的源代码而不实际执行它的做法称为静态代码分析。通过对我们感兴趣的语言使用AST解析器,我们可以将源代码转换为可以像处理任何其他代码一样进行处理的数据。使用Python标准库的ast包,我们可以将Python代码块解析成可以遍历和分析的数据结构。这对于回答文件是否包含单元测试非常方便!

导入os def is_test_file(Path)->;bool:#TODO:impl pass def find_test_files(Repo_Root):test_files=[]表示操作系统中的根、目录、文件。Walk(Repo_Root):对于FILES中的文件:如果不是FILE。Endswith(";.py";):Continue path=os。路径。Join(root,file)if is_test_file(Path):test_files。追加(路径)返回test_files。

Python 3.7.7(默认,2020年7月15日,21:51:02)[Clang 10.0.1(clang-1001.0.46.4)]on Darwin Type";Help";,";,";Credits";或";License";了解详细信息。>;>;>;来源=";";";...。类测试(unittest.TestCase):...。Def test_hello(自身):...。通过..。";";>;>;>;导入ast>;>;>;tree=ast。解析(源)>;>;>;打印(树)<;_ast。位于0x10379c6d0>的模块对象;

因此,当使用ast.parse解析源代码块时,我们会得到一个树状数据结构,它具有:

在我们的文件中,只有一个元素,即名为Test的ClassDef对象。

Body有一个名为test_hello的FunctionDef(我们的测试方法定义。

Base是我们的ClassDef继承的基类列表,在我们的例子中,如果我们稍微斜视一下,就会看到unittest.TestCase

太好了,这里有我们需要的一切来做决定。我们的最终方法将如下所示:

将ast def is_test_file(Abspath)->;bool:with open(abspath,";r";)导入为f:data=f。Read()source_ast=ast。解析(Data)source_ast中的节点。正文:if isinstance(node,ast。ClassDef)和_is_testcase_class(Node):用于节点中的class_node。正文:if isinstance(class_node,ast。FunctionDef)和class_node。名字。以(";test_";)开始:返回True返回False def_is_testcase_class(self,classdef:ast.。ClassDef)->;bool:用于classdef中的base_class。Bases:#if测试类看起来像class Test(TestCase)if isinstance(base_class,ast.。名称):base_name=base_class。Id#如果测试类类似于class Test(unittest.TestCase):elif isinstance(base_class,ast。属性):base_name=base_class。属性否则:如果base_name==';测试用例:返回True,则继续;返回False。

如果找到,我们将通过查看类定义的base属性来检查它是否继承自unittest.TestCase。

如果我们找到TestCase类,我们将遍历ClassDef主体,查找名称以test_开头的FunctionDef。

当然,我们可能会使用带有一些基于启发式的正则表达式的shell脚本来获得良好的结果,但是使用AST来解析解释器看到的源代码,我们可以得到明确的答案,这些答案对于任何边缘情况都是健壮的,比如奇怪的格式和将代码放在注释或字符串中。能够创建工具来简化您的工作是作为一名软件工程师最好的事情之一,能够以编程方式解析和分析您自己的源代码将您的工具构建可能性提升到了一个新的水平!