分类目录归档:计算机与编程

一个困扰我两年的 Flask「Bug」

TL;DR 版本:

如果你的程序存储在单脚本里,比如 app.py,那么 .flaskenv 和 .env 应该放在程序脚本的同级目录:

myproject/
    - app.py
    - templates
    - static
    - .flaskenv
    - .env

如果你的程序存储在程序包里,那么 .flaskenv 和 .env 应该放在程序包的同级目录:

myproject/
    - app/
        - templates
        - static
        - __init__.py
        - views.py
    - .flaskenv
    - .env

如果你把 .flaskenv 和 .env 放到了再往上一层(myproject 再往上),或是把你的程序包或程序脚本放到了一个子目录(比如 /myproject/myapp),那么执行 flask run 就会出错。


严格来说算不上 bug,而是一个很容易导致出错的设计。具体行为是,如果你安装了 python-dotenv,同时在 Flask 程序的上层目录创建了 .env 或 .flaskenv 文件,那么你将没法成功执行 flask run 等命令,因为这会导致 Flask 没法正确找到对应的 Flask 程序实例。

这个问题从 Flask 开始引入 CLI 机制开始就存在了,困扰了我两年。18 年偶然在用户根目录创建了一个 .env 文件,发现 Flask 程序没法运行了,当时遇到的各种 bug 太多,没仔细考虑这两者之间的关联。后来经过几次测试,才确定下来是上层目录的 .env 和 .flaskenv 文件导致,但是一时找不到原因,就暂时放下了。直到 19 年 11 月,花了几个小时排查,还是没找到原因。

中间花了很长时间来追踪 Windows 特定的 Flask 程序无法启动的 bug(TypeError: environment can only contain strings),实在是怕了。因为 Flask 的 CLI 涉及太多东西,有时你要钻进 python-dotenv(#101) 和 Werkzeug(#1320) 才能找到问题的原因。

但是问题不解决的话,你永远睡不好觉。《Flask Web 开发实战》第一部分的示例程序都放在了一个程序仓库,而且都放在了子目录,这意味着如果读者错误的在仓库根目录创建 .env 和 .flaskenv 文件的话,就会导致子目录下的六个示例程序没法运行。前后大概收到 6 个相关的读者反馈,虽然后续在网站上添加了提醒,在重印的书里介绍创建 .env/.flaskenv 文件的地方追加提醒,但这终究没有真正解决问题,而且总会有人可以完美的错过所有提示。

如果每个人都可以在书上标记出错位置并共享,那么这一页应该会有很多红色小叉号(参考《超级马里奥制造》,也许未来某个电子书平台会做出来这个功能 :P)。

前几天在这个 Issue 的提醒下,又花了两个小时排查,这次终于找到原因。加上写 PR(#3560)和 Issue(#3561),前后两年一共花了 8 个小时,这个问题终于有了着落。没意外的话,预计会在下一个版本的 Flask 中更新。下面是具体原因。

本来以为这个问题和 python-dotenv 或 Werkzeug 相关,没想到只是 Flask 本身代码的问题,我太笨了,这个问题本可以早一点解决。

按照预定的行为,当安装了 python-dotenv,Flask 会自动加载 .env 和 .flaskenv 里的环境变量。python-dotenv 在搜索存储环境变量的文件时,会从当前目录开始向上搜索,如果找到就返回对应的文件路径。但是这时 Flask 如果发现 .env 或 .flaskenv 的所在目录不是当前目录,就会把当前工作目录切换到 .env 和 .flaskenv 所在的目录(相关源码)。而如果你的程序模块或程序包不是和 .env/.flaskenv 同级目录的话,就会导致找不到程序实例。

使用下面的步骤可以重现:

$ git clone https://github.com/greyli/flask-env-test
$ cd flask-env-test
$ pip install -r requirements.txt  # or just pip install flask[dotenv]
$ cd hello
$ flask run

示例项目的文件结构如下:

- flask-env-test
    - .env
    - hello
        - app.py

像示例程序这样把程序存储在 app.py 文件中时,运行 flask run 你会看到下面的报错:

$ flask run
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
Usage: flask run [OPTIONS]

Error: Could not locate a Flask application. You did not provide the "FLASK_APP" environment variable, and a "wsgi.py" or "app.py" module was not found in the current directory.

如果你使用 FLASK_APP 指定了程序的导入路径,那么错误大概会是这样:

$ flask run
 * Serving Flask app "myapp"
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
Usage: flask run [OPTIONS]

Error: Could not import "myapp".

相关链接

安装 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 和论坛,我会定期回复,群聊沟通效率很低,更适合闲聊)

《Flask 入门教程》第二版发布

Flask 入门教程》第二版发布了!新版本主要有以下变化(详见 commit 列表):

  • 去掉了 Pipenv 的介绍,改为使用 venv/virtualenv+pip
  • 所有提示和注意段落使用引用样式标注
  • 修正了大量笔误
  • 调整前言和后记内容
  • 调整部分措辞

访问本书主页下载 PDF 或在线阅读。

Wii Remote——支持上下左右四个方向的幻灯片翻页笔

如果你用 reveal.js 做幻灯片,你会发现没有合适的翻页笔可以用。具体来说,是没有支持上下左右四个方向的翻页笔。

参加 PyCon China 上海站的时候,因为现场条件限制,不得不用翻页笔,但是发现翻页笔只支持上下翻页,不支持左右翻页,而使用 reveal.js 做的幻灯片常常会包含上下左右四个方向的切换。当时还和其他志愿者说这是个潜在的商机——做一个支持上下左右四个方向的翻页笔。

回来去网上搜了一下,发现真的没有支持四个方向的翻页笔。想想也没有多少人会需要用到四个方向切换。市场份额最大的幻灯片制作软件是 PPT、Keynote 和 Prezi。前两者上下翻页,Prezi 左右翻页,所以有一些翻页笔会支持在「上下」和「左右」两种模式切换,还有一些会支持切换成「PageUp 和 PageDown」翻页模式,用来给 PDF 文件翻页。

接着,我调整方向开始找有没有只有四个方向键的蓝牙小键盘,找的过程中突然想到以前用手柄映射键盘按键来玩一些不支持手柄的电脑游戏,那么 Wii Remote(Wii 右手控制器)也许也可以通过映射按键来当做一个翻页笔用。搜了一些资料,发现真的可以!找到了一个叫做 WiinRemote 的程序来做按键映射(macOS 可以使用 Darwiin Remote,另外还有其他通用的替代选项)。

以 Windows 10 为例,你需要先通过蓝牙把 Wii Remote 连接到电脑,基本步骤如下:

  • 任务栏蓝牙图标右键
  • 加入个人区域网
  • 添加设备
  • 同时按住 Wii Remote 的 1 和 2 键进入匹配模式(四个指示灯闪烁)
  • 选择设备,一般名称会显示 Nintendo RVL-CNT,点下一步
  • PIN 码留空点下一步

第一次和电脑配对会有些麻烦,后续就可以很容易连接了。

连接成功后打开 WiinRemote 配置按钮。在 Options – Preferences – Button Assign 选项里可以定义按键映射。除了设置上下左右四个方向切换以外,Wii Remote 上的其他按钮也可以利用起来,比如分别用来映射 Enter、Esc 这些按钮。

设置按键映射

唯一的缺点就是 Wii Remote 和常见的翻页笔相比有点大……

和翻页笔比大小

虽然看起来很完美,但后来我还是采用了另一个替代方案,买了一个支持切换 PageDown 和 PageUp 翻页模式的翻页笔,这样可以让 reveal.js 的幻灯片按照从左到右,从上到下的顺序逐页切换。

也许下次演讲你会看到我用 Wii Remote 来翻页。

P.S. 除了用来当翻页笔,Wii Remote 甚至可以用来实现数字白板、触控屏幕和 3D 头戴显示器(Free or cheap Wii Remote hacks)。

我在一本技术书里放置的十个彩蛋

Flask Web 开发实战》已经出版一年多,书里面的一些彩蛋也不知道有多少被人发现了。再不公开,我可能都要忘了。所以就趁着出版一周年这个契机整理一下吧。

这些彩蛋大都是关于电影的一些双关和文字游戏,有一些说是彩蛋可能会有些勉强。比较正式的一共有 10 个,分别对应十部电影。也就是第三章介绍模板引擎时的实例程序电影清单里列出来的十部电影。

下面按照出现顺序一一剧透。

阿甘正传(Forrest Gump)

P14 第一章:初识 Flask 1.3

这一节介绍运行 Flask 程序,章节标题是「Run, Flask, Run!」。

阿甘正传里,当阿甘被欺负的时候,珍妮总会喊「Run, Forrest, Run!」。

blank

红白蓝三部曲(Three Colours trilogy)

P36 第二章:Flask 和 HTTP 2.2.3.3

介绍 URL 规则的 any 转换器时使用的示例如下:

@app.route('/colors/<any(blue, white, red):color>')
def three_colors(color):
    return '<p>Love is patient and kind. Love is not jealous or boastful or proud or rude.</p>'

这个并没有什么特殊含义,就是列了三个颜色作为 URL 选项,返回的都是圣经里的那句话。

电影清单

P76 第三章:模板

第三章在示例程序里的填充数据是整本书所有彩蛋相关的十部电影,作为一个足够显眼的线索。

movies = [
    {'name': 'My Neighbor Totoro', 'year': '1988'},
    {'name': 'Three Colours trilogy', 'year': '1993'},
    {'name': 'Forrest Gump', 'year': '1994'},
    {'name': 'Perfect Blue', 'year': '1997'},
    {'name': 'The Matrix', 'year': '1999'},
    {'name': 'Memento', 'year': '2000'},
    {'name': 'The Bucket list', 'year': '2007'},
    {'name': 'Black Swan', 'year': '2010'},
    {'name': 'Gone Girl', 'year': '2014'},
    {'name': 'CoCo', 'year': '2017'},
]

另外这个电影清单也有现实版:IMDb镜像豆列

寻梦环游记(CoCo)

P105 第四章:模板

这一章一开始给出了一个 HTML 登录表单的示例,示例代码和渲染后的画面(图 4-1)中,用户名输入框的占位文字是 Héctor Rivera,密码输入框的占位文字是 19001130,勾选了记住登录状态的「记住我(Remember me)」选项。

blank

Héctor Rivera 是电影里小男孩在亡灵世界里遇到的死去的爸爸,他的生日是 1900 年 11 月 30 日,勾选了「Remember me」,他就会被人记住,所以就不会消失了……

消失的爱人(Gone Girl)

P139 第五章:数据库

第一小节介绍关系型数据库的时候给出了一个示例表格(表 5-1),如下:

blank

这两条数据分别对应《消失的爱人》里的男女主角。

记忆碎片(Memento)

同样在第五章,这一章写了一个用来记笔记的示例程序,分别使用了三条笔记内容作为示例演示 CRUD 操作,即:

blank

这三条笔记均是《记忆碎片》里男主角记在各处的笔记。

中国发出的第一封电子邮件

P178 第六章:电子邮件

第六章介绍电子邮件使用的示例邮件正文是「Across the Great Wall we can reach every corner in the world.」。这是 1987 年从中国发出的第一封电子邮件正文。

本来是想把 Wall 换成另一个单词以便反映现状的,但是出于安全考虑,没那么做。不过这句话原版现在来看已经是够讽刺的了。

黑客帝国(The Matrix)

P246 第八章:个人博客

介绍自定义 flash 消息样式的时候,给出了下面的自定义 CSS 类示例:

.alert-matrix {
    color: #66ff66;
    background-color: #000000;
    border-color: #ebccd1; /* 这一行好像没用 */
}

调用示例如下:

flash('Knock, knock, Neo.', 'matrix')

实际的显示效果如下所示:

blank

模拟了电影原图:

blank

 

《未麻的部屋》(Perfect Blue)和《黑天鹅》(Black Swan)

P236 第八章:个人博客

这一章的示例博客名字叫做 Bluelog,模仿了《未麻的部屋》里记录未麻生活的网站,初始化账户的信息如下:

def fake_admin():
    admin = Admin(
        username='admin',
        blog_title='Bluelog',
        blog_sub_title="No, I'm the real thing.",
        name='Mima Kirigoe',
        about='Um, l, Mima Kirigoe, had a fun time as a member of CHAM...'
    )
    ...

另外介绍博客的主题地方提供了蓝色和黑色两个主题,主题名分别是「Perfect Blue」和「Black Swan」,对应《未麻的部屋》和《黑天鹅》两部电影的英文名(后者致敬了前者)。

龙猫(My Neighbor Totoro)

P260 第八章:个人博客

这个算不上彩蛋……在介绍生成英文标题 slug 的 slugify 函数的时候,实际调用的示例使用了龙猫的几种名字作为输入数据:

>>> slugify(u'My Neighbor Totoro')
u'my-neighbor-totoro'
>>> slugify(u'邻家的豆豆龙')
u'lin-jia-de-dou-dou-long'
>>> slugify(u'となりのトトロ')
u'tonarinototoro'

遗愿清单(The Bucket List)

P421 第十章:待办事项程序

这个彩蛋是刻意加的,因为这一章的示例程序是一个 Todo List 程序,所以要找一个和清单相关的电影。示例程序里的几条待办事项就是电影里的一些遗愿。当时还没看过这个电影,特意去看了一遍,感觉一般。

blank

应该就这些了,如果还有其他的,欢迎补充。

那么放置这些彩蛋的目的是?没目的……仅仅就是为了好玩。同时为了能让发现彩蛋的人也会觉得好玩。

 

要不我们还是用回 virtualenv/venv 和 pip 吧

这篇文章没什么新东西,只是介绍古老又靠谱的 Python 虚拟环境和依赖管理方式:virtualenv/venv+pip。一来方便被我在《Flask 入门教程》和《Flask Web 开发实战》带入 Pipenv 坑的初学者了解基础工具的用法,二来方便其他 Python 初学者参考,自己顺便做个总结。如果你想了解更多详细内容,Python 官方教程这一章写的更好,可以替代这篇文章。

既然大部分试图简化 Python 虚拟环境和依赖管理工作流程的新工具都不够稳定,继续使用 virtualenv/venv 和 pip 这样的底层工具也是一个不错的选择,而且大多数人也是这么做的。它们虽然用起来有一点麻烦,但至少更可靠。

基本概念

下面是一些基本概念:

  • PyPA:指 Python Packaging Authority,一个维护 Python 打包相关项目的小组,相关项目具体见 https://github.com/pypa
  • pip:Python 包安装器 。
  • virtualenv:Python 虚拟环境管理工具。
  • venv:Python 标准库内置的虚拟环境管理工具,Python 3.3 加入,Python 3.5 开始作为管理虚拟环境的推荐工具,用法类似 virtualenv。如果你使用 Python 3,推荐使用 venv 来替代 virtualenv。

使用 virtualenv/venv 管理虚拟环境

venv 模块

如果你使用 Python 3(具体说是 Python 3.3 及以上版本),推荐使用标准库内置的 venv 模块替代 virtualenv,两者的使用方式基本相同,唯一不同的是创建虚拟环境的方式。

如果你使用 Python 2,那就只能选择 virtualenv,你需要额外安装它。我先假设你已经安装了 pip,因为在 Python 2 >=2.7.9 或 Python 3 >=3.4 这些版本的 Python 会一并安装 pip,其他版本可以参考文档的安装部分。在 Windows 下使用下面的命令安装 virtualenv:

$ pip install virtualenv

其他操作系统可以使用下面的命令安装:

$ sudo pip install virtualenv

尽管不推荐使用 sudo pip 的方式安装 Python 包,但这仍然是最简单和统一的方式。更安全的方式是使用系统包管理器来安装,或是使用 pip –user 方式安装。

创建虚拟环境

假设我们的项目名叫 snow,创建对应的文件夹然后切换到根目录:

$ mkdir snow
$ cd snow

如果使用 venv,那么使用下面的命令创建虚拟环境,其中 snow-venv 是虚拟环境的名字,也作为创建的虚拟环境文件夹名称,你可以自由修改(通常会使用 venv 或 env 作为虚拟环境名):

$ python -m venv snow-venv

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

$ virtualenv snow-venv

这会在当前目录创建名为 snow-venv 的虚拟环境文件夹,你需要把这个文件夹名称加入 .gitignore 文件以便让 Git 忽略。

激活虚拟环境

通过执行对应的激活脚本来激活虚拟环境,不同操作系统的激活命令(激活脚本及路径)有一点不同。Windows(CMD.exe)使用下面的命令激活:

$ snow-venv\scripts\activate

Linux 和 macOS(bash/zsh)使用下面的命令:

$ source snow-venv/bin/activate

或:

$ . snow-venv/bin/activate

类似的,其他终端程序可以执行对应的激活脚本来激活虚拟环境。

激活虚拟环境以后,命令行提示符前会显示当前虚拟环境的名字:

(snow-venv) $

使用 deactivate 命令可以退出虚拟环境。

使用 pip 管理依赖

简单列一下基本用法,虽然大部分人都很熟悉了……以 Flask 为例,首先是安装依赖:

(snow-venv) $ pip install flask

更新依赖:

(snow-venv) $ pip install --upgrade flask

或是:

(snow-venv) $ pip install -U flask

卸载依赖:

(snow-venv) $ pip uninstall flask

除此之外,还有 pip show flask 命令可以查看某个依赖的详细信息,pip list 列出所有依赖。

下面的命令可以手动生成依赖列表:

(snow-venv) $ pip freeze > requirements.txt

如果你需要手动开发依赖和生产依赖,可以手动把开发相关的依赖放到单独的文件,比如 requirements-dev.txt。

当你需要在新的机器创建程序运行环境时,(创建虚拟环境后)只需要使用下面的命令从依赖文件安装所有依赖:

(snow-venv) $ pip install -r requirements.txt

如果安装包的时候速度太慢,可以考虑设置 PyPI 国内镜像,具体参考这篇文章

从其他工具迁移回来

如果你想从 Pipenv 迁移回来,方法很简单,只需要生成一个 requirements.txt 文件即可。使用下面的命令生成一般依赖列表:

$ pipenv lock -r

使用下面的命令输出开发依赖列表:

$ pipenv lock -r --dev

然后手动把两个命令的输出保存为 requirements.txt 和 requirements-dev.txt。

从 Poetry、Conda 等其他工具迁移回来可以使用 pipreqs 来生成 requirements.txt,它会基于项目代码的导入语句来生成依赖列表。

在下一篇文章,我会介绍一些辅助工具来搭配 virtualenv/venv+pip 使用,让虚拟环境和依赖管理更方便,比如 virtualenvwrapper、pip-tools 等。

(4)

只用来管理 Python 版本和虚拟环境,Miniconda 也是一个很好的选择

Anaconda 是一个面向数据科学的 Python 发行版,它打包了 Conda、Python 和一堆机器学习和人工智能相关和常用的包,而且还可以用来安装一些非 Python 编写的库。对于科学计算相关的用途非常方便,开箱即用。看起来很适合实验室或是学校机房使用……

对于常规的 Python 开发来说,Anaconda 太重了,而且主要面向科学计算领域,但是精简版的 Miniconda 却是一个很好的 Python 版本和虚拟环境管理工具。

更重要的是 Miniconda 兼容三个主流操作系统,而且不同平台使用同样的命令和接口(Conda 4.6 以上)。这大概算是个人偏好,因为我总在写东西给别人看,所以总是喜欢兼容主流操作系统并且接口统一的解决方案。而且另一方面 pyenv、virtualenvwrapper、direnv 等等相关替代工具都没有原生 Windows 支持。 

Miniconda 是什么

先来理清几个概念:

  • Conda:包、依赖和环境管理器。
  • Anaconda(某种蟒蛇的名字):面向数据科学的 Python 发行版,包含 conda、conda-build、Python 和 100+ 常用的数据科学常用的库及其依赖。
  • Miniconda:精简版的 Anaconda,也是一个 Python 发行版,只包含 conda、Python 和一些基本的包。

相关资源:

Miniconda 基本用法

首先你需要访问下载页面下载对应操作系统的安装包进行安装,Python 版本选择你想作为默认选项(base version)的版本。然后打开 Windows 下的 Anaconda Prompt 或是 Anaconda Powershell Prompt,Linux 或 macOS 直接使用终端程序。

下面是一些关键用法介绍,详细内容可以阅读 Conda 文档

创建虚拟环境

使用 conda create 命令创建虚拟环境,使用 –name 选项(-n)指定虚拟环境名称:

$ conda create --name foo

使用 conda info 命令查看当前环境名称、Python 版本、虚拟环境文件夹位置、Conda 版本等等各种信息:

$ conda info

使用 –envs 选项(-e)查看所有已创建的虚拟环境。在列出的虚拟环境中,使用星号(*)标识的是当前激活的虚拟环境:

$ conda info --envs

激活虚拟环境

使用 conda activate 命令激活虚拟环境,添加虚拟环境名作为参数:

$ conda activate foo

激活以后会在命令行提示器前显示虚拟环境名称,比如:

(foo) $

不添加虚拟环境名称,就会重新激活基础环境(base):

$ conda activate

设置虚拟环境的 Python 版本

在创建虚拟环境的时候,可以使用 python 参数指定 Python 版本。假设你使用的 Miniconda 默认版本是 Python 3.7,如果你想创建一个 Python 2.7 的虚拟环境,使用下面的命令:

$ conda create --name snakes python=2.7

你可以在虚拟环境名加上标识方便识别 Python 版本,比如:

$ conda create --name snakes-py27 python=2.7

搭配 pip / Poetry 来管理依赖

因为 Conda 安装库的时候默认使用 Conda 自己的仓库,这里包含的 720 多个库除了流行的 Python 包外大多是数据科学相关的包。所以,更好的选择是使用官方 PyPI 仓库,这样可以确保你使用到最近更新的包,而且不会出现有些包找不到的情况。

我们要做的就是只用 Conda 的 Python 版本和虚拟环境管理功能,不用它来管理依赖。依赖管理(安装、卸载、更新等)仍然使用 pip 进行,或是进一步搭配 pip-tools 来管理依赖。

你需要在 conda 环境内使用下面的命令安装 pip:

$ conda install pip

或是统一使用下面的命令格式创建虚拟环境:

$ conda create --name bar pip

这样在执行 pip 命令时会使用虚拟环境内的 pip,而不是系统全局的 pip。这样做的副作用是会产生几个多余的依赖。

除此之外,你也可以搭配使用 Poetry。Poetry 默认会自动创建虚拟环境,所以需要关闭 Poetry 自动创建虚拟环境的设定,执行下面的命令即可:

$ poetry config settings.virtualenvs.create false

总结

按照我目前掌握到的信息,Miniconda 的优缺点总结如下:

优点

  • 用法简单,​易于上手
  • 替代 pyenv+virtualenv/venv+virtualenvwrapper 的多个工具组合
  • 类似 virtualenvwrapper,可以在任意位置激活虚拟环境,而不是必须在项目根目录
  • 支持管理不同的 Python 版本
  • 兼容性很好,支持 Windows

缺点

  • 在 Windows 下,需要使用专用的命令行程序
  • 依赖管理比较弱,需要搭配 pip/Poetry 来使用
  • conda 和 pip/Poetry 组合存在潜在的冲突,但情况在改善

既然一个 Miniconda 就能很好的胜任 Python 版本管理和虚拟环境管理的任务,为什么要用 pyenv+virtualenv/venv+virtualenvwrapper 呢?遗憾的就是依赖管理功能不够完善,和 pip/Poetry 搭配使用则可能会有潜在的冲突。所以,推荐觉得 virtualenv/venv+pip 搭配太麻烦的初学者使用;推荐能接受 Conda+pip/Poetry 这种搭配的人使用;推荐使用 Python 做数据科学相关工作的人使用。

对比之下,最稳定的解决方案大概还是 virtualenv/venv+pip+其他工具……
(如果你发现了错误描述、有不同看法和想要补充的信息,欢迎评论。)

(3)

相比 Pipenv,Poetry 是一个更好的选择

前情提要

Pipenv 描绘了一个美梦,让我们以为 Python 也有了其他语言那样完善的包管理器,不过这一切却在后来者 Poetry 这里得到了更好的实现。

这几年 Pipenv 收获了很多用户,但是也暴露了很多问题。虽然 Lock 太慢、Windows 支持不好和 bug 太多的问题都已经改进了很多,但对我来说,仍然不能接受随时更新锁定依赖的设定,在上一篇文章《不要用 Pipenv》里也吐槽了很多相关的问题。

在这篇文章里,我会介绍一个看起来和事实上都更靠谱的 Python 虚拟环境和依赖管理工具 Poetry,作者是 Sébastien Eustace。这是一个新的坑吗?我想并不是,尽管这是一个更年轻的工具,1.0 还没有发布,也存在各种各样的 bug,但至少基本使用流程没有问题,用法设计也符合直觉。

Poetry 是什么

Poetry 和 Pipenv 类似,是一个 Python 虚拟环境和依赖管理工具,另外它还提供了包管理功能,比如打包和发布。你可以把它看做是 Pipenv 和 Flit 这些工具的超集。它可以让你用 Poetry 来同时管理 Python 库和 Python 程序。

安装 Poetry

官方推荐的安装命令是使用自带的 get-poetry.py 脚本,使用 curl:

$ curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python

或者直接下载这个安装脚本 get-poetry.py,然后在本地执行。

因为这个命令在安装时会从 GitHub 下载一个 7M 的压缩包,如果不用代理某些地区可能会很慢。实际测试使用代理安装耗时约 30 秒,不用代理等了 5 分钟,然后连接被重置。

如果没有用代理,可以用 pip 安装(不过 Poetry 官方文档不建议这么做,因为有可能会造成依赖冲突,可以考虑用 pipxpipsi):

$ pip install --user poetry

安装后可以使用下面的命令确认安装成功:

$ poetry --version
Poetry 0.12.17

如果报错,可以试试重新创建一个命令行会话。

附注 在 Mac 上安装报错 ~/.bash_profile 权限错误,临时没管,也能正常运行。后续可以考虑手动把 ~/.poetry/bin 加进去。

Poetry 的基本用法

Poetry 的用法很简单,大部分命令和 Pipenv 接近。我们需要先了解一些基本概念和 Tips:

  • 使用 PEP 518 引入的新标准 pyproject.toml 文件管理依赖列表和项目的各种 meta 信息,用来替代 Pipfile、requirements.txt、setup.py、setup.cfg、MANIFEST.in 等等各种配置文件。
  • 依赖分为两种,普通依赖(生产环境)和开发依赖。
  • 安装某个包,会在 pyproject.toml 文件中默认使用 upper bound(中文翻译?)版本限定,比如 Flask^1.1。这被叫做 Caret requirements(中文翻译?),比如某个依赖的版本限定是 ^2.9.0,当你执行 poetry update 的时候,它或许会更新到 2.14.0,但不会更新到 3.0.0;假如固定的版本是 ^0.1.11,它可能会更新到 0.1.19,但不会更新到 0.2.0。总之,在更新依赖的时候不会修改最左边非零的数字号版本(对于 SemVer 版本号而言),这样的默认设定可以确保你在更新依赖的时候不会更新到具有不兼容变动的版本。另外也支持更多依赖版本限定符号
  • 不会像 Pipenv 那样随时更新你的锁定依赖版本,锁定依赖存储在 poetry.lock 文件里(这个文件会自动生成)。所以,记得把你的 poetry.lock 文件纳入版本控制。
  • 执行 poetry 或 poetry list 命令查看所有可用的命令。

如果你想了解更多进阶的内容,比如设置命令行补全、打包和发布等等,请阅读 Poetry 文档

准备工作

如果你是在一个已有的项目里使用 Poetry,你只需要执行 poetry init 命令来创建一个 pyproject.toml 文件:

$ poetry init

根据它的提示输入你的项目信息,不确定的内容就按下 Enter 使用默认值,后续也可以手动更新。指定依赖的环节可以跳过,手动安装会更高效一点。

如果你想创建一个新的 Python 项目,使用 poetry new <文件夹名称> 命令可以创建一个项目模板:

$ poetry new foo

这会创建一个这样的项目结构:

foo
├── pyproject.toml
├── README.rst
├── foo
│   └── __init__.py
└── tests
    ├── __init__.py
    └── test_foo.py

如果你想使用 src 文件夹,可以添加 –src 选项,这会把程序包嵌套在 src 文件夹里。

创建虚拟环境

使用 poetry install 命令创建虚拟环境(确保当前目录有 pyproject.toml 文件):

$ poetry install

这个命令会读取 pyproject.toml 中的所有依赖(包括开发依赖)并安装,如果不想安装开发依赖,可以附加 –no-dev 选项。如果项目根目录有 poetry.lock 文件,会安装这个文件中列出的锁定版本的依赖。如果执行 add/remove 命令的时候没有检测到虚拟环境,也会为当前目录自动创建虚拟环境。

激活虚拟环境

执行 poetry 开头的命令并不需要激活虚拟环境,因为它会自动检测到当前虚拟环境。如果你想快速在当前目录对应的虚拟环境中执行命令,可以使用 poetry run <你的命令> 命令,比如:

$ poetry run python app.py

如果你想显式的激活虚拟环境,使用 poetry shell 命令:

$ poetry shell

安装包

使用 poetry add 命令来安装一个包:

$ poetry add flask

添加 –dev 参数可以指定为开发依赖:

$ poetry add pytest --dev

追踪 & 更新包

使用 poetry show 命令可以查看所有安装的依赖(可以传递包名称作为参数查看具体某个包的信息):

$ poetry show

添加 –tree 选项可以查看依赖关系:

$ poetry show --tree

添加 –outdated 可以查看可以更新的依赖:

$ poetry show --outdated

执行 poetry update 命令可以更新所有锁定版本的依赖:

$ poetry update

如果你想更新某个指定的依赖,传递包名作为参数:

$ poetry update foo

卸载包

使用 poetry remove <包名称> 卸载一个包:

$ poetry remove foo

常用配置

Poetry 的配置存储在单独的文件中,比 Pipenv 设置环境变量的方式要方便一点。配置通过 poetry config 命令设置,比如下面的命令可以写入 PyPI 的账号密码信息:

$ poetry config http-basic.pypi username password

下面的命令设置在项目内创建虚拟环境文件夹:

$ poetry config settings.virtualenvs.in-project true

另一个常用的配置是设置 PyPI 镜像源,以使用豆瓣提供的 PyPI 镜像源为例,你需要在 pyproject.toml 文件里加入这部分内容:

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

不过经过测试 Poetry 会使用 pip.ini 设置的 PyPI 镜像,而且豆瓣的源好像很久没更新了(创建虚拟环境安装的默认依赖里 importlib-metadata==0.20 找不到),这篇文章列出了一些其他国内的 PyPI 源。

总结

总的来说,我愿意深入尝试和使用 Poetry。当然,经过使用 Pipenv 的痛苦经历,我对推荐工具这种事情变得更保守了。所以我不推荐 Python 初学者使用,不推荐直接在生产环境使用,不推荐没法正常访问国际互联网的人使用。

列一些我了解到的优缺点:

优点

  • 使用标准的 pyproject.toml 标准,不用写多个配置文件
  • 同时支持管理 Python 程序和 Python 库
  • 更符合直觉的默认设计,比如不会随便更新锁定版本的依赖
  • 干净简洁的命令行输出,没有星星和蛋糕
  • 安装包的时候,使用 upper bound 版本限定,而不是 Pipenv 默认的通配符
  • 卸载包的时候,直接卸载孤立的子依赖,不需要像 Pipenv 那样需要再执行 pipenv clean

缺点

  • 「poetry」这个单词有一点难打……
  • 引入新的 pyproject.toml 标准,旧项目需要一点迁移成本和学习成本
  • 会有一些潜在的 bug
  • 解析依赖的过程有时候会久一点
  • 对虚拟环境的管理控制有些弱,没有 Pipenv 那样的删除虚拟环境和清空依赖的操作
  • 缺少一个稳定的维护团队,有大量 issue 和 PR 等待处理,但情况在逐渐好转

当然,你还是可以选择继续使用 virtualenv 和 pip 这些基础工具,直到有一个完美的解决方案出现。或者,也可以选择试试新东西,然后尝试改进它,让完美的解决方案早一点出现。

(2)

回应《也谈「不要用 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」:

blank

和在 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

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)

PyCon China 2019 Tutorial:Python Web 开发第一课

这是在 PyCon China 2019 上海场 9 月 22 号上午九点开始的 Tutorial(实践课程)《Python Web 开发第一课》 的介绍和相关信息。

购票链接:https://www.bagevent.com/event/5886131(Tutorial T3,优惠码 hellogrey)

标题

Python Web 开发第一课

介绍

这是一个面向 Python 程序员的 Web 开发课程,目标听众需要对 Python 基本语法有一定的了解,但对 Web 开发的了解程度没有要求。

在这个课程里,我会将 Python Web 开发所涉及的相关概念进行一个系统的梳理和介绍,包括 HTTP 协议、前端基础知识、常用的 Python Web 框架以及其他各种工具。

这个课程还会包含一个动手编程的环节。我会从最让人头疼的开发环境搭建开始,一步一步教你如何使用 Flask 开发一个简单的 Web 程序。

在课程过后,参与者会对整个 Python Web 开发技术栈有一个全局认识,并掌握基本的 Web 开发知识,而且会对接下来的学习路径有一个清晰的了解。

流程

时长:三小时

一、基本概念

  • Python Web 开发技术栈地图
  • HTTP 协议基础知识(请求与响应、URL 等)
  • 前端基础知识(HTML、CSS、JavaScript、AJAX 等)
  • Python 后端框架的特点和选择(Flask、Django 等)
  • 传统 Web 程序和 Web API 的对比
  • 测试、部署、持续集成等相关概念快速扫盲

二、动手编程

  • 开发环境搭建
  • 运行和调试程序
  • 编写 HTML 模板
  • 添加表单支持
  • 添加数据库支持

三、Q&A

  • 介绍常见的学习误区和建议的学习方向
  • 关于代码或其他任何相关内容的提问

内容难度

初级

目标听众

  • 想了解 Web 开发的前端、运维、测试或其他工程师
  • 想自己做网站的编程爱好者
  • Web 开发或 Python 初学者

听众要求

  • 了解 Python 基本语法
  • 有一台安装了 Python 和浏览器的电脑,并且了解命令行基本操作

讲者介绍

李辉,Flask 等相关项目的维护者,《Flask 入门教程》和《Flask Web 开发实战》的作者,HelloFlask 社区创建者。他撰写过大量技术文章,回答过大量技术问题,在这个过程中积累了一些编程教学的技巧,擅长用简单的语言解释复杂的编程概念。你可以在他的个人网站 greyli.com 了解到更多相关信息。