0 观前提示
❗❗❗多图预警❗❗❗
1 文章缘起
今天在使用 pipenv --three
创建虚拟环境时,出现了以下报错信息:

核心信息是:Using D:/Anaconda3/Scripts/python.exe (None) to create virtualenv...
,原因一目了然,就是 pipenv
使用我已经卸载了的 Anaconda3 的 Python 来创建虚拟环境。
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
对环境进行诊断,于是得到了下面的结果:

根据诊断结果,我发现 pipenv
查找到了以前安装的 Python,那么应该是 pipenv
在进行 Python 版本搜索时,使用了某个地方遗留的信息。
接下来的思路就是看看 pipenv
是如何进行版本搜索的。
2.2 浏览代码
第一阶段
打开 pipenv
的位置,也就是上图中的 Pipenv location,找到参数解析相关的代码,如下图。

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

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

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



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

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

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

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

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

findall
的定义引导我找到了 _get_sources
:

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

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


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

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

3 复盘总结
行文至此,问题已经解决,那么是时候进行复盘和总结了。这次排查问题的过程中,我发现主要存在以下两个问题:
- 在第一次搜索无果后,我已经可以直接从
--three
这个参数入手,看看pipenv
是如何查找到错误的版本的。但是我还是进行了再一次搜索,直到看到了--support
参数的运行结果后才想起来可以顺藤摸瓜找到具体逻辑。 - 指在排查过程中花费时间去查阅
attrs
这个库的文档,查找具体调用逻辑的这一步。这是因为在排查的过程中我忘了具体想要达到的目标。
无论如何,问题解决,可喜可贺。
P.S. 顺便附上一张图

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





