Pipenv 使用错误的 Python 版本创建虚拟环境

1,167 阅读5分钟

0 观前提示

❗❗❗多图预警❗❗❗

1 文章缘起

今天在使用 pipenv --three 创建虚拟环境时,出现了以下报错信息:

error

核心信息是:Using D:/Anaconda3/Scripts/python.exe (None) to create virtualenv...,原因一目了然,就是 pipenv 使用我已经卸载了的 Anaconda3Python 来创建虚拟环境。


tl;dr

排查结果发现,pipenv 使用的 pythonfinder 库在查找 Windows 环境下的 Python 版本时,会优先从注册表(HKEY_CURRENT_USER\Software\Python)中读取。而我之前卸载的 Anaconda3 还在注册表里残留了键值,这才导致了上面的报错信息。修复方法就是把注册表里遗留的键值删除。


2 具体排查

2.1 搜集信息

首先,我尝试使用 pipenv --python 3.8 来创建虚拟环境,发现这次是正常的。那么我就怀疑是不是 pipenv 有什么配置文件,记录了默认版本对应的路径,因为我是用 --three 参数进行创建的。

在经过一番搜索后发现,pipenv 是使用 Pipfile 和环境变量来进行配置的。但是我的虚拟环境还没创建,Pipfile 里是一片空白,而环境变量里也没有相关的变量值。

而后再经过一番搜索后,在 pipenv 的 issues 里发现可以使用 pipenv --support 对环境进行诊断,于是得到了下面的结果:

diagnostics

根据诊断结果,我发现 pipenv 查找到了以前安装的 Python,那么应该是 pipenv 在进行 Python 版本搜索时,使用了某个地方遗留的信息。

接下来的思路就是看看 pipenv 是如何进行版本搜索的。

2.2 浏览代码

第一阶段

打开 pipenv 的位置,也就是上图中的 Pipenv location,找到参数解析相关的代码,如下图。

pipenv/cli/command.py#L144

现在找到具体搜索相关的代码了,原来 pipenv 是使用了 pythonfinder 这个库来进行 Python 版本搜索的。

pipenv/help.py#L32

接着打开 pipenv/vendor/pythonfinder 目录,找到 Finder.find_all_python_version 的具体代码,发现有以下两处需要进行确认:

pipenv/vendor/pythonfinder/pythonfinder.py#L117

所以我打开了 ipython,使用 pdb 进行确认,具体如下图:

pdb-0

pdb-1

pdb-2

那么现在就是要找到 self.system_path.find_all_python_versions 的具体代码了。从上图可以看到,代码所在文件是 pythonfinder/models/path.py。所以现在就打开这个文件,找到相关代码:

pipenv/vendor/pythonfinder/models/path.py#L324

上图中的 sub_finderoperator.methodcaller,用处就是调用对象的指定方法,这里是调用指定对象的 find_all_python_versions 方法,文档如下:

operator.methodcaller

Ok,接下来就是找 self.windows_finder.find_all_python_versions 的定义了。往上翻了翻,发现 self.windows_finder 是在 pythonfinder/models/windows.py 中定义的 WindowsFinder,而 find_all_python_versions 的定义如下图:

pipenv/vendor/pythonfinder/models/windows.py#L18

由此可见,版本信息的来源就是 self.version_list 了。那么接下来就是查找这个值是如何来的。

第二阶段

这里之所以分了第二阶段,是因为我在查找 version_list 的初始化代码时陷入了“查找 version_list 的初始化代码是如何被调用” 的歧途,花费一定时间去阅读 attrs 这个库的文档了。结果看着看着我发现这根本没必要,我的目的是找到版本信息是如何得到的,而不是如何调用得来的。这就是走了弯路,所以说牢记目的才能保证不偏离路途。扯远了,继续排查吧。

version_list 的初始化代码如下:

pipenv/vendor/pythonfinder/models/windows.py#L95

由图中代码可知,version_list 中的元素来源于 pep514env.findall() 的执行结果。因此接下来就是跳转到 pythonfinder._vendor.pep514tools.environment.findall

pipenv/vendor/pythonfinder/_vendor/pep514tools/environment.py#L90

findall 的定义引导我找到了 _get_sources

pipenv/vendor/pythonfinder/_vendor/pep514tools/environment.py#L77

看到这,我基本知道 pythonfinder 是在哪找到的版本信息了,没错,就是从 Windows 的注册表里。

接下来看看 open_source 的定义:

pipenv/vendor/pythonfinder/_vendor/pep514tools/_registry.py#L193

可以看到,open_source 就是从 _REG_KEY_INFO 中获取指定键值,然后返回一个 RegistryAccessor 实例。那么让我们看看 _REG_KEY_INFORegistryAccessor 的定义:

pipenv/vendor/pythonfinder/_vendor/pep514tools/_registry.py#L21

pipenv/vendor/pythonfinder/_vendor/pep514tools/_registry.py#L99

回想 version_list 的初始化就是通过一个 for 循环来迭代 pep514env.findall 的结果完成的,而我在 RegistryAccessor.__iter__ 的定义中也看到了使用 winreg 库读取注册表的相关代码。那么接下来就是打开注册表,找到对应项并删除即可:

registry

(注:可能在 HKEY_LOCAL_MACHINE 里也存在相应的项)

至此,删除完注册表里 Anaconda 遗留的项后,再次使用 pipenv --support 进行诊断,可以看到找到的 Python 版本就是我现在所安装的,而且 pipenv --three 也可以正常运行了。

correct result

3 复盘总结

行文至此,问题已经解决,那么是时候进行复盘和总结了。这次排查问题的过程中,我发现主要存在以下两个问题:

  1. 在第一次搜索无果后,我已经可以直接从 --three 这个参数入手,看看 pipenv 是如何查找到错误的版本的。但是我还是进行了再一次搜索,直到看到了 --support 参数的运行结果后才想起来可以顺藤摸瓜找到具体逻辑。
  2. 指在排查过程中花费时间去查阅 attrs 这个库的文档,查找具体调用逻辑的这一步。这是因为在排查的过程中我忘了具体想要达到的目标。

无论如何,问题解决,可喜可贺。

P.S. 顺便附上一张图

route

4 题外话

在删除了注册表里残留的项之后,我在运行 pipenv --support 时发现,终端打印到 Python installations found: 这一行后会停顿一段时间,而后才继续输出结果。最终经过以下排查后发现,是在从环境变量里搜索 Python 的安装版本,所以才会有一段停顿。

pipenv/vendor/pythonfinder/models/path.py#L328

pipenv/vendor/pythonfinder/models/path.py#L277

pipenv/vendor/pythonfinder/models/path.py#L264

pipenv/vendor/pythonfinder/models/path.py#L233

pipenv/vendor/pythonfinder/models/path.py#L430

pipenv/help.py#L32