标签归档:Pipenv

安装 Python 依赖出现 MarkupSafe ImportError … Feature 报错的解决方法

这个报错在 3 月 8 号 setuptools 发布新版本之后出现,通常会在安装 Python 依赖时触发。

报错信息

使用 pip 安装依赖时的报错如下:

Collecting markupsafe==1.0
  Downloading https://.../MarkupSafe-1.0.tar.gz (14 kB)
    ERROR: Command errored out with exit status 1:
     command: '...\python.exe' -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'...\\pip-install-bsormril\\markupsafe\\setup.py'"'"'; __file__='"'"'...\\pip-install-bsormril\\markupsafe\\setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' egg_info --egg-base '...\pip-install-bsormril\markupsafe\pip-egg-info'
         cwd: ...\pip-install-bsormril\markupsafe\
    Complete output (5 lines):
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "...\pip-install-bsormril\markupsafe\setup.py", line 6, in <module>
        from setuptools import setup, Extension, Feature
    ImportError: cannot import name 'Feature'
    ----------------------------------------
ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.

使用 Pipenv 安装依赖时的报错如下:

An error occurred while installing markupsafe==1.0 --hash=sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665! Will try again.
Installing initially failed dependencies…
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 1874, in do_install
[pipenv.exceptions.InstallError]:       keep_outdated=keep_outdated
[pipenv.exceptions.InstallError]:   File "...Python\Python36\site-packages\pipenv\core.py", line 1253, in do_init
[pipenv.exceptions.InstallError]:       pypi_mirror=pypi_mirror,
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 859, in do_install_dependencies
[pipenv.exceptions.InstallError]:       retry_list, procs, failed_deps_queue, requirements_dir, **install_kwargs
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 763, in batch_install
[pipenv.exceptions.InstallError]:       _cleanup_procs(procs, not blocking, failed_deps_queue, retry=retry)
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 681, in _cleanup_procs
[pipenv.exceptions.InstallError]:       raise exceptions.InstallError(c.dep.name, extra=err_lines)
[pipenv.exceptions.InstallError]: ['Looking in indexes: https://.../pypi/simple', 'Collecting markupsafe==1.0', '  Using cached https://.../MarkupSafe-1.0.tar.gz (14 kB)']
[pipenv.exceptions.InstallError]: ['ERROR: Command errored out with exit status 1:', '
command: \'...\\.virtualenvs\\helloflask-evdb6idn\\scripts\\python.exe\' -c \'import sys, setuptools, tokenize; sys.argv[0] = \'"\'"\'...\pip-install-pkgojp4t\\\\markupsafe\\\\setup.py\'"\'"\'; 
__file__=\'"\'"\'...\\\\pip-install-pkgojp4t\\\\markupsafe\\\\setup.py\'"\'"\';f=getattr(tokenize, \'"\'"\'open\'"\'"\', open)(__file__);code=f.read().replace(\'"\'"\'\\r\\n\'"\'"\', \'"\'"\'\\n\'"\'"\');f.close();exec(compile(code, __file__, \'"\'"\'exec\'"\'"\'))\' egg_info --egg-base \'...\\pip-install-pkgojp4t\\markupsafe\\pip-egg-info\'', '
cwd: ...\\pip-install-pkgojp4t\\markupsafe\\', '    Complete output (5 lines):', '    
Traceback (most recent call last):', '      
File "<string>", line 1, in <module>', ' 
File "...\\pip-install-pkgojp4t\\markupsafe\\setup.py", line 6, in <module>', '
from setuptools import setup, Extension, Feature', "
ImportError: cannot import name 'Feature'", '----------------------------------------',
'ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.']
ERROR: ERROR: Package installation failed...

其他工具输出类似,主要异常信息是 MarkupSafe setup.py: ImportError: cannot import name Feature。通常会在安装 Flask 项目的依赖时发生,因为 MarkupSafe 是 Flask 的依赖之一。

原因和解决方法

出现这个报错的原因是因为 Python 打包工具 setuptools 在 46.0.0 版本删掉了弃用的 Feature,而 MarkupSafe 刚好在 setup.py 文件里导入了这个类,所以会出现报错。

解决方法很多,最直接的是更新 MarkupSafe 到最新版本(1.1.1),新版本去掉了对 Feature 类的导入。如果使用 requirements.txt 存储依赖列表,那就把 MarkupSafe 的版本号改成 1.1.1(找到 MarkupSafe 开头那一行,替换版本号):

MarkupSafe==1.1.1

然后重新执行:

$ pip install -r requirements.txt

对于 Pipenv,可以直接执行:

$ pipenv install markupsafe==1.1.1

如果你是《Flask Web 开发实战》的读者,正在为第一部分的示例程序安装依赖,那你还需要执行下面的命令固定 sendgrid-python 的版本(它在新版本添加了不向后兼容的 API 变动):

$ pipenv install sendgrid==5.3.0

《Flask Web 开发实战》读者备注

如果你在 2020 年 3 月 8 号到 4 月 5 号之间买了《Flask Web 开发实战》,把示例程序克隆到了本地,然后尝试运行 pipenv install 或 pip install -r requirements.txt 命令来安装依赖,那很大概率你会遇到这个问题。

除了使用上面的方法之外,你还可以通过更新本地代码来解决。我最近给所有示例程序的依赖文件做了一次更新,除了书里涉及的 API 产生变动的依赖,其他依赖都已经更新到最新版本。

你可以使用下面的命令来更新你在本地的程序仓库(注意这会重置你对源码进行的修改):

$ git fetch --all
$ git fetch --tags
$ git reset --hard origin/master

然后重新执行一次 pipenv install 或 pip install -r requirements.txt 即可解决这个问题。如果遇到其他虚拟环境和依赖安装的问题可以参考这篇文章解决。

相关链接:

《Flask Web 开发实战》虚拟环境/依赖/Pipenv 等问题解决方法

注:这篇文章的主要受众是《Flask Web 开发实战》的读者。

注2:文中的 $ 符号标识一条命令行命令的开始,$ 前面是当前工作目录,# 号后面是注释。你实际只需要输入 $ 符号和 # 号之间的内容,不包括开头和结尾的空格。

在群聊和论坛里总是看到和虚拟环境和依赖安装相关的各类问题,这篇文章希望能够提供一个统一的解决方案。下次如果遇到有人问虚拟环境/Pipenv/依赖安装相关的问题,请把这篇文章的链接丢过去。

安装 Python 库非常慢?

在进入正题之前,你需要先解决基础设施问题。你在执行 pip install 命令或 pipenv install 等命令时会不会网速非常慢?20k/s 或者干脆看到 Time out,Connection reset 之类的报错,这种情况下,你需要设置 PyPI 镜像。具体操作可以在这篇《从国内的 PyPI 镜像(源)安装 Python 包》看到。

要不要继续使用 Pipenv?

因为书里面在一开始介绍了使用 Pipenv 管理依赖和虚拟环境,同时所有的安装第三方库的命令也都是使用 Pipenv,所以我们要解决的第一个问题是「要不要继续使用 Pipenv?」

我的建议是,如果你在使用的过程中没有遇到过任何报错,那么就继续使用它。直到你觉得它在某些地方不再让你满意。

但是如果你在使用的过程中遇到了问题(首先确保你使用的是最新版本的 Pipenv),比如:

  • 锁定依赖很慢,停留在「locking…」这样的提示不动
  • 执行正确的命令但是总是出现报错

附注 如果你安装依赖时的报错是「MarkupSafe setup.py: ImportError: cannot import name Feature」,请参考这篇文章解决。

那么就继续看下去。

如果不用 Pipenv,我该怎么办?

解决方法和替代工具非常多,这里给出两个。

方法一:不用虚拟环境

最简单的解决方法就是不用虚拟环境。如果你是一个初学者,那么不用虚拟环境完全没问题。现在你把所有的 Python 包全都安装在一个盒子里,你只需要会使用 pip 安装依赖,也就是使用下面的 pip install 命令:

$ pip install flask

附注 顺便说一句,不用虚拟环境时,如果你是使用 Linux 和 macOS 系统的 Python 3 用户,那么执行 Python 和 pip 相关命令的时候需要输入的是 python3 和 pip3,比如:pip3 install flask。后面不再提示。

这个命令会为你安装 Flask。每到需要安装一个包,你就执行这个命令,把上面的 flask 替换成你要安装的包名即可。类似的,书中所有 pipenv install xxx 形式的命令也都替换为 pip install xxx。

这时你可以跳过 Pipenv 和虚拟环境相关的内容。在第一章,当你把当前工作目录切换到 helloflask 文件夹内之后,整个 1.1 小节你只需要执行下面这行命令:

$ pip install -r requirements.txt

而第二部分每章开头的下面这两行命令:

$ pipenv install --dev
$ pipenv shell

都要替换为:

$ pip install -r requirements.txt

这个命令会安装对应项目的所有依赖,所以后续介绍各个 Python 库时的安装命令不需要再执行。

方法二:使用 virtualenv/venv 来管理虚拟环境,搭配 pip 来管理依赖

第二种方法是改用更基础的工具:你仍然使用 pip 安装依赖,同时搭配 virtualenv 或 venv 来管理虚拟环境。如果你选择这个方法,那就跳过书中对 Pipenv 的介绍,改为阅读这篇《要不我们还是用回 virtualenv/venv 和 pip 吧》。阅读完上述文章后,再继续阅读。

现在你可以跳过 Pipenv 相关的内容。第一章切换进 helloflask 目录后,整个 1.1 小节你只需要执行下面的命令:

$ python -m venv env  # Linux、macOS 系统的 Python3 用户,使用 python3 -m venv env
$ env\Scripts\activate  # Linux、macOS 系统使用 source env/bin/activate
$ pip install -r requirements.txt  # 这个命令会安装对应项目的所有依赖

附注 :上面命令里 # 号及之后的文字是注释,不需要输入。如果你使用 Python2,第一条命令需要改为 virtualenv env。这三行命令的作用依次为:创建虚拟环境、激活虚拟环境、从 requirements.txt 文件安装依赖列表。

类似的,第二部分每章开头的下面这两行命令:

$ pipenv install --dev
$ pipenv shell

都要替换为:

$ python -m venv env  # Linux、macOS 系统的 Python3 用户,使用 python3 -m venv env
$ env\Scripts\activate  # Linux、macOS 系统使用 source env/bin/activate
$ pip install -r requirements.txt  # 这个命令会安装对应项目的所有依赖

同时书中所有 pipenv install xxx 形式的命令都替换为 pip install xxx(不过你并不需要一个一个执行,因为每章开头执行 pip install -r requirements.txt 时会安装所有项目相关的依赖)。

对于 PyCharm 设置 Python 解释器部分的内容,大致可以沿用,只不过在图 1-4 的位置你需要从列表里选择当前目录 env 文件夹(虚拟环境文件夹)中的 Python 解释器,根据操作系统的不同,将会是 env/bin/python 或 env\Scripts\python.exe。

退出虚拟环境时,使用下面的命令:

$ deactivate

使用书中同样的代码,但是却出现报错?/ 我该怎么安装依赖?

有一些 Python 库在版本变化时会带来 API 的变化,而书中示例程序代码是基于每章开头注明的 Python 库来开发的,所以在更新依赖版本的时候可能会导致示例程序的代码出错。我建议你按照书中的命令来安装依赖,这会从项目依赖文件里安装固定版本的依赖列表,不要跳过第二部分每章开头的命令。

具体来说,如果使用 Pipenv,对应的安装依赖的命令是:

$ pipenv install --dev

如果使用 pip,对应的命令则是(如果创建了虚拟环境,需要先激活虚拟环境):

$ pip install -r requirements.txt

如果是第一章的示例程序,那么在 helloflask 目录下执行一次即可。如果是第二章的四个程序,那么在每一个程序的根目录执行一次即可。后续所有介绍新的 Python 包时给出的安装命令不用再执行。

关于第一部分示例程序的项目结构和启动问题

在第一部分的源码中,一共有 6 个 Flask 程序,分别保存在 helloflask/demos/ 目录下的六个子文件夹内。用来存储环境变量的 .env 和 .flaskenv 文件需要在这些子文件夹内创建,而不是放到顶层目录(helloflask/)。同时为了方便操作,这 6 个程序共用同一个虚拟环境,所以在 helloflask/ 目录下创建虚拟环境。

注意 不要在 helloflask/ 目录下创建 .env 和 .flaskenv 文件,这会导致子目录下的程序无法正确启动(issue #3561)。

如果你按照书里给出的提示执行命令,一般不会出现问题。但是为了防止各种意外情况,这里列一下本书第一部分的操作流程($ 符号前是当前工作目录)。

具体的操作顺序就是,克隆仓库以后,切换到 helloflask 目录:

$ cd helloflask

然后创建虚拟环境、激活虚拟环境、安装依赖:

/helloflask $ python -m venv env  # Linux、macOS 系统的 Python3 用户,使用 python3 -m venv env
/helloflask $ env\Scripts\activate  # Linux、macOS 系统使用 source env/bin/activate
/helloflask $ pip install -r requirements.txt  # 这个命令会安装对应项目的所有依赖

接着看到介绍 Flask 最小程序部分,启动第一部分的示例程序:

helloflask/ $ cd demos/hello
helloflask/demos/hello $ flask run

现在看到了第二章,启动第二部分的示例程序(先执行 cd .. 回到上层目录,即 demos/ 目录):

helloflask/demos/hello $ cd ..
helloflask/demos $ cd http
helloflask/demos/http $ flask run

看到第三章,启动第三部分的示例程序:

helloflask/demos/http $ cd ..
helloflask/demos $ cd template
helloflask/demos/template $ flask run

以此类推。

遇到了自己没法解决的问题怎么办?

如果遇到问题,你可以先尝试:

  • 看一下本书 GitHub 仓库里的 FAQ 页面 有没有你要找的答案
  • 在搜索引擎搜索你的错误信息关键字,尝试自己解决

自己无法解决的话,可以:

  • 发到 GitHub(issue)和论坛(用文字形式贴出完整的错误信息、相关代码和命令,尽可能的详细描述相关信息)。
  • 发到交流群(建议优先选择发到 GitHub 和论坛,我会定期回复,群聊沟通效率很低,更适合闲聊)

回应《也谈「不要用 Pipenv」》

看了董伟明老师(@董伟明)的《也谈「不要用 Pipenv」》,这篇文章对其中的一些观点做出一些回应和解释。

也看了 Frost Ming 老师(@岂不美哉)的《Pipenv 有什么问题》,很感谢他做出的努力,祝 Pipenv 早日脱离 Kenneth Retiz 的影响,越来越好。

(Kenneth Retiz 下文简称 KR)

KR 有没有借 PyPA 之名来做背书?

所以作者并没有想着用来背书。

我仍然认为 KR 有利用 PyPA 做背书,甚至在误导别人 Pipenv 是 Python 官方(混淆 PyPA 和 Python 官方的概念)推荐的工具。

证据 1

2017 年 8 月 29 号,在 Pipenv 还不够成熟的时候,PyPA 成员 Thea Flowers 创建了一个 PR 要把 Pipenv 添加到 PyPA 的打包教程里,介绍使用 Pipenv 安装和管理依赖(注意这时候 Pipenv 根据教程的内容在 Windows 上是没法正常使用的,具体见 9 月 2 号的这个 issue)。

2017 年 8 月 31 号,Thea Flowers 自己合并了 PR。注意这个教程页面是临时的单独页面,还没有正式放到打包教程的页面里。

2017 年 9 月 1 号,KR 在 Pipenv 的 README 里加了这样一行介绍语:「Pipenv — the officially recommended Python packaging tool from Python.org, free (as in freedom).」(commit

其中的关键内容翻译过来大概是「官方推荐的 Python 打包工具,来自 Python.org」。

仅仅因为在 PyPA 的打包文档里加入了一个短教程(其中介绍了使用 Pipenv 安装和管理依赖),然后 KR 就在 Pipenv 的介绍里宣传这是「官方推荐」,而注明的官方来源则是「Python.org」,这两个关键词背后的超链接都是 PyPA 打包文档的 Pipenv 介绍。packaging.python.org(PyPA) 和 python.org(Python 官方) 的区别很大,很明显,他清楚两者的区别,但又故意没有表达清楚。
退一步讲,不管他的意图是什么,这样的措辞都会让人以为是 Python 官方推荐的打包工具,尤其是对 PyPA 这个组织不了解的人,看到 Python.org 都会认为是 Python 官方。

证据 2

在 PyCon 2018(五月),KR 在演讲《Pipenv: The Future of Python Dependency Management》里介绍 Pipenv 的卖点的时候,列在第一条的仍然是上面那一句「Officially recommended tool from python.org」:

和在 README 里不同的是,这次在演讲上别人没法去点那个 python.org 的链接去甄别究竟是 python.org(Python 官方) 还是 packaging.python.org(PyPA)。而 KR 在介绍这里的时候没有任何说明,直接说是「来自 Python.org 的官方推荐」。

至于 KR 和 PyPA 或者说和 Thea Flowers 有什么关系?把 Pipenv 的介绍加到打包文档是 Thea Flowers 的个人意愿?还是 PyPA 的 35 个成员全部同意的结果?是什么促使 PyPA 在 Pipenv 还不成熟、甚至教程里的内容没法在 Windows 上正常操作的情况下添加到打包教程里?这些问题我暂时找不到答案。

(注:PyPA 指的是 Python Packaging Authority,一个负责维护 Python 打包相关的库(比如 pip、virtualenv 等)和文档的组织。)

从 0 升到 18 的措辞问题

而所谓的18.X.X是 calver versioning(基于日历的版本)

在上一篇文章里,我引用了一段 HN 上的评论来概括 Pipenv 在推广方式上的问题:

Kenneth Retiz 滥用他在 PyPA 的位置(而且快速把一个实际上是 beta 状态的产品的版本号从 0 升到 18)来暗示 Pipenv 已经非常稳定,受到大力支持并且非常官方,但事实却并不是这样。

这句话的英文原文是:

However, Kenneth abused his position with PyPA (and quickly bumped a what is a beta product to version 18) to imply Pipenv was more stable, more supported and more official than it really was.

其中关于版本的部分是「and quickly bumped a what is a beta product to version 18」,可能是我乱翻译造成了误解……我认为原文里的 18 就是一个夸张的表达方式,把这里的数字换成 100 也可以表达同样的意思(也可能是指 0.3.0 跳到 3.0.1 那次)。换用日期版本号(CalVer)那次看起来没什么问题(11.10.4 -> 2018.05.12),所以我认为这里的 18 和日期版本号没关系。

Lockfile 只要过期就重新生成是合理的吗?

Kenneth Reitz 先是说 lockfile 只要是过期了就总是会被重新生成

这是对的,Pipfile和Pipfile.lock是对应的,当执行pipenv install后改了Pipfile,对应的Pipfile.lock就定会改。错误的是,不应该改那些不相关包的版本: 既然已经是==的了,就表明确定了具体版本呀。

这些问题,其实源于 Pipfile对应依赖在一开始没指定具体版本,也就是Pipfile对标requirements.txt,而Pipfile.lock只是当前环境的一个「快照」,如果Pipfile没有明确版本就用Pipfile.lock里面指定的。

我的主要想法是这样的功能实现是不合理的。Pipenv 在安装一个包的时候默认就使用通配符(*)版本写到 Pipfile 是不合理的设计。这样的设计不符合正常的开发流程和使用习惯。如果我在安装一个包的时候就要明确自己要安装哪个版本,以便在 Pipfile 里固定版本,这样会很不方便,而且让 Pipfile.lock 的存在意义变得很弱。

按照 KR 自己的解释,Pipfile 对标的是 requirements.in,Pipfile.lock 对标的是 requirements.txt:

blank
按照大部分人的理解,Pipfile 是所有不固定版本的高层依赖的列表(unpinned),而 Pipfile.lock 是固定安装时采用版本的详细依赖列表(pinned),用来复现程序具体的依赖环境;除非我主动执行 update 命令更新某个依赖,否则 Pipfile.lock 不应该被改动。但实际的 Pipenv 并不是这样,更新 Pipfile.lock 变成了频繁发生(install/uninstall/update)的默认行为。

Poetry 分析依赖慢

Resolving dependencies… (422.9s)

安装个包7分钟,这… 谁能忍?你们试试把bluelog项目的依赖用poetry add加一遍需要多久?我反正体验不下去了

实际测试安装 Flask-SQLAlchemy,解析依赖花了 42 秒(Windows+代理),没有 7 分钟那么夸张。因为解析依赖的结果会被缓存,我就在另一台 Mac(代理)上也试了一遍,结果只花了 8.3 秒。可能是网络状况的问题?

另外我试了把 Bluelog 的所有依赖一次性安装(Windows+代理),其中解析依赖只花了 16.8 秒(因为解析结果缓存的原因,实际也许会稍久一点):

$ poetry add flask flask-ckeditor flask-mail flask-sqlalchemy flask-wtf flask-moment python-dotenv bootstrap-flask flask-login flask-debu gtoolbar gunicorn psycopg2 flask-migrate
Using version ^1.1 for flask
Using version ^0.4.3 for flask-ckeditor
Using version ^0.9.1 for flask-mail
Using version ^2.4 for flask-sqlalchemy
Using version ^0.14.2 for flask-wtf
Using version ^0.9.0 for flask-moment
Using version ^0.10.3 for python-dotenv
Using version ^1.0 for bootstrap-flask
Using version ^0.4.1 for flask-login
Using version ^0.10.1 for flask-debugtoolbar
Using version ^19.9 for gunicorn
Using version ^2.8 for psycopg2
Using version ^2.5 for flask-migrate

Updating dependencies
Resolving dependencies... (16.8s)

所以 Poetry 或许没那么糟糕(当然我还没深入使用过)。

星星

虽然不是决定性的,但是对于这Star不到6K的项目来说我是不敢用的

Pipenv 的 Star 的确很多(而且 README、文档甚至代码里到处都是星星✨),KR 可是个营销专家,但项目质量却并没有那么好。反正我现在一看见星星和蛋糕就有点头疼。

✨🍰✨

另外,顺便说一句,pip(5627)、virtualenv(3189)和 setuptools(883) 的 Star 数量都没到六千……

好吧,我承认最后这两段是在抬杠 :D

不要用 Pipenv

注意:本文写于 2019 年 8 月,其中描述的内容在新版本的 Pipenv 中或已得到修复或改进,请谨慎参考。

如果你是《Flask Web 开发实战》的读者,请访问《Flask Web 开发实战》虚拟环境/依赖/Pipenv 等问题解决方法


Pipenv 让我用的很痛苦,有一种被欺骗的感觉,而且很后悔在《Flask Web 开发实战》里采用它。

大部分情况下,它很好用,但却存在太多问题,有一些问题让人简直没法接受。我知道有人会说「这是开源程序,有 bug 就自己去修」、「爱用不用,没人强迫你」,但问题是,一个进行大肆推广,甚至借 PyPA 做背书来宣传(经常让人误以为是 Python 官方推荐)的工具却连基本的使用流程都没做好,这不是合理和正常的行为。 引用这个 HN 评论的话说就是:

Kenneth Retiz 滥用他在 PyPA 的位置(而且快速把一个实际上是 beta 状态的产品的版本号从 0 升到 18)来暗示 Pipenv 已经非常稳定,受到大力支持并且非常官方,但事实却并不是这样。

在这篇(劝退)文章里,我会分别从包的安装、更新、卸载来测试并指出 Pipenv 的一些问题。

测试准备

  • 项目:Bluelog(一个 Flask 博客)
  • Pipenv 版本:2018.11.26(最新版本)
  • 操作系统:Windows 10,Cmder
  • 测试流程:每一次测试命令前都会删除已经创建的虚拟环境(pipenv –rm),重置 Pipfile 和 Pipfile.lock 文件变动,然后重新创建虚拟环境(pipenv install)。

安装包

假设想给这个旧项目 Bluelog 添加新功能,拿到旧项目的代码,打算安装一个 Flask-Avatars 包。查了文档,发现安装包要使用 pipenv install 命令,所以执行了下面的命令:

$ pipenv install flask-avatars

结果发现其他所有的不相干依赖都被更新了……

WTF,这不是反人类吗(说好的「Python Development Workflow for Humans.」呢)?我安装一个包,默认行为竟然是更新其他所有不相干且已经锁定版本的依赖!

翻了文档才发现,要加一个 –keep-outdated 选项才能避免更新其他锁定的依赖:

$ pipenv install --help
...
--keep-outdated Keep out-dated dependencies from being updated in
Pipfile.lock. [env var: PIPENV_KEEP_OUTDATED]

好吧,那先忍着,多打一个命令行选项就是了:

$ pipenv install --keep-outdated flask-avatars

WTF,为什么所有依赖还是被更新了?

好吧,有 bug 很正常,我来提个 issue 吧,哎,好像有很多 issue 了?

重点评论:

Kenneth Reitz 先是说 lockfile 只要是过期了就总是会被重新生成(这是什么逻辑?),接着又说用 pipenv update depname,但其他人都回复不起作用(我下面会进行单独测试)。

接着,看到其他评论提到用 –selective-upgrade 选项:

$ pipenv install --help
...
--selective-upgrade Update specified packages.

我又继续使用 –selective-upgrade 选项:

$ pipenv install --selective-upgrade flask-avatars

仍然会更新所有依赖……

对了,顺便还测试了这个命令,依然没用:

$ pipenv install --keep-outdated --selective-upgrade flask-avatars

除了安装某个包会导致所有依赖版本被更新,Pipenv 在解决依赖的冲突上面也有一些不足,比如执行下面的安装命令(具体见 Poetry README):

$ pipenv install oslo.utils==1.4.0

会提示无法安装成功:

ERROR: ERROR: Could not find a version that matches pbr!=0.7,!=2.1.0,<1.0,>=0.6,>=2.0.0

更新包

假设我想更新 Bluelog 这个项目用的 Flask 版本(从 1.0.2 更新到最新的 1.1.1)。查了文档,找到了 update 命令(https://docs.pipenv.org/en/latest/basics/#example-pipenv-upgrade-workflow),于是我执行下面的命令:

$ pipenv update flask
Locking [dev-packages] dependencies…
Success!
Locking [packages] dependencies…
Success!
Updated Pipfile.lock (fd55e3)!
Installing dependencies from Pipfile.lock (fd55e3)…
================================ 26/26 - 00:00:12
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
All dependencies are now up-to-date!

突然看到最后一行赫然写着「All dependencies are now up-to-date!」,我以为是搞错了,赶紧看了下 Pipfile.lock,WTF,为什么我所有的依赖(包括和 Flask 完全不相关的)又都被更新了?
依然,已经有很多相关 issue:

重点评论:

如果这个 issue 没有被锁定,这一句「I have no idea.」下面的图标不知道还会被点多少次。我猜 Kenneth Reitz 对这个 issue 让多少人头疼也没有 idea。

继续搜索,查文档,发现 update 命令也有 –keep-outdated 和 –selective-upgrade 两个选项:

$ pipenv update --help 
... 
--selective-upgrade Update specified packages.
--keep-outdated Keep out-dated dependencies from being updated in
Pipfile.lock. [env var: PIPENV_KEEP_OUTDATED]

先来试下 –keep-outdated:

$ pipenv update --keep-outdated flask

no luck,还是更新了所有依赖。继续试一下 –selective-upgrade:

$ pipenv update --selective-upgrade flask

依然没用,仍然会更新所有依赖……

继续查 issue,发现下面这些:

在 #3461 里发现了下面这个评论:

(因为 Frost Ming 是国内的同学,也是核心维护者,说明一下,这里无意冒犯,引用这个评论只是想说明 Pipenv 现在的开发状态。)

这段评论的重点是「In fact, the package name passed as argument is not used at all.」。

也就是说,pipenv update 实际上是不接受包名称参数的。这在下面这个评论也得到了印证:

Here is the important caveat:pipenv updatealwaystargets every package in your lockfile, without exception. It does not accept arguments.

一个还没实现的功能就写到文档里了?这真的不是开玩笑吗?不仅是写到了文档里,还写到了命令行帮助文档里:

$ pipenv update --help
Usage: pipenv update [OPTIONS] [PACKAGES]...

类似下面的场景:

  • 用户:怎么运行程序呢?好,查下文档,文档里说「执行 run 命令就可以运行程序」。哎?怎么没用?
  • 开发者:哦,这个功能还没实现,先写出来让你练练手。

最终的结果就是,如果你想更新一个包,那就只能手动把更新版本的包版本和 hash 编辑到 Pipfile.lock 里。这么做实在是太蠢了。

卸载包

假设我决定不再使用 Gunicorn,需要卸载它,在文档里查到 pipenv uninstall 命令https://docs.pipenv.org/en/latest/basics/#pipenv-uninstall),于是执行下面的命令:

$ pipenv uninstall gunicorn

结果呢?为什么我所有的依赖又都被更新了!?好,我已经习惯了。看命令行帮助文档,同样有 –keep-outdated 命令:

$ pipenv uninstall --help
...
--keep-outdated Keep out-dated dependencies from being updated in
Pipfile.lock. [env var: PIPENV_KEEP_OUTDATED] 

再试一下,虽然我已经不抱期待了:

$ pipenv uninstall --keep-outdated gunicorn

顺便说一句,卸载的另一个问题是,当你卸载一个包的时候,只会卸载这个包本身,而这个包引入的相关依赖都会被保留,需要手动使用 clean 或 sync 命令修正(参考 Poetry README)。

不要使用 Pipenv(至少是现在)

当然,Pipenv 一直在改进。比如 Windows 支持,Lock 很慢的问题,都有过很多的优化(暂且不提没优化之前的痛苦经历)。

但是,种种证据都在表明,这其实是一个半成品。承诺了很多,兑现的却很少。或许过一段时间等它真正成熟了,能够保证基本使用流程,并且可以修改哪些反人类的设定以后再考虑用它(我怀疑这一条是否能实现,除非完全「去 Kenneth Reitz 化」,并且有一个核心维护者能够来推动执行)。

现在,请不要使用它。

我很抱歉在《Flask Web 开发实战》以及文章《Pipenv:新一代Python项目环境与依赖管理工具》中,推动更多人用它,给大家带来潜在的麻烦。我计划了一些补救措施,会逐一执行:

  • 写这篇文章【DONE】
  • 给《Flask Web 开发实战》的五个实例项目追加 requirements.txt 文件,并在 README 中添加说明。
  • 如果《Flask Web 开发实战》能出第二版,修改所有 Python 包安装命令,去掉所有 Pipenv 相关介绍。
  • 写文章介绍替代的几种工具和用法,包括原有的 virtualenv+pip、virtualenvwrapper、Poetry 等。
  • 在 PyCon China 2019 的闪电演讲《Python 虚拟环境和依赖管理工具大乱斗》里提及这个信息。
  • 以后写推荐文章时对这种带着强烈个人风格的项目保持警惕,并对 Kenneth Reitz 这个名字相关的东西保持警惕。

你可以选择用回 virtualenv+pip(+virtualenvwrapper),或是尝试新工具,我会在下一篇文章介绍主要替代品 Poetry 的基本用法。

相关文章

(1)

从国内的 PyPI 镜像(源)安装 Python 包

不论是使用 pip,还是 Pipenv、Poetry等工具,安装 Python 包的时候会默认从官方的 PyPI 源下载文件,速度比较慢。国内的一些公司和机构提供了 PyPI 镜像源(mirror source),你可以设置让这些工具从国内的镜像源安装 Python 包,以便提高下载速度。

官方 PyPI 源的 URL 为 https://pypi.org/simple (旧的 URL 为 https://pypi.python.org/simple ),下面我们将以豆瓣提供的镜像源为例(URL 为 https://pypi.doubanio.com/simple/),介绍不同工具更改 PyPI 镜像源的方法:

pip

临时设置可以通过 -i 选项:

$ pip install -i https://pypi.doubanio.com/simple/ flask

全局设置有不同的层级和文件位置,以用户全局(per-user)为例,在 Linux & macOS 中,配置需要写到 ~/.pip/pip.conf 文件中;Windows 中,配置文件位置为 %HOMEPATH%\pip\pip.ini,%HOMEPATH% 即你的用户文件夹,一般为“\Users\<你的用户名>”,具体值可以使用 echo %HOMEPATH% 命令查看。

通常你需要手动创建对应的目录和文件,然后写入下面的内容:

[global]
index-url = https://pypi.doubanio.com/simple
[install]
trusted-host = pypi.doubanio.com

附注:按照 pip 文档,上面的配置文件位置是旧(legacy)的配置方式,但是因为比较方便设置,这里沿用了。新的建议是 Linux & macOS 放到 $HOME/.config/pip/pip.conf,Windows 则放到 %APPDATA%\pip\pip.ini。具体可以访问 pip 文档配置部分查看。

Pipenv

类似 pip 的 -i (–index-url)选项,你可以使用 –pypi-mirror 临时设置镜像源地址:

$ pipenv install --pypi-mirror https://pypi.doubanio.com/simple flask

如果想对项目全局(per-project)设置,可以修改 Pipfile 中 [[source]] 小节:

[[source]]

url = "https://pypi.doubanio.com/simple"
verify_ssl = true
name = "douban"

另外一种方式是使用环境变量 PIPENV_PYPI_MIRROR 设置(Windows 系统使用 set 命令):

$ export PIPENV_PYPI_MIRROR=https://pypi.doubanio.com/simple

你可以通过把这个环境变量的设置语句写入到终端的配置文件里实现“永久”设置,Linux & macOS 可参考这里,Windows 可参考这里

Poetry / Flit

因为 Poetry,Flit 这些工具遵循 PEP 518 创建了一个 pyproject.toml 文件来替代 setup.py、Pipfile 等配置文件,所以我们可以在这个文件里更改 PyPI 源。

使用 Poetry 时,在 pyproject.toml 末尾添加下面的内容来设置自定义镜像源:

[[tool.poetry.source]]
name = "douban"
url = "https://pypi.doubanio.com/simple/"

目前暂时没有其他方式,未来或许会为 poetry add 命令添加一个相关的设置选项。

同样的,Flit 大概要添加下面这些内容(未测试):

[[tool.flit.source]]
name = "douban"
url = "https://pypi.doubanio.com/simple/"

常用的国内 PyPI 镜像列表

  • 豆瓣 https://pypi.doubanio.com/simple/
  • 网易 https://mirrors.163.com/pypi/simple/
  • 阿里云 https://mirrors.aliyun.com/pypi/simple/
  • 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple/

顺便提一下,使用镜像源需要注意一个问题:包的版本可能不会及时更新,遇到这种情况可以通过临时换回官方源解决。

《Flask 入门教程》第 1 章:准备工作

在通过这本书学习 Flask 开发前,我假设你了解了 Python 和 HTML 的基础知识。你的 Python 版本可以是 2.7,也可以是 3.3 及以上版本。电脑的操作系统可以是 Windows,也可以是 macOS 或 Linux。

安装编辑器和浏览器

对于编辑器来说,每个人都有不同的偏好,你可以自由选择。可以选择功能丰富的IDE(集成开发环境),比如 PyCharm;也可以选择相对轻量的编辑器,比如 Atom 或 Sublime Text。浏览器建议使用 Firefox 或 Chrome

使用命令行

在本书中,你需要使用命令行窗口来执行许多操作。你可以使用 Windows 下的 cmd.exe,或是 macOS 和 Linux 下的终端(Terminal)。下面我们执行一个最简单的 whoami 命令(即 Who Am I?):

$ whoami
greyli

这个命令会打印出当前计算机用户的名称。其他常用的命令还有 cd 命令,用来切换目录(change directory);mkdir 命令,用来创建目录(makdirectory)。在不同的操作系统上,执行某个操作的命令可能会有所不同,在必要的地方,书里会进行提示。

我们先来为我们的程序创建一个文件夹:

$ mkdir watchlist
$ cd watchlist

除非特别说明,从现在开始,本书假设你的工作目录将是在项目的根目录,即 watchlist/ 目录。

为了确保你已经正确安装了 Python,可以执行下面的命令测试是否有报错:

$ python --version
Python 2.7.11

对于 Windows 用户,请使用 cmder(一个基于 ConEmu 实现的终端模拟器) 来代替系统自带的 cmd.exe,或是使用安装 Git for Windows 后(下一节)附带的 Git Bash。cmder 集成了 Git Bash,支持一些在 Linux 或 macOS 下才能使用的命令(程序),比如 ls、cat、nano、ssh 等,这些命令我们在后面会用到。

使用 Git

Git 是一个流行的版本控制工具,我们可以用它来记录程序源码和文件的变动情况,或是在编程时进行多人协作,你可以把它看做一个优雅的代码变动备份工具。

如果你还不熟悉 Git 也没关系,可以先按照书中的命令去做,有时间再去了解原理。现在要做的第一件事就是在你的电脑上安装 Git (可以执行 git --help 命令检查是否已经安装,没有提示“命令未找到(Command not found)”则表示已安装)。

安装后可以在命令行先使用使用下面的命令查看版本,没有报错则表示已正确安装:

$ git --version
git version 2.17.1

为了让 Git 知道你是谁,以便在提交代码到版本仓库的时候进行记录,使用下面的命令设置你的信息:

$ git config --global user.name "Grey Li"  # 替换成你的名字
$ git config --global user.email "withlihui@gmail.com"  # 替换成你的邮箱地址

现在为我们的项目文件夹创建一个 Git 仓库,这会在我们的项目根目录创建一个 .git 文件夹:

$ git init
Initialized empty Git repository in ~/watchlist/.git/

Git 默认会追踪项目文件夹(或者说代码仓库)里所有文件的变化,但是有些无关紧要的文件不需要记录变化,我们在项目根目录创建一个 .gitignore 文件,在文件中写入忽略文件的规则。因为文件内容比较简单,我们直接在命令使用 nano 来创建:

$ nano .gitignore

在 nano 编辑界面写入常见的可忽略文件规则

*.pyc
*~
__pycache__
.DS_Store

使用 Control + O 和 Enter 键保存,然后按下 Control + X 键退出。在后续章节,对于简单的文件,都会使用 nano 创建,这部分操作你也可以使用编辑器来完成。

将程序托管到 GitHub(可选)

这一步是可选的,将程序托管到 GitHub、GitLab 或是 BitBucket 等平台上,可以更方便的备份、协作和部署。这些托管平台作为 Git 服务器,你可以为本地仓库创建远程仓库。

首先要注册一个 GitHub 账户,点击访问注册页面,根据指示完成注册流程。登录备用。

设置 SSH 密钥

一般情况下,当推送本地改动到远程仓库时,需要输入用户名和密码。因为传输通常是通过 SSH 加密,所以可以通过设置 SSH 密钥来省去验证账号的步骤。

首先使用下面的命令检查是否已经创建了 SSH 密钥:

$ cat ~/.ssh/id_rsa.pub

如果显示“No such file or directory”,就使用下面的命令生成 SSH 密钥对,否则复制输出的值备用:

$ ssh-keygen

一路按下 Enter 采用默认值,最后会在用户根目录创建一个 .ssh 文件夹,其中包含两个文件,id_rsa 和 id_rsa.pub,前者是私钥,不能泄露出去,后者是公钥,用于认证身份,就是我们要保存到 GitHub 上的密钥值。再次使用前面提到的命令获得文件内容:

$ cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3Nza...省略 N 个字符...3aph book@greyli

选中并复制输出的内容,访问 GitHub 的 SSH 设置页面(导航栏头像 – Settings – SSH and GPG keys),点击 New SSH key 按钮,将复制的内容粘贴到 Key 输入框里,再填一个标题,比如“My PC”,最后点击“Add SSH key”按钮保存。

创建远程仓库

访问新建仓库页面(导航栏“+” – New repository),在“Repository name”处填写仓库名称,这里填“watchlist”即可,接着选择仓库类型(公开或私有)等选项,最后点击“Create repository”按钮创建仓库。

因为我们已经提前创建了本地仓库,所以需要指定仓库的远程仓库地址(如果还没有创建,则可以直接将远程仓库克隆到本地):

$ git remote add origin git@github.com:greyli/watchlist.git  # 注意更换地址中的用户名

这会为本地仓库关联一个名为“origin”的远程仓库,注意将仓库地址中的“greyli”换成你的 GitHub 用户名

创建虚拟环境

虚拟环境是独立于 Python 全局环境的 Python 解释器环境,使用它的好处如下:

  • 保持全局环境的干净
  • 指定不同的依赖版本
  • 方便记录和管理依赖

我们将使用 Pipenv 来创建和管理虚拟环境、以及在虚拟环境中安装和卸载依赖包。它集成了 pip 和 virtualenv,可以替代这两个工具的惯常用法。另外,它还集成了 Pipfile,它是新的依赖记录标准,使用 Pipfile 文件记录项目依赖,使用 Pipfile.lock 文件记录固定版本的依赖列表。这两个文件替代了手动通过 requirements.txt 文件记录依赖的方式。我们首先使用 pip 安装 Pipenv,Windows 系统使用下面的命令:

$ pip install pipenv

Linux 和 macOS 使用下面的命令:

$ sudo -H pip install pipenv

使用 Pipenv 创建虚拟环境非常简单,使用 pipenv install 命令即可为当前项目创建一个虚拟环境:

$ pipenv install

这个命令执行的过程包含下面的行为:

  • 为当前目录创建一个 Python 解释器环境,按照 pip、setuptool、virtualenv 等工具库。
  • 如果当前目录有 Pipfile 文件或 requirements.txt 文件,那么从中读取依赖列表并安装。
  • 如果没有发现 Pipfile 文件,就自动创建。

创建虚拟环境后,我们可以使用 pipenv shell 命令来激活虚拟环境,如下所示:

$ pipenv shell

安装 Flask

无论是否已经激活虚拟环境,你都可以使用下面的命令来安装 Flask:

$ pipenv install flask

这会把 Flask 以及相关的一些依赖包安装到对应的虚拟环境,同时 Pipenv 会自动更新依赖文件中。

本章小结

当你进行到这里,就意味这我们已经做好学习和开发Flask程序的全部准备了。使用 git status 命令可以查看当前仓库的文件变动状态:

$ git status

下面让我们将文件改动提交进 Git 仓库,并推送到在 GitHub 上创建的远程仓库:

$ git add .
$ git commit -m "I'm ready!"
$ git push -u origin master # 如果你没有把仓库托管到 GitHub,则跳过这条命令,后面章节亦同

这里最后一行命令添加了 -u 参数,会将推送的目标仓库和分支设为默认值,后续的推送直接使用 git push 命令即可。在 GitHub 上,你可以通过 https://github.com/你的用户名/watchlist 查看你的仓库内容。

提示 你可以在 GitHub 上查看本书示例程序的对应 commit:1b6fe4a

进阶提示

Pipenv使用时出现TypeError、ResourceWarning、或ImportError错误的解决方法

10月16日更新

Pipenv 10.13 据说已经修复所有相关问题,可以尝试更新:

$ python3 -m pip install --upgrade pip 
$ python3 -m pip install --upgrade pipenv

Pipenv 2018.7.1

Pipenv 2018.7.1版本和最新版本的pip不兼容,根据不同的命令和环境,会出现下面的异常:

  • TypeError: ‘module’ object is not callable (pipenv install)
  • ResourceWarning: Implicitly cleaning up … (pipenv install)
  • ImportError: cannot import name ‘get_installed_distributions’ (pipenv graph)

目前可以临时通过降级pip来解决:

$ python3 -m pip install pip==10.0.1

如果你使用Python2,则使用下面的命令:

$ python -m pip install pip==10.0.1

相关issue见#2924#2925#2944

Pipenv 2018.10.9

如果使用的是最新的2018.10.9版本,那么在Python 2.7下会出现下列异常:

  • ImportError: No module named weakref
  • ImportError: No module named shutil_get_terminal_size

可以临时通过手动安装这两个库来解决:

$ pip install backports.weakref backports.shutil_get_terminal_size

Pipenv 修复版本发布后

等到Pipenv发布新版本修复了这些问题后,可以通过下面的命令升级pipenv和pip:

$ python3 -m pip install --upgrade pip
$ python3 -m pip install --upgrade pipenv

如果你在阅读《Flask Web开发实战》时还遇到了其他问题,可以先到FAQ中查看,没有解决可以在helloflask仓库创建issue。