最近在开发ApiTestEngine时遇到一个安装包依赖的问题,耗费了不少时间寻找解决方案,考虑到还算比较有普遍性,因此总结形成这篇文章。

从 pip install 说起

先不那么简单地描述下背景。

ApiTestEngine作为一款接口测试工具,需要具有灵活的命令行调用方式,因此最好能在系统中进行安装并注册为一个CLI命令。

在Python中,安装依赖库的最佳方式是采用pip,例如安装Locust时,就可以采用如下命令搞定。

1
2
3
4
5
$ pip install locustio
Collecting locustio
  Using cached locustio-0.7.5.tar.gz
[...]
Successfully installed locustio-0.7.5

但要想采用pip install SomePackage的方式,前提是SomePackage已经托管在PyPI。关于PyPI,可以理解为Python语言的第三方库的仓库索引,当前绝大多数流行的Python第三方库都托管在PyPI上。

但是,这里存在一个问题。在PyPI当中,所有的包都是由其作者自行上传的。如果作者比较懒,那么可能托管在PyPI上的最新版本相较于最新代码就会比较滞后。

Locust就是一个典型的例子。从上面的安装过程可以看出,我们采用pip install locustio安装的Locust版本是v0.7.5,而在LocustGithub仓库中,v0.7.5已经是一年之前的版本了。也是因为这个原因,之前在我的博客里面介绍Locust图表展示功能后,已经有不下5个人向我咨询为啥他们看不到这个图表模块。这是因为Locust的图表模块是在今年(2017)年初时添加的功能,master分支的代码版本也已经升级到v0.8a2了,但PyPI上的版本却一直没有更新。

而要想使用到项目最新的功能,就只能采用源码进行安装。

大多数编程语言在使用源码进行安装时,都需要先将源码下载到本地,然后通过命令进行编译,例如Linux中常见的make && make install。对于Python项目来说,也可以采用类似的模式,先将项目clone到本地,然后进入到项目的根目录,执行python setup.py install

1
2
3
4
5
$ git clone https://github.com/locustio/locust.git
$ cd locust
$ python setup.py install
[...]
Finished processing dependencies for locustio==0.8a2

不过,要想采用这种方式进行安装也是有前提的,那就是项目必须已经实现了基于setuptools的安装方式,并在项目的根目录下存在setup.py

可以看出,这种安装方式还是比较繁琐的,需要好几步才能完成安装。而且,对于大多数使用者来说,他们并不需要阅读项目源码,因此clone操作也实属多余。

可喜的是,pip不仅支持安装PyPI上的包,也可以直接通过项目的git地址进行安装。还是以Locust项目为例,我们通过pip命令也可以实现一条命令安装Github项目源码。

1
2
3
4
$ pip install git+https://github.com/locustio/locust.git@master#egg=locustio
Collecting locustio from git+https://github.com/locustio/locust.git@master#egg=locustio
[...]
Successfully installed locustio-0.8a2

对于项目地址来说,完整的描述应该是:

1
pip install vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir

这里的vcs也不仅限于gitsvnhg也是一样的,而protocol除了采用SSH形式的项目地址,也可以采用HTTPS的地址,在此不再展开。

通过这种方式,我们就总是可以使用到项目的最新功能特性了。当然,前提条件也是一样的,需要项目中已经实现了setup.py

考虑到ApiTestEngine还处于频繁的新特性开发阶段,因此这种途径无疑是让用户安装使用最新代码的最佳方式。

问题缘由

ApiTestEngine中,存在测试结果报告展示这一部分的功能,而这部分的功能是需要依赖于另外一个托管在GitHub上的项目,PyUnitReport

于是,问题就变为:如何构造ApiTestEngine项目的setup.py,可以实现用户在安装ApiTestEngine时自动安装PyUnitReport依赖。

对于这个需求,已经确定可行的办法:先通过pip安装依赖的库(PyUnitReport),然后再安装当前项目(ApiTestEngine)。

1
2
$ pip install git+https://github.com/debugtalk/PyUnitReport.git#egg=PyUnitReport
$ pip install git+https://github.com/debugtalk/ApiTestEngine.git#egg=ApiTestEngine

这种方式虽然可行,但是需要执行两条命令,显然不是我们想要的效果。

经过搜索,发现针对该需求,可以在setuptools.setup()中通过install_requiresdependency_links这两个配置项组合实现。

具体地,配置方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
install_requires=[
   "requests",
   "flask",
   "PyYAML",
   "coveralls",
   "coverage",
   "PyUnitReport"
],
dependency_links=[
   "git+https://github.com/debugtalk/PyUnitReport.git#egg=PyUnitReport"
],

这里有一点需要格外注意,那就是指定的依赖包如果存在于PyPI,那么只需要在install_requires中指定包名和版本号即可(不指定版本号时,默认安装最新版本);而对于以仓库URL地址存在的依赖包,那么不仅需要在dependency_links中指定,同时也要在install_requires中指定。

然后,就可以直接通过ApiTestEngine项目的git地址一键进行安装了。

1
$ pip install git+https://github.com/debugtalk/ApiTestEngine.git#egg=ApiTestEngine

虽然在寻找解决办法的过程中,看到大家都在说dependency_links由于安全性的问题,即将被弃用,而且在setuptools的官方文章中的确也没有看到dependency_links的描述。

1
DEPRECATION: Dependency Links processing has been deprecated and will be removed in a future release.

不过在我本地的macOS系统上尝试发现,该种方式的确是可行的,因此就采用这种方式进行发布了。

但是当我后续在Linux服务器上安装时,却无法成功,总是在安装PyUnitReport依赖库的时候报错:

1
2
3
4
5
$ pip install git+https://github.com/debugtalk/ApiTestEngine.git#egg=ApiTestEngine
[...]
Collecting PyUnitReport (from ApiTestEngine)
  Could not find a version that satisfies the requirement PyUnitReport (from ApiTestEngine) (from versions: )
No matching distribution found for PyUnitReport (from ApiTestEngine)

另外,同时也有多个用户反馈了同样的问题,这才发现这种方式在LinuxWindows下是不行的。

然后,再次经过大量的搜索,却始终没有特别明确的答案,搞得我也在怀疑,dependency_links到底是不是真的已经弃用了,但是就算是弃用了,也应该有新的替代方案啊,但也并没有找到。

这个问题就这么放了差不多一个星期的样子。

解决方案

今天周末在家,想来想去,不解决始终不爽,虽然只是多执行一条命令的问题。

于是又是经过大量搜索,幸运的是终于从pypa/pipissues中找到一条issue,作者是Dominik Neise,他详细描述了他遇到的问题和尝试过的方法,看到他的描述我真是惊呆了,跟我的情况完全一模一样不说,连尝试的思路也完全一致。

然后,在下面的回复中,看到了Gary Wukbuilds的解答,总算是找到了问题的原因和解决方案。

问题在于,在dependency_links中指定仓库URL地址的时候,在指定egg信息时,pip还同时需要一个版本号(version number),并且以短横线-分隔,然后执行的时候再加上--process-dependency-links参数。

回到之前的dependency_links,我们应该写成如下形式。

1
2
3
dependency_links=[
   "git+https://github.com/debugtalk/PyUnitReport.git#egg=PyUnitReport-0"
]

在这里,短横线-后面我并没有填写PyUnitReport实际的版本号,因为经过尝试发现,这里填写任意数值都是成功的,因此我就填写为0了,省得后续在升级PyUnitReport以后还要来修改这个地方。

然后,就可以通过如下命令进行安装了。

1
$ pip install --process-dependency-links git+https://github.com/debugtalk/ApiTestEngine.git#egg=ApiTestEngine

至此,问题总算解决了。

后记

那么,dependency_links到底是不是要废弃了呢?

pipGitHub项目中看到这么一个issue--process-dependency-links之前废弃了一段时间,但是又给加回来了,因为当前还没有更好的可替代的方案。因此,在出现替代方案之前,dependency_links应该是最好的方式了吧。

最后再感叹下,老外提问时描述问题的专业性和细致程度真是令人佩服,大家可以再仔细看下这个issue好好感受下。

阅读更多