cricheditview实现语法高亮和行号_在Python中从源文件和行号查找定义

35d2dc56e1e65dad0901285d3f4ada65.png

7d751cc38acee54ad88f6b9655b52c71.png

我在Datadog的工作让我一直忙于应对新的和有问题的挑战。最近我偶然发现了一个问题,听起来很简单,但比我想象的要难。

问题是这样的:假设有一个文件名和一个行号,您能说出这行代码属于哪个函数、方法或类吗?

我开始深入研究标准库,但没有找到解决这个问题的任何东西。听起来好像我必须自己编写一些东西来解决这个问题。

第一步听起来很容易。打开一个文件,读取它,找到行号。就是这样。

那么,您如何知道这一行是在哪个函数中呢?您是无法预期的,除非您解析整个文件并跟踪函数定义。那使用一个正则表达式来解析每一行可能是一个解决方案呢?

您必须小心,因为函数定义可以跨越多行。

使用AST

我认为一个好的健壮的策略不是使用手动解析或类似的方法,而是直接使用Python抽象语法树(AST)。通过利用Python自己的解析代码,我确信在解析Python源文件时不会失败。

这可以通过以下代码来简单地实现:

fea41245464012538f6c1087f599675b.png

这样您就完成了。是吗?并不是,因为这只适用于99.99%的情况。如果您的源文件使用的编码现在是ASCII或UTF-8,则该函数会失败。我知道您认为我疯了,但我希望我的代码是健壮的。

原来Python有一个cookie来以# encoding: utf-8的形式指定编码方式,正如PEP 263中定义的那样。阅读这个cookie将有助于找到其编码方式。

要做到这一点,我们需要以二进制模式打开文件,使用一个正则表达式来匹配数据,并且……嗯,它很乏味,而且已经有人为我们实现了它,所以让我们来使用Python所提供的神奇的tokenize.open函数:

2977bb66d6ae3c3476cab7cbde4269bc.png

这应该在100%的时间内都有效。直到被证明并非如此。

浏览AST

parse_file函数现在会返回一个Python AST。如果您从未使用过Python AST,那么它是一个巨大的树,表示您的源代码在被编译成Python字节码之前的样子。

在这个树中,应该有语句和表达式。在本例中,我们所感兴趣的是找到与我们的行号最接近的函数定义。下面是该函数的实现:  

50ad3a1d09e16f6b42861d22a2ee5bae.png

这个函数会遍历AST的所有节点,并返回行号与我们的定义最接近的节点。如果我们有一个文件包含以下代码:

fa2a3e7f6d56ef64c046c383b6a419c6.png

下面是以第一行到第五行为参数,调用函数filename_and_lineno_to_def得到的5个结果:

284a8cb63fea139c69d103893a1aed5c.png

以第一行到第五行为参数,调用函数filename_and_lineno_to_def

很有用!

闭包?

前面描述的简单方法可能适用于90%的代码,但也有一些边缘情况。例如,当定义函数闭包时,上面的算法就不行了。当有如下代码时:

4572a674923eefb82ebf73ed58ffcaec.png

以第1行到第7行为参数,调用函数filename_and_lineno_to_def:

5ed22af083e969e7ae0c589e25f211ea.png

返回第1行到第7行的filename_and_lineo_to_def的结果

哎呀。显然,第6行和第7行不属于foo函数。我们的方法太简单了,以至于从第6行开始,我们就回到了y方法。

区间树

正确处理上述问题的方法是将每个函数定义视为一个区间:

7d751cc38acee54ad88f6b9655b52c71.png

可以视为区间的代码片段

无论我们请求的行号是什么,我们都应该返回负责该行所在的最小间隔的节点。

在这种情况下,我们需要一个正确的数据结构来解决我们的问题:一个区间树正好非常适合我们的情况。它允许我们快速搜索与我们的行号相匹配的代码片段。

要解决我们的问题,我们需要以下几个东西:

  • 一种计算函数的起始行号和结束行号的方法。

  • 一个用我们之前计算的间隔来填充的树。

  • 当一个行是多个函数(闭包)的一部分时,选择最佳匹配区间的方法。

计算函数区间

一个函数的区间是组成该函数主体的第一行和最后一行。通过遍历函数AST节点很容易找到这些行:

87dabe75e35d44667f5d775bf144b882.png

对于任意AST节点,该函数会返回一个此节点的第一个和最后一个行号的元组。

构建区间树

我们将使用intervaltree库而不是自己实现一个区间树。我们需要创建一个树,并用计算出的区间来填充它: 

318e02260f28b3f298cf3cc659736f63.png

就是这样:该函数会解析作为参数传入的Python文件,并将其转换为它的AST表示。然后遍历它并为区间树提供每个类和函数定义。

查询区间树

现在已经构建了区间树,我们就可以使用行号来对它进行查询了。这很简单:

59834d0253b4056a9e3dbc244f697cfd.png

如果有多个区间包含我们的行号,则该构建树可能会返回几个匹配项。在这种情况下,我们选择最小的区间并返回此节点的名称——也就是我们的类名或函数名!

任务完成

我们做到了!我们从一个简单的方法开始,并迭代到一个最终的解决方案,它覆盖了我们100%的情况。选择正确的数据结构,这里是区间树,帮助我们用一个智能的方法解决了这个问题。

>>> 今日的签到口令:z23p <<<

英文原文:https://julien.danjou.info/finding-definitions-from-a-source-file-and-a-line-number-in-python/ 
译者:Nothing
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值