闲侃 sys.path 与 buildout 实践

这篇文章我们来讨论下 Python 中的 sys.path,以及其在 buildout 中的应用与实践,最后分享下在 neo/vim 中给 python-modecoc-python 添加自定义本地 Python 包路径的方法。

sys.path

Python 中的 sys.path 是一个字符串列表,用于指定 Python Runtime 的模块搜索路径。

>>> import sys
>>> sys.path
[
    '',
    '/usr/local/lib/python27.zip',
    '/usr/local/lib/python2.7',
    '/usr/local/lib/python2.7/plat-darwin',
    '/usr/local/lib/python2.7/plat-mac',
    '/usr/local/lib/python2.7/plat-mac/lib-scriptpackages',
    '/usr/local/lib/python2.7/lib-tk',
    '/usr/local/lib/python2.7/lib-old',
    '/usr/local/lib/python2.7/lib-dynload',
    '/Users/jiawei/.local/lib/python2.7/site-packages',
    '/usr/local/lib/python2.7/site-packages',
    '/usr/local/lib/python2.7/site-packages/install-1.3.3-py2.7.egg'
]

该列表由以下三个部分组成,在程序启动时,会从以下三个方面进行初始化:

python-path-img

1. 第一部分 sys.path[0] 是你当前调用 python 解释器的脚本的目录,也就是你当前的 Python 项目路径,如果是在 REPL 环境中,其为空字符串 ""

2. 第二部分是开发者可以修改控制的,通过几种方式读取开发者定义的模块路径,例如通过读取环境变量 PYTHONPATH 等,下面具体介绍。

3. 最后一部分是你 Python 安装相关的默认环境。

当我们在程序中导入 import m 时,可能会遇到 ImportError: No module named m 这种类似的包无法导入的问题时,原因在于它所在的路径不在 sys.path 里,下面我将列举向 sys.path 添加自定义模块路径几种方法。

1. 通过创建 .pth 文件,将目录列出来,例如:

/my/prodir1
/my/prodir2

但这种方式我们通常是不提倡的,关于更多如何使用 .pth 文件拓展 sys.path 的介绍可以阅读 site 模块的描述。

2. 通过设置环境变量 PYTHONPATH,写过 Go 的朋友应该都比较熟悉,类似于 Golang 中有 GOROOTGOPATH

export PYTHONPATH=/my/prodir1:/my/prodir2

现在再来看看我们的 sys.path

>>> import sys
>>> sys.path
[
    '',
    '/my/prodir1',
    '/my/prodir2',
    '/usr/local/lib/python27.zip',
    '/usr/local/lib/python2.7',
    '/usr/local/lib/python2.7/plat-darwin',
    '/usr/local/lib/python2.7/plat-mac',
    '/usr/local/lib/python2.7/plat-mac/lib-scriptpackages',
    '/usr/local/lib/python2.7/lib-tk',
    '/usr/local/lib/python2.7/lib-old',
    '/usr/local/lib/python2.7/lib-dynload',
    '/Users/jiawei/.local/lib/python2.7/site-packages',
    '/usr/local/lib/python2.7/site-packages',
    '/usr/local/lib/python2.7/site-packages/install-1.3.3-py2.7.egg'
]

3. 还有一种方式是写脚本来手动添加,在进入你程序的 work runtime 之前将路径插入到 sys.path中, 这种方式可以做到项目级,比较灵活:

import sys

sys.path.insert(0, '/my/prodir1')
sys.path.insert(0, '/my/prodir2')

当然,上面只是一个简单的示例,实践中需要尽量避免硬编码,导致添加错误的路径。

buildout

对于一个 Python 开发者来说,虚拟环境工具你一定不陌生,例如 virtualenvvirtualenvwrapperpipenv 等,它们通常提供了一个相对隔离的环境来存放和管理你需要的第三方包,并为你设置好 sys.path。不对,貌似有点跑题了,关于虚拟环境的工作原理我将在后面单独分享,下面请出本节的主角大哥大 buildout

Buildout 是一个 Python 项目的自动化构建打包工具,通过它我们可以为大型系统创建复杂但可重用的设置。其主要根据配置将项目的依赖以 egg 包的形式全部归在统一的一个 eggs 目录下,这样就不用依赖任何外部的资源,比虚拟环境来得更彻底。egg 包 类似于 Java 中的 jar 包,想了解更多介绍的同学可以阅读这篇文章

关于 buildout 的更多介绍和以及如何配置,官方文档上都有详细的介绍,而且因为本文的主题是 sys.path,所以这里就不赘述了。

这里我们主要关心它包的组织,eggs 会生成到你项目下的 buildout.cfg 配置中的 eggs-directory 目录下,而一般我们并不需要额外去配置,默认会生成在当前项目的根目录下,为你的项目使用。

当然 buildout 也支持在多个项目之间共享 eggs。这需要你新建 $HOME/.buildout/default.cfg 文件,设置用户级的默认配置:

[buildout]
eggs-directory = ~/.buildout/eggs
download-cache = ~/.buildout/downloads

除此之外,你还可以通过每个 parts 下的 extra-paths 配置你本地的包:

[buildout]
parts = app

[app]
recipe = zc.recipe.egg
interpreter = python
eggs = my-pro-name
extra-paths = ${buildout:directory}/my-extra-pkg

好,回到主题,既然是将依赖包打在项目里,所以要正确启动服务,buildout 肯定要将项目的所有依赖包 eggs 中的包路径以及 extra-paths 加入到 sys.path 中,它采用的方式是我们上面说的第三种,我们可以打开 bin 下生成的可执行文件一看便知:

#!/Path/to/your/local/python

import os, sys

base = os.path.dirname(os.path.abspath(os.path.realpath(__file__)))
base = os.path.dirname(base)

sys.path[0:0] = [
    base,
    '/path/to/local/pro/eggs/pkg1.egg',
    '/path/to/local/pro/eggs/pkg2.egg',
    #  ...
    '/path/to/local/pro/my-extra-pkg'
  ]
# ...

如果打包过程中,有一些包是你已经安装了,也就是在上面谈到的 sys.path第三部分的中的某个路径下,比如 /usr/local/lib/python2.7/site-packages, 那么在打包过程中就不会生成相应的 egg 包了,所以会在上述 sys.path 也会加上这个路径,因为 buildout 必须保证项目依赖包的完备性。

vim 插件支持

buildout 虽然很完美,但这也给 neo/vim 下的 Python 环境搭建带来了一点麻烦,据我所知目前的一些 Python 相关的插件并没有直接支持 buildout 的(PyCharm 是支持的,直接配置即可),所以并不能正确识别 buildout 的配置来加载我们项目的那些依赖包,在 rope init 或者 lint 时会报错找不到包,因此代码跳转补全等基础功能都会受到影响。所以我们必须要自己来处理这个问题,将上述依赖包加入到其运行时环境中。

当然你可以直接通过我们上述的几种方法来实现,但这不是我推荐的,因为配置环境变量等等都是全局的,幸好目前我用的几个插件都支持自定义包的拓展配置,比如 python-modecoc-python

1. 对于 pymode

pymode 提供了 g:pymode_paths 数组来存放额外的包路径,所以在你的 pymode 配置中加入如下配置即可:

if $BUILDOUT_EGGS_PATH != ''
    let g:pymode_paths = reverse(split(globpath($BUILDOUT_EGGS_PATH, '*'), '\n'))
else
    let g:pymode_paths = reverse(split(globpath(getcwd().'/eggs', '*'), '\n'))
endif

在上面的配置中,如果你的 buildout 使用了多个项目共享 eggs 模式的话,为 BUILDOUT_EGGS_PATH 配置共享 eggs 的路径即可:

export BUILDOUT_EGGS_PATH=/path/to/buildout/eggs/path

而如果并不是共享 eggs 的模式,那么项目的包都会在当前目录下的 eggs 中,就不用任何配置,配置会走到 else 逻辑自动获取包路径。

对于上述配置在 extra-paths 下的包可以:

let g:pymode_paths = g:pymode_paths + reverse(split(getcwd().'/my-extra-pkg', '\n')

2. 对于 coc-python

同样的,coc-python 也提供了相应的配置:

python.autoComplete.extraPaths: List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list., default: []

所以我们只需在 coc-settings.json 中加入相应配置即可,我们依旧使用上面的例子:

{
    // ...
    "python.autoComplete.extraPaths":[
    "/path/to/local/pro/eggs/pkg1.egg",
    "/path/to/local/pro/eggs/pkg2.egg",
    "my-extra-pkg"
    ]
    // ...
}

加入配置之后可以在 neo/vim COMMAND 模式下使用 CocInfo 命令来查看 path 的查找详情。


感兴趣的同学可以查看我的配置,同时也欢迎大家评论留言,与我交流。