Python Import Hook

最近看到了__Haoyi Li__大神的macropy项目,整体实现的思路很有意思。hack进python的import过程, 然后用macros的代码展开ast模块parse过的AST。这里有一个基础问题就是pythonimport hook是什么样子的。

python 2

python2python3import hook上有一些的不同。这里先看python2的部分。

python2import hook的在pep 302中描述。存在两个import object的对象列表,sys.meta_path以及sys.path_hooks,满足一定的格式,每一次import文件(不一定是py,可能是其他的,比如是zip文件)的时候,都会先后询问sys.meta_path以及sys.path_hooks的文件是否能handle文件,直到找出一个能够handle的对象。对于那种不依赖sys.pathimport比如Built-in或者frozen的部分,就应该注册到sys.meta_path里面。

接下来的问题就是import object应该是长成什么样子的。import protocol中定义了两种对象finderloaderfinder中应该有一个find_module方法

1
finder.find_module(fullname, path=None)

其中fullname用来表示import的全称,meta_path中的对象,第二个参数会给None代表top-level的模块,或者package.__path__代表子模块的位置。如果找到了返回对应的loader对象,如果不能的话就返回None,而raise异常会导致import的中断。

然后loader对象应该有方法

1
loader.load_module(fullname)

这个函数应该返回module对象。

Python 3

python3之后importlib中提供了相应的脚手架。

importlib.abc中提供了相应的抽象的代码。

object
 +-- Finder (deprecated)
 |    +-- MetaPathFinder
 |    +-- PathEntryFinder
 +-- Loader
 +-- ResourceLoader --------+
  +-- InspectLoader          |
       +-- ExecutionLoader --+
                             +-- FileLoader
                             +-- SourceLoader

然后在cpython的实现中import过程中,给出了具体的实现。其中PathFinder等一些类都是可以复用的,因为这些不同,所以在hack宏的时候还要针对Python版本做区分做点事情。

一个完整的流程

要把自己的代码加到import的过程,就要重新实现一套object,然后把自己处理AST的部分插进流程里面就行。

根据之前的分析我们需要两个对象finderload(其实一个也无所谓,finder返回自己就好了)。

整体的入口应该在import的过程中,将我们的类加到sys.meta_path里面。MacroFinder_MacroLoader分别实现了finderloader

finder中根据packagepath找到code,然后主管parse这段代码,得到AST,之后的宏就可以在这里进行处理,然后构造新的module,在上下文中exec这个module

1
2
3
4
5
6
7
8
9
10
11
12
13
try:
source, path = self._get_source(fullname, path)
except:
"""pass if exception occur"""
return None


astree = ast.parse(source)
code = compile(astree, path, "exec")
# todo find macros and expand them
module = self._construct_module(fullname, path)
exec(code, module.__dict__)
return module.__loader__

其中_get_source_construct_module都可以利用现有的脚手架来实现。其中只不过需要根据不同的python版本来调整实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 def _get_source(self, module_name, package_path):
if six.PY2:
# python2中可以利用imp来实现
import imp
(file, pathname, desc) = imp.find_module(
module_name.split('.')[-1],
package_path
)

source_code = file.read()
file.close()
file_path = file.name
else:
# python3中就利用importlib.machinery的内容来实现
from importlib.machinery import PathFinder
loader = PathFinder.find_module(module_name, package_path)
source_code = loader.get_source(module_name)
file_path = loader.path

return source_code, file_path

在使用的时候只要在文件开始将

1
sys.meta_path.insert(0, MacroFinder())

之后就可以用了,为了证明效果,可以在获得AST的时候进行一些操作。不过由于这种实现机制,每次使用宏的时候都要在最上面执行这个命令。而且宏和使用宏的文件需要分开。