第 3 章 Python 项目的结构与包的创建

在 Python 圈子里,有许多开发者会无偿公开自己开发的程序库。为了让使用者能够通过 pip 命令安装这些库,我们在发布时需要将其创建成一种特殊的文件,这种文件就是程序包。在使用 Python 语言进行开发的过程中,Python 自带的库往往不能满足我们,因此我们还需要用到这些程序包。

本章将介绍程序包的制作流程。首先,我们要了解一下 Python 项目开发环境的相关工具。然后,我们来了解一下该环境下的相对易于掌握的 Python 项目目录结构以及文件结构,同时对第 2 章中开发的留言板应用进行整理,封装成包。最后还将学习如何将我们开发的项目发布在 PyPI 上,与全世界人分享。

3.1 Python 项目

用 Python 开发的应用程序达到一定规模后,必然会出现多个模块(.py)或程序包目录。同时除源码以外,说明性质的文本文件、管理相关程序库的元信息等都会越来越多。这些为同一个目的服务的文件、目录以及元信息,就是我们所说的项目。

实际上,Python 项目的内部结构是因项目而异的。这里,一个完整的结构需要满足以下条件。

  • 拥有一个在版本管理之下的源码目录

  • 程序信息在 setup.py 中定义

  • 在一个 virtualenv 环境中运行

对于 Python 开发上的一些约定俗成的工具来说,满足上述条件的结构更便于处理。我们在第 1 章中介绍的 pip 和 virtualenv 都属于这类工具。

这些 Python 中的约定俗成的工具也随着时代逐渐变化着。近年来 PEP 标准正被逐渐推行。为了规范这些约定俗成的东西,2013 年成立了 PyPA(Python Packaging Authority)工作组。PyPA 负责 Python 封装方面相关工具的维护,以及 PEP 标准化等工作。许多老牌的封装相关工具都被移交给 PyPA 管理,包括本书中使用的 pip、virtualenv、wheel 现在也都由 PyPA 提供。

如今,许多 Python 项目的结构都以 PyPA 提供的工具为参照,选用了适合这些工具的文件、目录结构。

如果项目的结构符合标准,那么它与工具之间就会有很强的亲和力,而且便于今后自己或其他开发者进一步开发。另外,本章中介绍的结构与流程不但适用于个人的开发环境,同样也适用于团队的开发环境。

NOTE

在 PyPA 的封装文档中,将以发布为目的的一个整体单位称为一个项目(Project)。

https://packaging.python.org/en/latest/glossary.html#term-project

3.2 环境与工具

本节,我们将对 Python 项目开发的必备工具进行了解。此外,还将学习项目的目录结构、必备程序包的管理方法以及安装方法。

3.2.1 用 virtualenv 搭建独立环境

如果大量项目全都混杂在一个环境下,程序很可能会在预想不到的地方停止运行,而且不利于我们把握当前环境的具体状态。所以为了防止出现这些恼人的情况,我们建议各位搭建简单的独立环境。

使用 virtualenv 可以给每个项目搭建一个独立的 Python 环境。

独立环境有以下优点。

  • 添加程序包以及变更版本时,能将影响控制在当前环境内

  • 便于判断已安装的程序包是否可以删除

  • 不再需要该环境时,可以直接删除整个环境

  • 一旦出了问题,那么问题必然出在该环境的项目上,这就有助于我们找出问题所在

virtualenv

NOTE

当前介绍的版本为 virtualenv 1.11.6。

用 pip 安装外部程序库时,该库会被安装到 Python 的安装目录下。比如 Python 是安装在了 usrlocal/ 目录下,那么该外部程序库就会被安装在 usrlocal/lib/python2.7/sitepackages 目录下,这就是库的默认安装路径。但这样一来,不同目的的程序库就全都安装到了同一目录下,不但容易导致版本冲突,而且很难分辨出哪些程序库已经没用了。

另外,在 usrlocal/ 下安装东西时,需要我们提供该目录的写入权限。我们对自己的计算机中的 OS 目录具有写入权限,但是不一定对其他计算机的 OS 目录也拥有写入权限。就算有,乱用权限也很容易导致意外事故,因此应尽量避免乱用权限。

其实,这些问题都可以用 virtualenv 解决。

virtualenv 的主要特征体现在下列功能上。

  • 在 virtualenv 环境中可自由安装 Python,不需要提供 OS 管理员权限

  • 在 virtualenv 环境下,可根据目的不同来安装程序库,这样一来包的安装目的和依存关系就会更加明确

  • 仍然使用 Python 主体,且虚拟环境仅由一小部分备份文件构成,因此环境搭建速度快,占用硬盘空间小

  • 无视 Python 主体的 sitepackages,而且能分离主体上已安装完毕的程序包

  • 可以用 activate/deactivate 命令随时启动 / 关闭 virtualenv 环境

virtualenv 命令可以将任意目录设置为“virtualenv 环境(Python 虚拟环境)”。激活 virtualenv 环境之后,Python 解释器会将该目录识别为默认安装目录。所以,如果我们此时用 pip 命令安装程序库,那么这个程序库将被安装到 virtualenv 环境中。

下面我们来了解一下 virtualenv 的一般使用方法以及一些常用选项。其他详细知识请参考以下网站。

Reference Guide - virtualenv 1.11.6 documentation

https://virtualenv.pypa.io/en/latest/

专栏 Python 标配的用户站点目录功能与 virtualenv 的差异

用户站点目录功能由PEP 3701 提出,随后被 Python 采纳并从 Python 2.6 版本开始提供。这个功能让 Python 解释器能够识别安装在用户目录 $HOME/.local 下的程序库。与此同时,pip 也纳入了这一机制,允许用户使用 —user 选项将程序库安装到 $HOME/.local 目录下。

有了这个功能之后,用户不必使用 virtualenv 就能自由地安装程序库。不过,用户站点目录功能无法像 virtualenv 一样搭建多个环境,自然也就不能在多个环境中切换。

1https://www.python.orgdevpeps/pep-0370

virtualenv 的使用方法

接下来我们看一看 virtualenv 的安装、virtualenv 环境的搭建以及启动(LIST 3.1、LIST3.2)。

LIST 3.1 安装 virtualenv

  1. $ sudo pip install virtualenv

LIST 3.2 virtualenv 环境的搭建及启动

  1. $ cd homebpbook/work
  2. $ virtualenv venv
  3. $ ls -F
  4. venv/
  5. $ source venv/bin/activate
  6. (venv)$

如上所示,使用某一 virtualenv 环境的 activate 命令后,该 virtualenv 环境就会被设置为默认的 Python 执行环境。这里我们搭建了名为 venv 的 virtualenv 环境,因此命令提示符前会出现环境名 (venv)

执行 activate 后,PATH、PROMPT 等数个环境变量会被改写。这样一来,对象 virtualenv 环境的 bin 目录将在 PATH 搜索中被优先处理。此时,只有环境变量会被修改,文件并不会有任何变动。

在这个状态下,计算机会优先使用 venv/bin 目录下的执行文件来执行各个命令。给 virtualenv 环境安装额外的程序库时,我们需要如 LIST 3.3 所示,在命令提示符前有“(venv)”的状态下执行 pip 命令。

LIST 3.3 给 virtualenv 环境安装程序库

  1. (venv)$ pip install requests bottle
  2. (venv)$ pip freeze
  3. bottle==0.12.7
  4. requests==2.4.3

如果要使用这个 virtualenv 环境的 Python,则要执行该 virtualenv 环境的 python 命令(LIST 3.4)。

LIST 3.4 运行 virtualenv 环境的 Python

  1. (venv)$ python
  2. Python 2.7.6 (default, Mar 22 2014, 22:59:56) [GCC 4.8.2] on linux2
  3. Type "help", "copyright", "credits" or "license" for more information.
  4. >>> import sys
  5. >>> sys.executable
  6. 'homebpbook/work/venv/bin/python'
  7. >>> import requests
  8. >>> import bottle

解除 activate 时,需要还原之前被 activate 更改的环境变量。因此我们要结束 shell,或者执行 deactivate 命令(LIST 3.5)。

LIST 3.5 通过 deactivate 命令关闭 virtualenv 环境

  1. (venv)$ deactivate
  2. $ python -c "import sys; print sys.executable"
  3. usrlocal/bin/python

virtualenv 环境的数量没有上限。这里我们再来搭建一个名为 another-venv 的 virtualenv 环境(LIST 3.6)。

LIST 3.6 搭建另一个 virtualenv 环境

  1. $ virtualenv another-venv
  2. $ ls -F
  3. another-venv/ venv/

使用 another-venv 环境的 Python 时,我们无法 import 其他 virtualenv 环境安装的库。

LIST 3.7 运行另一个 virtualenv 环境的 Python

  1. $ source another-venv/bin/activate
  2. (another-venv)$ python
  3. Python 2.7.6 (default, Mar 22 2014, 22:59:56) [GCC 4.8.2] on linux2
  4. Type "help", "copyright", "credits" or "license" for more information.
  5. >>> import sys
  6. >>> sys.executable
  7. 'homebpbook/work/another-venv/bin/python'
  8. >>> import requests
  9. Traceback (most recent call last):
  10. File "<stdin>", line 1, in <module>
  11. ImportError: No module named 'requests'

可见,每一个 virtualenv 环境都是相互独立运行的 Python 环境(LIST 3.7)。而且每个环境都仅由 Python 的一部分文件构成,所以能够很快搭建完成或者删掉,并不会占用太多硬盘空间。

专栏 virtualenv 环境的硬盘使用量

在 Ubuntu 14.04 下,Python 主体的硬盘使用量大约为 100 MB,每个 virtualenv 环境的硬盘使用量约为 6 MB。不过,如果使用了我们后面即将了解的 —always-copy 选项,那么每个 virtualenv 环境将占用约 50 MB。

不再使用的 virtualenv 环境可以连同其所在目录一起删除(LIST 3.8)。

LIST 3.8 删除 virtualenv 环境

  1. (another-venv)$ deactivate
  2. $ rm -R another-venv

专栏 在不 activate 的状态下执行 virtualenv 环境的命令

我们执行 activate 命令启动 virtualenv 环境时,环境变量会被更新,此后执行命令时会优先使用 venv/bin/ 目录下的文件。实际上,即便是在没有执行 activate 命令的状态下,只要我们用完整路径执行了 venv/bin/ 下的命令,就可以运行 virtualenv 环境中的程序。

  1. $ python -c "import sys; print sys.executable"
  2. usrlocal/bin/python
  3.  
  4. $ venv/bin/python -c "import sys; print sys.executable"
  5. homebpbook/work/venv/bin/python
  6.  
  7. $ venv/bin/python
  8.  
  9. >>> import requests # 可以从venv环境中导入
  10.  
  11. $ venv/bin/pip install flask # 安装到venv环境中

因此,当我们想有计划地执行 venv 环境的程序,或者想启动服务器时,只要用完整路径指定 venv 环境的程序,就可以在不执行 activate 命令的状态下完成运行了。

virtualenv 的选项

virtualenv 为用户提供了许多可用选项。我们可以通过 virtualenv --help 来查看选项列表。现在我们选其中一些比较好用的选项来了解一下。

  • -p、—python

指定virtualenv环境下使用的Python,格式如--python=usrlocal/bin/python2.7

如果省略该选项,则默认选择执行virtualenv命令的Python。

  • —system-site-packages

使用当前Python主体上已经安装的程序库。如果省略该选项,则默认忽略Python主体上已经安装的程序库。

  • —always-copy

无论符号链接是否可用,一概不使用符号链接,而是直接复制文件。即便是允许使用符号链接的OS,在某些文件系统下仍会出现符号链接不可用的情况。此时就可以用到这个选项。

  • —clear

删除指定的virtualenv环境中安装的依赖库等,初始化环境。

  • -q、-quiet

执行virtualenv命令时,控制向工作台输出的信息量。

  • -v、-verbose

执行virtualenv命令时,增加向工作台输出的信息量。

LIST 3.9 virtualenv 的选项示例

  1. $ virtualenv -q --system-site-packages -p usrlocal/bin/python2.7 venv

如果我们经常用到某些选项,可以事先将其写入设置文件。这样一来,我们就不用每次执行命令时都再写一遍了。

在 Linux 系统下,设置文件默认使用 $HOME/.virtualenv/virtualenv.ini。举个例子,如果要在 virtualenv.ini 中指定 LIST 3.9 里的选项,我们可以按照 LIST 3.10 进行描述。

LIST 3.10 virtualenv.ini

  1. [virtualenv]
  2. python = usrlocal/bin/python2.7
  3. quiet = true
  4. system-site-packages = true

另外,还可以在环境变量中指定选项。如果我们同时用环境变量和设置文件指定了某个选项,那么将以环境变量的指定为准。当然,命令行传值参数的指定优先于一切。

环境变量名是根据选项名自动生成的。将选项名的“--”后的字母全改为大写,短线改为下划线,然后在开头加上 VIRTUALENV_,就是该选项所对应的环境变量。请各位在设置环境变量时记住这个规律。

● 示例

  1. --quiet -> VIRTUALENV_QUIET=true
  2. --python -> VIRTUALENV_PYTHON=usrlocal/bin/python2.7
  3. --system-site-packages -> VIRTUALENV_SYSTEM_SITE_PACKAGES=true

专栏 —system-site-packages 选项

virtualenv 环境不会引用 Python 主体上安装的程序包,也就是 usrlocal/lib/python2.7/sitepackages 目录下的程序包。因此,当我们新建了一个 virtualenv 环境时,这个环境处于干净的初始状态,没有安装任何多余的包。

相反地,如果我们想在 virtualenv 环境中使用 Python 主体上安装的程序包,那么在新建环境时可以加上 —system-site-packages 选项。

不过,一旦加上这个选项,我们就相当于同时使用两个环境,因此很难分辨出程序包在哪里,以及正在使用的是哪个程序包。这会导致 virtualenv 环境的优势大打折扣。所以,除非迫不得已,建议各位尽量不要使用 —system-site-packages 选项。

3.2.2 用 pip 安装程序包

NOTE

我们使用的是 pip 1.5.6。

给 virtualenv 环境额外安装程序包时,需要用该环境的 pip 命令。

pip 是用来安装程序包的命令。既可以经由网络进行安装,也可以直接从本地的程序包文件进行安装。第三方的程序库发布在 PyPI 上。pip 命令会默认从 PyPI 上搜索并安装程序包。

pip 提供了多个子命令。下面便是其子命令列表。

install安装程序包(指定包名、包文件名、URL 等)
uninstall卸载程序包
freeze以 requirements 格式列表输出当前已安装的程序包及其版本
list列表显示当前已安装的程序包
show列表显示当前已安装程序包的版本信息
search按指定关键字在 PyPI 上搜索程序包并显示结果列表
wheel根据指定 requirement 构建 wheel 文件
help显示帮助

接下来,我们将对 pip 的选项以及安装、卸载方法进行学习。其他详细内容请参考以下网站。

Reference Guide - pip 1.5.6 documentation

https://pip.pypa.io/en/latest/reference/index.html

User Guide - pip 1.5.6 documentation

https://pip.pypa.io/en/latest/user_guide.html#configuration

pip 的选项

pip 的选项有两种,一种是不依赖于子命令的共通选项,另一种是指定给子命令的选项。比如 —quiet 和 —proxy 就是所有命令共通的选项,而 install 子命令则拥有 —upgrade 等自己独有的选项(LIST 3.11)。

这两种选项都需要在执行时通过命令行指定。

LIST 3.11 pip 的选项示例

  1. $ pip --quiet --proxy=server:9999 install --upgrade requests

常用的选项可以事先写在设置文件中。这样可以省去每次都写的麻烦。

Linux 的默认设置文件为 $HOME/.pip/pip.conf。比如我们要在 pip.conf 中指定 LIST 3.11 pip 的选项示例中的选项,则可以按照 LIST 3.12 进行描述。

LIST 3.12 pip.conf

  1. [global]
  2. quiet = true
  3. proxy = server:9999
  4. [install]
  5. upgrade = true

NOTE

上述内容只是个例子,并非推荐设置。各位请严格按照自己的需要设置各个选项。

另外,还可以在环境变量中指定选项。如果我们同时用环境变量和设置文件指定了某个选项,那么将以环境变量的指定为准。当然,命令行传值参数的指定优先于一切。

环境变量名是根据选项名自动生成的。将选项名的“--”后的字母全改为大写,短线改为下划线,然后在开头加上 PIP_,就是该选项所对应的环境变量。请各位在设置环境变量时记住这个规律。

● 示例

  1. --quiet -> PIP_QUIET=true
  2. --proxy -> PIP_PROXY=server:9999
  3. --upgrade -> PIP_UPGRADE=true

专栏 关于指定HTTP 代理

pip 需要使用 HTTP 协议从外部网站获取程序包。在某些企业或环境下,我们需要经由代理才能访问到外部网站。而且有些时候,我们必须输入 ID 和密码才可以使用代理访问外部网站(认证代理)。

在这类环境下使用 pip 时,我们要用 —proxy 选项指定代理,格式为 [user:passwd@]proxy.server:port(LIST 3.13、LIST 3.14)。

LIST 3.13 指定 pip 的代理

  1. $ pip --proxy=proxy.example.com:1234 install requests

LIST 3.14 指定 pip 的认证代理

  1. $ pip --proxy=beproud:passwd@proxy.example.com:1234 install requests

我们也可以像 LIST 3.15 这样,在设置文件或环境变量中指定 —proxy 选项。

LIST 3.15 在 PIP_PROXY 环境变量中指定

  1. $ export PIP_PROXY=proxy.example.com:1234
  2. $ pip install requests

除 PIP_PROXY 环境变量外,我们还可以在 HTTP_PROXY 环境变量中指定 Proxy(LIST 3.16)。

LIST 3.16 在 HTTP_PROXY 环境变量中指定

  1. $ export HTTP_PROXY=proxy.example.com:1234
  2. $ pip install requests

安装程序包

pip install 是安装程序包的命令。install 子命令可以详细指定“安装什么”“从哪里安装”“如何安装”。因此,我们在看帮助文档时会发现它有很多选项和对象指定方法(LIST 3.17)。

LIST 3.17 pip install 的帮助

  1. $ pip install -h
  2. Usage:
  3. pip install [options] <requirement specifier> ...
  4. pip install [options] -r <requirements file> ...
  5. pip install [options] [-e] <vcs project url> ...
  6. pip install [options] [-e] <local project path> ...
  7. pip install [options] <archive url/path> ...
  8. ...

从 PyPI 安装程序包的方法如 LIST 3.18 所示。这里,我们可以指定一个或多个程序包。

LIST 3.18 从 PyPI 安装

  1. $ pip install requests
  2. $ pip install flask bottle

如果手边有源码,还可以像 LIST 3.19 这样进行安装。

LIST 3.19 从源码包安装

  1. $ pip install ./logfilter-0.9.2

像上例这样从本地目录安装时,我们需要写明本地路径,如 ./logfilter-0.9.2file://logfilter-0.9.2 等。如果只写 pip install logfilter-0.9.2,计算机会跑到 PyPI 上去找名为 logfilter-0.9.2 的程序包。

从源码安装时,我们可以使用 -e(—editable)选项。这样一来,安装时不会复制源码,而是直接在该目录下原地进行安装。对于一些尚在开发中的源码,用 editable 进行安装可以省去每次修改代码后重新安装的麻烦(LIST 3.20)。由于程序可以在运行时直接使用我们编辑过的代码,所以 editable 也被称为可编辑安装。

LIST 3.20 指定 editable 安装源码目录

  1. $ pip install -e ./logfilter-0.9.2

将版本库 clone 下来并进行安装时,需要在版本库的 URL 前添加例如 hg+ 等版本库类别,然后再执行 pip install(LIST 3.21)。在执行本例的过程中,需要在内部使用 hg 命令,因此必须有一个可以使用 hg 命令的环境。对象为 git 版本库时请将上述 hg 替换为 git

现在让我们来执行安装。

LIST 3.21 从 hg 上 clone 并安装

  1. $ pip install hg+https://bitbucket.org/shimizukawa/logfilter

如果想在 clone 来的源码上进行开发,我们可以同时加上 -e 选项。此时需要如 LIST 3.22 所示,在 URL 末尾添加 #egg=<< 程序包名 >>

LIST 3.22 从 hg 上 clone 并 editable 安装

  1. $ pip install -e hg+https://bitbucket.org/shimizukawa/logfilter#egg=logfilter

如果要一次安装多个程序包,比较简便的方法就是使用对象程序包以及描述这些程序包的 requirements 格式的文件。文件名一般使用 requirements.txt。我们可以自己动手准备这个文件,不过一般我们都会用 pip freeze 来输出。requirements.txt 文件用 -r(—requirement)选项指定(LIST 3.23)。

LIST 3.23 通过 requirements.txt 安装

  1. $ pip install -r requirements.txt

使用 pip 时,如果之前已经安装过指定的程序包,计算机并不会自动将其更新为新版本。指定更新到某一版本需要用 -U(—upgrade)选项(LIST 3.24)。

LIST 3.24 用 -U 选项更新版本

  1. $ pip install -U requests

如果不想每次都从 PyPI 下载程序包,我们可以用 —download-cache 选项指定缓存目录(LIST 3.25)。此时,为了检查版本,计算机仍会进行网络通信。如果缓存目录中有我们要用的版本,计算机则会直接用缓存文件进行安装。

LIST 3.25 指定下载缓存

  1. $ pip install --download-cache=~/.pip-cache requests

如果想一直使用缓存目录,建议各位在环境变量中设置该选项(LIST 3.26)。只要维持环境变量中的这一设置,我们就能有效运用缓存。

LIST 3.26 在环境变量中设置下载缓存

  1. $ export PIP_DOWNLOAD_CACHE=~/.pip-cache
  2. $ pip install requests

NOTE

从 pip-6.0 起,下载缓存改为默认有效。指定 —download-cache 选项时会出现无效警告。

记录程序包一览表

pip freeze 命令用来将已安装程序包及其版本的一览表以 requirements 格式输出。其执行如 LIST 3.27 所示。

LIST 3.27 pip freeze

  1. $ pip freeze
  2. Flask==0.10.1
  3. Jinja2==2.7.3
  4. MarkupSafe==0.23
  5. Werkzeug==0.9.6
  6. argparse==1.2.1
  7. itsdangerous==0.24
  8. wsgiref==0.1.2

NOTE

从 pip-6.0 起,不再显示 Python 标准库 argparse 和wsgiref。

pip freeze 以 requirements 格式输出程序包及其版本的列表。为了方便 pip install 使用这些内容,它们会被保存在 requirements.txt 文件中(LIST 3.28)。这个文件名并不是硬性要求,但大多数情况下我们习惯使用 requirements.txt。

LIST 3.28 将 pip freeze 的输出保存在 requirements.txt 中

  1. $ pip freeze > requirements.txt

这样一来,我们就可以用类似 pip install -r requirements.txt 的方式在其他环境中安装相同版本的程序包了。

如果环境中安装的程序包发生变动,我们可以再次执行 pip freeze > requirements.txt,以更新文件内容。此时,用 -r(—requirement)选项指定原来的 requirements.txt 可以确认更新前后的差别(LIST 3.29)。用这种方法能帮助我们确认是否存在意外更改。

LIST 3.29 pip freeze -r 的结果

  1. $ pip install bottle
  2. $ pip freeze -r requirements.txt
  3. Flask==0.10.1
  4. Jinja2==2.7.3
  5. MarkupSafe==0.23
  6. Werkzeug==0.9.6
  7. argparse==1.2.1
  8. itsdangerous==0.24
  9. wsgiref==0.1.2
  10. ## The following requirements were added by pip --freeze:
  11. bottle==0.12.7

专栏 在 requirements.txt 内指定版本库

如果我们在当前环境下用 -e(—editable)安装了版本库的代码,那么执行 pip freeze 后,输出结果如下。

  1. $ pip install -e hg+https://bitbucket.org/shimizukawa/logfilter#egg=logfilter
  2. $ pip freeze
  3. -e hg+https://bitbucket.org/shimizukawa/logfilter@96fd26dae42053c015d3285c
  4. 002d45aa5fe6e324#egg=logfilter-dev
  5. Flask==0.10.1
  6. Jinja2==2.7.3
  7. MarkupSafe==0.23
  8. Werkzeug==0.9.6
  9. itsdangerous==0.24
  10. wsgiref==0.1.2

该版本库特定版本的代码会被安装到使用 requirements.txt 进行 pip install 时的环境中。所以各位请注意,安装时会执行版本库 clone 操作,而且无论版本库是否接到了新的提交,安装时都会使用我们指定的版本。

卸载程序包

pip uninstall 命令用于卸载、删除当前已安装的程序包。其执行方式如 LIST 3.30 所示。

LIST 3.30 pip uninstall

  1. $ pip uninstall flask
  2. Uninstalling flask:
  3. homebpbook/work/venv/lib/python2.7/sitepackages/...
  4. ...
  5. Proceed (y/n)? y
  6. Successfully uninstalled flask

屏幕上会显示待删除文件的列表,并向用户询问是否真的要删除。此时输入 y 并按下 Enter 键即可完成删除操作。

如果不需要确认,可以加上 -y(—yes)选项,其执行如 LIST 3.31 所示。

LIST 3.31 pip uninstall -y

  1. $ pip uninstall -y flask

请注意,pip uninstall 命令只会卸载我们指定的程序包。比如安装 Flask 时会捆绑安装 Jinja2 等 4 个关联程序包,这些程序包在 Flask 被卸载之后仍会残留在环境中。如果我们不再需要这些程序包,就必须通过 pip uninstall 手动删除它们,或者直接重建 virtualenv 环境,选我们需要的程序包重新安装。

3.2.3 小结

开发 Python 项目时,我们可以用 virtualenv 搭建该项目专用的虚拟环境。有了这个环境,我们使用程序库时就不会影响到其他项目了。项目所需的程序库用 pip 进行安装。用 pip freeze 命令搭建某项目环境的 requirements.txt 文件之后,我们能够轻松重建该项目所需的环境。除此之外,部署等方面也有 pip 和 virtualenv 的用武之地。这两个工具是 Python 开发环境的基础,建议各位牢记它们的使用方法。

3.3 文件结构与发布程序包

编写完程序之后,我们还要面对从封装成包到发布的复杂流程。在本部分中,我们会尝试把第 2 章中开发的留言板应用放到 PyPI 上进行公开,并在此过程中学习一下 setup.py 的写法以及如何向 PyPI 上传程序包等。

3.3.1 编写 setup.py

首先我们来了解一下 setup.py 的功能。Python 的封装离不开 setup.py。将开发完毕的程序封装成包,可以方便其他用户或其他项目拿去用。而封装的绝大部分时间都要消耗在编写 setup.py 上。

我们用 setup.py 来设置 Python 程序包的信息(元数据)、定义程序包。setup.py 这个文件名是 Python 中定好了的,不可以更改。我们要在这个文件中定义程序包名称、包及依赖包的信息等元数据。

有了 setup.py 之后,我们便能在命令行中执行诸如 python setup.py sdistpython setup.py install 等与包相关的操作了。另外,将程序包注册到 PyPI 的操作也需要通过 setup.py 进行。

setup.py 的命令

首先,我们来做出一个能运行的 setup.py 空壳(LIST 3.32)。

LIST 3.32 setup.py

  1. from setuptools import setup
  2. setup(name='guestbook')

现在计算机已经可以执行 setup.py 的命令了。各位可以通过 —help-commands 选项查看 setup.py 提供的命令(LIST 3.33)。我们在下面列举了一些有代表性的命令。命令后面的英文注释已经译为了中文。

LIST 3.33 setup.py 的指令一览

  1. $ python setup.py --help-commands
  2. 标准命令
  3. build 构建安装所需的全部内容
  4. clean 删除 'build' 命令创建的所有临时文件
  5. install 安装 build 目录下的全部内容
  6. sdist 创建源码包(以 tarzip 等格式)
  7. register 将程序包注册到 Python Package Index
  8. bdist 创建二进制包
  9. bdist_dumb 创建 'dumb' 格式的二进制包
  10. bdist_rpm 创建 RPM 格式的二进制包
  11. bdist_wininst 创建面向 MS Windows 的安装包
  12. upload 将二进制包上传至PyPI
  13. check 检查程序包的设置值是否正确
  14. 扩展命令
  15. develop 以开发模式安装程序包
  16. setopt setup.cfg 等文件中记录一个选项
  17. saveopts 将给定的多个选项记录在 setup.cfg 等文件中
  18. upload_docs PyPI 上传文档
  19. alias 定义快捷命令
  20. bdist_egg 创建 'egg' 格式的程序包
  21. test 原地构建后运行 Unit Test

下面我们来创建最基本的源码包。源码包需要通过 python setyp.py sdist 命令创建(LIST 3.34)。

LIST 3.34 python setup.py

  1. $ python setup.py sdist
  2. running sdist
  3. running egg_info
  4. -( 中间省略)-
  5. warning: sdist: standard file not found: should have one of README, README.rst, README.txt
  6. running check
  7. warning: check: missing required metadata: url
  8. warning: check: missing metadata: either (author and author_email) or (maintainer and maintainer_email) must be supplied
  9. creating guestbook-0.0.0
  10. -( 中间省略)-
  11. creating dist
  12. Creating tar archive
  13. removing 'guestbook-0.0.0' (and everything under it)
  14. $ ls dist/
  15. guestbook-0.0.0.tar.gz

现在,dist 目录下已经生成了 guestbook-0.0.0.tar.gz 文件。这个 tar.gz 文件目前只包含 setup.py。

另外,我们在执行过程中看到了几个 warning。这些 warning 指出的项目最好都设置一下。后面我们会学习如何进行设置。

3.3.2 留言板的项目结构

首先,我们来了解一下 Python 项目一般的目录结构。当封装对象只有一个“.py”文件时,其结构如 LIST 3.35 所示。

LIST 3.35 项目内只有一个文件时的结构示例

  1. homebpbook/projectname
  2. +-- MANIFEST.in
  3. +-- README.rst
  4. +-- packagename.py
  5. +-- setup.py

如果封装对象目录下包含多个“.py”或模板等文件,则结构如 LIST 3.36 所示。

LIST 3.36 项目内含多个文件时的结构示例

  1. homebpbook/projectname
  2. +-- MANIFEST.in
  3. +-- README.rst
  4. +-- packagename/
  5. | +-- _init_.py
  6. | +-- module.py
  7. | +-- templates/
  8. | +-- index.html
  9. +-- setup.py

关于这方面,我们的留言板应用由下述文件组成。

文件路径 说明
guestbook.py 服务器程序
guestbook.dat 提交数据文件
static/main.css CSS 文件
templates/index.html 输出 HTML 的模板,用于显示“提交 / 留言列表”的页面

虽然“.py”文件只有一个,但 static 和 templates 目录下都包含文件。由于我们介绍的前一种项目结构无法安装模板等文件,因此这里需要使用后一种项目结构。

文件最终的安排如 LIST 3.37 所示。

LIST 3.37 留言板项目的目录结构

  1. homebpbook/guestbook/
  2. +-- LICENSE.txt
  3. +-- MANIFEST.in
  4. +-- README.rst
  5. +-- guestbook
  6. | +-- _init_.py
  7. | +-- static/main.css
  8. | +-- templates/index.html
  9. +-- setup.py

现在我们来创建 guestbook 目录,将 guestbook.py 文件移动到该目录下并重命名为“ init.py”(init 前后各两个半角下划线)。另外,templates 和 static 目录也要移动到 guestbook 目录下。guestbook.dat 不是我们要发布的东西,所以这里不需要它。

接下来,我们来实际应用这个封装用的结构。

3.3.3 setup.py 与 MANIFEST.in——设置程序包信息与捆绑的文件

接下来我们将在 setup.py 中设置程序包的信息,然后在 MANIFEST.in 中指定捆绑的文件。那么,我们先来按照顺序了解一下。

setup.py

首先,我们像 LIST 3.38 这样描述 guestbook 项目的 setup.py。

LIST 3.38 最低限度内容的 setup.py

  1. from setuptools import setup, find_packages
  2. setup(
  3. name='guestbook',
  4. version='1.0.0',
  5. packages=find_packages(),
  6. include_package_data=True,
  7. install_requires=[
  8. 'Flask',
  9. ],
  10. )

如果一个环境能使用 pip,那么该环境中一定安装了 setuptools 库。虽然用 from distutils.core import setup 这种 Python 标准写法也没有问题,但一般情况下我们习惯使用 setuptools 提供的含有扩展功能的 setup 函数。

下面来了解一下各个参数的意义。

  • name

程序包的名称。这里我们定为'guestbook'。一般情况下,包名都与项目名称一致。但是,用于发布的程序包需要有一个独特的名称,以防止与其他程序包名撞车。实际上,guestbook这个名称实在不够独特。因此,如果一定要使用这个名称,最好在前面加上组织名等,例如beproud.guestbook。

  • version

代表版本号的字符串。这里我们定为'1.0.0'。

  • packages

指定所有捆绑的Python程序包(可以用python命令import的目录名)。举个例子,如果一个项目包含多级目录,那么我们需要用下例所示的方法,列表指定所有程序包。

  1. packages=[
  2. 'guestbook', 'guestbook.server', 'guestbook.server.dir',
  3. 'guestbook.storage', ...
  4. ],

find_packages()函数可以自动搜索当前目录下的所有Python程序包并返回程序包名。有了它,我们便可以省去一个个列举的麻烦。

NOTE

如果项目仅由一个“.py”文件构成,那么要用 py_modules 代替 packages 传值参数,在 py_modules 中指定对象模块名。

  • include_package_data

在packages指定的Python包(目录)中,除“.py”之外的文件都称为程序包资源。这个设置用来指定是否安装Python包中所含的程序包资源。

这里我们要安装templates和static这两个程序包资源,所以将它们指定为True

不过,这一设置并不能将程序包资源与我们要发布的程序包捆绑在一起。捆绑的方法将在MANIFEST.in中学习。

  • install_requires

列表指定依赖包。留言板应用要依赖Flask,所以我们在这里指定Flask。与requirements.txt不同,这里一般不指定版本。

MANIFEST.in

为将 HTML 文件、CSS 文件等程序包资源与程序包捆绑在一起,我们需要用 MANIFEST.in 来指定封装对象文件。

这里我们在 setup.py 所在的目录下创建 MANIFEST.in 文件,指定封装对象文件的范围(LIST 3.39)。

LIST 3.39 MANIFEST.in

  1. recursive-include guestbook .html .css

recursive-include 表示捆绑指定目录下所有与指定类型一致的文件。以 LIST 3.39 为例,我们捆绑了 guestbook 目录下所有与“.html”和“.css”一致的文件。

现在我们希望使用这个程序包的环境能安装这些捆绑好的程序包资源。为此,我们需要将前面提到的 install_package_data 指定为 True,这一点千万不能忘。

MANIFEST.in 还可以指定捆绑 guestbook 应用不使用的非程序包资源文件,比如 LICENSE.txt。在发布程序包时最好把许可文件也捆绑进去。

假设我们使用了 BSD 许可,并在 LICENSE.txt 文件中描述了许可条款。接下来,我们需要在 MANIFEST.in 里添加对它的捆绑指定(LIST 3.40)。

LIST 3.40 MANIFEST.in

  1. recursive-include guestbook .html .css
  2. include LICENSE.txt

include 会捆绑所有与指定类型一致的文件。所以添加了 LIST 3.40 中所示的指定语句后,LICENSE.txt 文件就和程序包捆绑在了一起。另外,我们要安装的是 guestbook 目录,而 LICENSE.txt 文件并不在该目录下,所以 LICENSE.txt 文件并不会被安装到使用该程序包的环境中。

MANIFEST.in 有许多种描述方式,不但可以将某个扩展名的文件全部捆绑起来,还可以剔除特定扩展名的全部文件。MANIFEST.in 的详细描述方法请查阅 Python 的参考手册。

Creating a Source Distribution - Python 2.7.12 documentation

https://docs.python.org/2.7/distutils/sourcedist.html

确认运行情况

为查看前面的设置是否正确,我们需要搭建一个用来开发程序包的 virtualenv 环境并安装该程序包。这里我们用“.venv”作为 virtualenv 环境的目录名(LIST 3.41)。

LIST 3.41 搭建 virtualenv 环境及安装

  1. $ cd ..
  2. $ virtualenv .venv

此时的目录结构如 LIST 3.42 所示。

LIST 3.42 目录结构

  1. homebpbook/guestbook/
  2. +-- .venv/
  3. +-- LICENSE.txt
  4. +-- MANIFEST.in
  5. +-- guestbook
  6. | +-- _init_.py
  7. | +-- static/main.css
  8. | +-- templates/index.html
  9. +-- setup.py

然后我们启动 virtualenv 环境并执行安装。在安装时请加上 -e(—editable)选项进行原地安装(在原目录下直接转为安装状态)(LIST 3.43)。这样一来,我们在开发过程中就不用每改一次都重新安装一遍了。

LIST 3.43 搭建 virtualenv 环境及 editable 安装

  1. $ source .venv/bin/activate
  2. (.venv)$ pip install -e .
  3. Obtaining file://homebpbook/guestbook
  4. Running setup.py (path:homebpbook/guestbook/setup.py) egg_info for package from file://homebpbook/guestbook
  5. Installing collected packages: guestbook
  6. -(中间省略:安装依赖包)-
  7. Running setup.py develop for guestbook
  8. Creating homebpbook/.venv/lib/python2.7/sitepackages/guestbook.egg-link(link to .)
  9. Adding guestbook 1.0.0 to easy-install.pth file
  10. Installed homebpbook/guestbook
  11. -(中间省略)-
  12. Successfully installed guestbook
  13. Cleaning up...
  14. (.venv)$ pip freeze
  15. Flask==0.10.1
  16. Jinja2==2.7.3
  17. MarkupSafe==0.23
  18. Werkzeug==0.9.6
  19. guestbook==1.0.0
  20. itsdangerous==0.24

现在,guestbook-1.0.0 已经安装到 virtualenv 环境中了。我们可以看到,记录程序包元数据位置的 guestbook.egg-link 文件被安装到 virtualenv 环境中了。easy-install.pth 文件中添加了 guestbook 的源码位置。另外,Flask 及其相关程序包也都安装好了。

这样一来,我们在其他 PC 或服务器上构建环境时,就不必再去一个个地安装依赖包了。如果今后需要添加或更改依赖库,各位只要按照前面讲的流程更新 setup.py,然后再执行一次 pip install 即可。

3.3.4 setup.py——创建执行命令

我们在第 2 章开发的留言板是一个直接从 Python 启动的脚本。要想让下载它的人用起来更方便,那最好生成一些用户命令。这里我们通过设置 setup.py,让其自动生成 guestbook 命令(LIST 3.44)。

LIST 3.44 让 setup.py 生成命令

  1. from setuptools import setup
  2. setup(
  3. name='guestbook',
  4. version='1.0.0',
  5. packages=find_packages(),
  6. include_package_data=True,
  7. install_requires=[
  8. 'Flask',
  9. ],
  10. entry_points="""
  11. [console_scripts]
  12. guestbook = guestbook:main
  13. """,
  14. )

我们在 setup.py 中添加了 entry_points。这样一来,在安装程序包时就会自动生成 guestbook 命令。用户执行 guestbook 命令时将会调用 guestbook 模块的 main 函数。

但是 guestbook/init.py 中还没有 main 函数,所以我们需要添加这个函数,具体代码如 LIST 3.45 所示。

LIST 3.45 guestbook/init.py

  1. def main():
  2. application.run('127.0.0.1', 8000)
  3. if __name__ == '__main__':
  4. # 在IP 地址127.0.0.1 的8000 端口运行应用程序
  5. application.run('127.0.0.1', 8000, debug=True)

然后我们再次执行安装命令,看看是否能生成 guestbook 命令。

即便是在 editable 安装的状态下,如果想反映出对元信息进行的修改(比如添加命令、更改依赖库等),也需要重新执行一次安装命令(LIST 3.46)。

LIST 3.46 重新安装

  1. (.venv)$ pip install -e ./guestbook
  2. -(中间省略)-
  3. Successfully installed guestbook
  4. Cleaning up...
  5. (.venv)$ ls .venv/bin/guestbook
  6. guestbook
  7. (.venv)$ guestbook
  8. Running on http://127.0.0.1:8000/
  9. Restarting with reloader

可以看到,guestbook 命令已经成功生成,而且可以正常运行了。

3.3.5 python setup.py sdist——创建源码发布程序包

创建用于发布的程序包时,需要如 LIST 3.47 所示,执行 python setup.py sdist 命令。

LIST 3.47 python setup.py sdist

  1. $ python setup.py sdist
  2. running sdist
  3. running egg_info
  4. writing requirements to guestbook.egg-info/requires.txt
  5. writing guestbook.egg-info/PKG-INFO
  6. writing top-level names to guestbook.egg-info/top_level.txt
  7. writing dependency_links to guestbook.egg-info/dependency_links.txt
  8. reading manifest file 'guestbook.egg-info/SOURCES.txt'
  9. reading manifest template 'MANIFEST.in'
  10. writing manifest file 'guestbook.egg-info/SOURCES.txt'
  11. running check
  12. creating guestbook-1.0.0
  13. -( 中间省略)-
  14. making hard links in guestbook-1.0.0...
  15. hard linking LICENSE.txt -> guestbook-1.0.0
  16. -( 中间省略)-
  17. Creating tar archive
  18. removing 'guestbook-1.0.0' (and everything under it)
  19. $ ls dist/
  20. guestbook-1.0.0.tar.gz

这样,我们就在 dist 目录下生成了 guestbook-1.0.0.tar.gz。这个 tar.gz 文件中包含 guestbook/init.py、setup.py、LICENSE.txt、HTML、CSS 等文件。

现在只要将这个文件放到我们想安装应用的环境中,就可以运行 pip install guestbook-1.0.0.tar.gz,直接从文件进行安装了。

3.3.6 提交至版本库

我们先将前面的内容提交到版本库。关于 hg 命令的操作,第 1 章和第 6 章中有详细介绍。

目前的目录结构如 LIST 3.48 所示。

LIST 3.48 留言板项目的目录结构

  1. homebpbook/guestbook/
  2. +-- .venv/
  3. +-- LICENSE.txt
  4. +-- MANIFEST.in
  5. +-- guestbook
  6. | +-- _init_.py
  7. | +-- static/main.css
  8. | +-- templates/index.html
  9. +-- guestbook.dat
  10. +-- guestbook.egg-info/
  11. +-- setup.py

开发 Python 项目时,我们习惯将 setup.py 放在版本库最初级目录(根目录)下。这样我们就能用 pip 直接从版本库进行安装了。

另外,有些文件和目录是不用保存到版本库中的。guestbook.dat 文件的作用是记录留言板接收到的数据,这些数据没必要记录到版本库里。

guestbook.egg-info 目录的作用是记录程序包的元数据。元数据将在执行 pip install -e . 时自动生成。如果缺乏元数据,editable 安装可能无法正常进行。不过,由于它是在安装时自动生成的,所以也不用保存到版本库。

“.venv”也可以重新生成,因此不必保存到版本库。

接下来,我们需要将除上述三者以外的文件提交给版本库(LIST 3.49)。

LIST 3.49 注册到版本库

  1. $ cd ~/guestbook
  2. $ hg init
  3. $ hg add LICENSE.txt MANIFEST.in guestbook setup.py
  4. $ hg ci -m "initial"

另外,如果在目前的状态下执行 hg status 命令,刚才那些不需要上传的文件和目录仍会显示为非管理对象文件。我们需要在“.hgignore”文件中添加设置,将这些不需要管理的文件剔除出显示对象(LIST 3.50)。

LIST 3.50 .hgignore

  1. .*.egg-info
  2. ^guestbook.dat$
  3. ^.venv$

“.hgignore”文件也要提交上去(LIST 3.51)。这样一来,在其他环境中使用 clone 的版本库时也就不会显示这些文件了。

LIST 3.51 提交“.hgignore”

  1. $ hg add .hgignore
  2. $ hg ci -m "add ignore list"

我们先暂且将其 push 到版本库服务器上。各位请在 Bitbucket 上创建一个空的 guestbook 项目,然后执行 LIST 3.52 所示的命令。

LIST 3.52 hg push

  1. $ hg push https://bitbucket.org/< 你的Bitbucket 账户>/guestbook

建议各位今后适时地将添加、修改过的源码提交到版本库中。

专栏 Bitbucket

Bitbucket 是 Mercurial 的版本库服务器。Bitbucket 为用户提供了许多免费功能,可以管理 Mercurial 和Git 版本库。

各位请先注册账户,创建空的 Mercurial 版本库,然后执行 LIST 3.53 所示的命令,完成 push 操作(本例中的用户名为 beproud,版本库名为 guestbook)。

LIST 3.53 hg push 示例

  1. $ hg push https://bitbucket.org/beproud/guestbook

3.3.7 README.rst——开发环境设置流程

下面我们来描述设置流程说明书,总结该留言板应用开发环境的搭建流程。我们前面讲到的流程如下。

① clone 项目的版本库

② 搭建项目专用的 virtualenv 环境

③ 在 virtualenv 环境内执行 pip install (如果用于开发,则执行 pip install -e

LIST 3.54 设置流程

  1. $ hg clone https://bitbucket.org/beproud/guestbook
  2. $ cd guestbook
  3. $ virtualenv .venv
  4. $ source .venv/bin/activate
  5. (.venv)$ pip install .
  6. (.venv)$ guestbook
  7. * Running on http://127.0.0.1:5000/

我们来把 LIST 3.54 中的流程原封不动地写入 README.rst。扩展名为 .rst 的文件是用 reStructuredText(reST)语法描述的文本文件。一般说来,Python 项目都会选用 reST 语法来写 README.rst。关于 reST 语法,我们将在第 7 章中详细了解。

通常,README.rst 包含 LIST 3.55 所示内容即可。

LIST 3.55 README.rst

  1. ===================
  2. 留言板应用
  3. ===================
  4. 目的
  5. =====
  6. 练习开发通过 Web 浏览器提交留言的 Web 应用程序
  7. 工具版本
  8. ====================
  9. :Python: 2.7.8
  10. :pip: 1.5.6
  11. :virtualenv: 1.11.6
  12. 安装与启动方法
  13. =======================
  14. 从版本库获取代码,然后在该目录下搭建 virtualenv 环境::
  15. $ hg clone https://bitbucket.org/beproud/guestbook
  16. $ cd guestbook
  17. $ virtualenv .venv
  18. $ source .venv/bin/activate
  19. (.venv)$ pip install .
  20. (.venv)$ guestbook
  21. * Running on http://127.0.0.1:5000/
  22. 开发流程
  23. =========
  24. 用于开发的安装
  25. ------------------
  26. 1. 检测
  27. 2. 按以下流程安装
  28. (.venv)$ pip install -e .

写完之后要记得将 README.rst 文件提交到版本库。

各位请注意,我们在安装流程中写的是直接安装,但在开发流程中写的却是用 pip install -e 进行安装。而关于这二者的区别,我们并没有在 README.rst 文件中提及。这是因为我们认为阅读这篇文档的人应该懂得如何使用 pip 的 -e 选项,知道有它和没它的不同。使用普及率较高的工具或选项的优势就在于此。即便我们阅读文档时不知道它是什么,也能立刻查到相关资料,或者根据类似知识进行摸索。

专栏 缩短、定型化环境的搭建流程

时间一久,就算是自己开发的项目,我们也会忘记如何搭建运行环境。所以为了将来不忘记,我们最好在文档的开头就记下运行程序之前所需的全部流程。另外,尽量能让自己在看文档时立刻回想起当时用了什么流程。因此,流程要尽量短,而且要用开发者们普遍采用的结构。

以常用命令定型化的简洁流程具有以下优势。

  • 不容易出现键入错误、流程颠倒等人为失误

  • 减少整个项目中需要记忆的东西

  • 需要向其他开发者或使用者传递的信息更少,减少文档量

  • 测试和部署更容易自动化

3.3.8 变更依赖包

留言板的依赖包是 Flask。但是,我们很难在开发初期就确定好一款应用程序内的所有依赖包,有些时候还会放弃当前的包而改用其他的。特别是周期短、发布频繁的项目,往往每发布一次都会变更一次依赖包。

举个例子,假设我们放弃 Flask 改用 Bottle。这时如果直接用 pip 命令安装了 Flask 或 Bottle,那就必须将这一步骤告知其他开发者甚至是未来的自己(LIST 3.56)。

LIST 3.56 用 pip 替换了程序包,这一步该如何告知其他人

  1. (.venv)$ pip uninstall flask
  2. (.venv)$ pip install bottle

留言板的 setup.py 里记录着依赖包的信息,因此我们只需更改 setup.py 的设置即可。如果改写了 setup.py 的 install_requires 行,需要再次执行 pip install -e .

这一步骤的命令和安装时的命令一样,因此不需要修改流程说明书。只要其他新建项目环境的开发者执行了 pip install -e . 命令,就能安装好该项目所需的全部程序包。

不过,还有一点需要注意。那就是,即使我们从 setup.py 中删除了 flask,之前安装到环境中的 Flask 及其关联程序包也不会被卸载。要想删除已经无用的程序包,需要用 virtualenv --clear . 等方法重建环境(LIST 3.57)。

LIST 3.57 重建环境

  1. (.venv)$ virtualenv --clear .venv # 删除.venv 环境内的全部依赖库
  2. (.venv)$ pip install -e . # 根据./setup.py 安装依赖库

这一处理会重新安装依赖库,所以运行时将占用较长时间。

NOTE

建议各位设置 pip 的 —download-cache 选项,缩短下载时间(pip-6.0 之前的版本)。使用第 9 章中介绍的 wheelhouse 能进一步加快速度。

NOTE

关于如何固定开发环境中安装的程序包的版本,各位请参考第 9 章。另外,第 9 章还会讲解强制指定依赖包范围的相关知识。

另外,最好在 README.rst 中添加 LIST 3.58 所示的流程。

LIST 3.58 README.rst

  1. 开发流程

变更依赖库时

  1. 更新 setup.pyinstall_requires
  2. 按以下流程更新环境::

    (.venv)$ virtualenv —clear .venv (.venv)$ pip install -e ./guestbook

  3. 将 setup.py 提交到版本库

3.3.9 通过 requirements.txt 固定开发版本

前面我们介绍了用 setup.py 管理依赖包的方法。实际上,我们还可以用 requirements.txt 管理依赖库。

setup.py 是在 PyPI 上发布程序包时必不可少的组成部分,而且安装时的依赖库也需要用 setup.py 的 install_requires 来指定。这种情况下,由于使用该包的各个环境大不相同,所以我们不能严格指定依赖库的版本,只能指定最低需求。当然,我们还可以手动编辑 requirements.txt,免除指定版本的工作。但要知道,pip install guestbook 是不会引用 requirements.txt 的,就算我们将 requirements.txt 与发布的程序包捆绑在了一起,计算机仍然不会自动安装依赖库。

相反,如果我们的项目不需要封装,只是被拿来当作一个 Web 应用在服务器上发布,就没必要使用 setup.py 了。在项目从开发到正式上线的过程中,有许多程序库和应用程序要一个版本用到底,因此我们要严格地指定版本。而对于不需要发布也不需要封装的项目,setup.py 就失去了用处。对这些项目而言,用 requirements.txt 效率更高。

创建 requirements.txt 的命令如 LIST 3.59 所示。

LIST 3.59 创建 requirements.txt

  1. (.venv)$ pip freeze > requirements.txt

requirements.txt 中记载着当前环境内已安装的所有程序包及明确的版本号(LIST 3.60)。

LIST 3.60 requirements.txt

  1. Flask==0.10.1
  2. Jinja2==2.7.3
  3. MarkupSafe==0.23
  4. Werkzeug==0.9.6
  5. guestbook==1.0.0
  6. itsdangerous==0.24

用 setup.py 管理依赖包时,我们只写了 Flask 但没有指定版本。这是 setup.py 管理和 requirements.txt 管理的一大区别。

要想在其他环境安装同样的程序包们,我们需要将这个 requirements.txt 文件放到该环境下,然后用如 LIST 3.61 所示的方法安装。

LIST 3.61 用 requirements.txt 进行安装

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

这样一来,环境中就安装了同样版本的程序包。

现在将创建好的 requirements.txt 文件也提交到版本库。另外,当我们变更依赖包时要记得更新这个文件。请各位打开 README.rst 文件,将变更依赖库时的流程更新成如 LIST 3.62 所示的内容。

LIST 3.62 README.rst

  1. 开发流程

变更依赖库时

  1. 更新 setup.pyinstall_requires
  2. 按以下流程更新环境::

    (.venv)$ virtualenv —clear .venv (.venv)$ pip install -e ./guestbook (.venv)$ pip freeze > requirements.txt

  3. 将 setup.py 和 requirements.txt 提交到版本库

在这个流程中,依赖包同时被 setup.py 和 requirements.txt 两个文件管理着。至于该用 setup.py 管理还是 requirements.txt 管理,要视项目的公开方式或使用方式而定。

3.3.10 python setup.py bdist_wheel——制作用于 wheel 发布的程序包

接下来,我们制作 wheel 程序包。wheel 程序包的使用方法在 9.1 节有详细讲解。

制作 wheel 程序包之前,我们先安装 wheel(LIST 3.63)。

LIST 3.63 安装 wheel

  1. $ pip install wheel
  2. Downloading/unpacking wheel
  3. Downloading wheel-0.24.0-py2.py3-none-any.whl (63kB): 63kB downloaded
  4. Installing collected packages: wheel
  5. Successfully installed wheel
  6. Cleaning up...

安装完成之后,我们就可以用 bdist_wheel 命令了。接下来,我们执行 python setup.py bdist_wheel 来生成 wheel 程序包(LIST 3.64)。

LIST 3.64 生成 wheel 程序包

  1. $ python setup.py bdist_wheel
  2. running bdist_wheel
  3. -( 中间省略)-
  4. creating build/bdist.linux-x8664/wheel/guestbook-1.0.0.dist-info/WHEEL
  5. $ ls dist/
  6. guestbook-1.0.0-py2-none-any.whl guestbook-1.0.0.tar.gz

执行完之后,dist 目录下就会生成 guestbook-1.0.0-p2-none-any.whl。这个扩展名为“.whl”的文件就是 wheel 程序包。在 wheel 程序包内,按照安装后的目录结构捆绑了源码和各种文件。与源码程序包不同,它里面没有 setup.py。

现在只要将这个文件复制到等待安装的环境中,我们就可以执行 pip install guestbook-1.0.0-p2-none-any.whl,直接从文件进行安装了。由于这时不需要运行 setup.py,所以会比源码程序包的安装速度快出一大截。

专栏 Universal Wheel:同时支持 Python2 和 Python3 的 wheel 程序包

我们将Python2 系列和 Python3 系列都可以用的 wheel 程序包称为 Universal Wheel。

刚才我们生成的程序包是 guestbook-1.0.0-p2-none-any.whl,从名字上就可以看出,这个程序包是对应 Python2 的。由于 guestbook 同时支持 Python3,所以我们在 Python3 下执行上述流程时,会生成名为 guestbook-1.0.0-p3-none-any.whl 的 wheel 文件。

如果想生成 Universal Wheel 程序包,就需要在 bdist_wheel 后面加上 —universal 选项,代码如下。

  1. $ python setup.py bdist_wheel universal
  2. —(中间省略)—
  3. $ ls dist
  4. guestbook-1.0.0-py2.py3-none-any.whl

当程序包仅由纯 Python 代码实现时,会生成 Universal Wheel。而在诸如需要二进制构建、Python 实现方面受限等情况下,则无法生成 Universal Wheel。

3.3.11 上传到 PyPI 并公开

我们之所以能用 pip 命令安装指定的程序包,是因为这些包都被注册到了 PyPI 上。PyPI 是 Python 的官方网站,所有人都能随意上传及下载 Python 程序包。如果各位不介意公开自己开发的程序包,不妨将它注册到 PyPI 上。

举个例子,如果我们要安装一个已经在 PyPI 上注册的程序包 bpmappers,那么只需执行 pip install bpmappers 即可。

PyPI 的作用相当于一台负责分发程序包的中央服务器。不过,注册到 PyPI 的程序包无法只对特定用户公开,所以各位要千万注意,别把对外保密的程序库注册上来。

NOTE

在这种情况下,我们可以在公司内部准备一台 PyPI 交换服务器,或者找其他等价的方法。这类方法我们将在第 9 章中详细介绍。

下面我们就把已做好的程序包文件注册到 PyPI。如果想在实际注册之前先注册到测试服务器,可以参考本节的专栏“PyPI 的测试服务器”。

执行 register 命令,注册 guestbook 程序包(LIST 3.65)。

LIST 3.65 注册程序包

  1. $ python setup.py register

如果发生下述情况,执行 register 命令时会被询问是否拥有 PyPI 账户。

  • 该环境第一次执行 register 命令

  • 保存账户信息的 .pypirc 文件无效

发生上述情况时,我们会接到如 LIST 3.66 所示的账户询问信息。

LIST 3.66 注册程序包时的账户询问

  1. $ python setup.py register
  2. running register
  3. ...
  4. We need to know who you are, so please choose either:
  5. 1. use your existing login,
  6. 2. register as a new user,
  7. 3. have the server generate a new password for you (and email it to you), or
  8. 4. quit
  9. Your selection [default 1]:

如果各位已经有 PyPI 账户,请选 1 并输入用户名和密码。如果没有,则需要先去 PyPI 网站创建账户之后再选 1,要么就是直接选择 2 或 3。这步操作会认证我们的 PyPI 账户,只要认证成功,我们就可以使用 registerupload 命令操作 PyPI 了。

专栏 在.pypirc 上保存密码时的注意事项

在输入完用户信息、即将完成注册时,系统会询问是否将登录信息保存在主目录的“.pypirc”文件中。如果输入Y,计算机会自动生成“.pypirc”文件,以纯文本形式保存用户名和密码。因此,我们最好采取一些对策(比如设置权限等),防止“.pypirc”文件的内容被第三者窃取。

另外,从 Python 2.7 开始,我们可以用编辑器将保存后的“.pypirc”文件的 password 栏设置为空栏。此后再进行上传时,只要用 register upload 命令代替单独的 upload 命令,就可以做到仅执行时验证密码。

完成注册之后,就可以向 PyPI 上传程序包了。执行下述命令之后,源码程序包就会被上传至 PyPI。

  1. $ python setup.py sdist bdist_wheel upload

刚才我们单独执行了 sdist 命令和 bdist_wheel 命令。其实如上例所示,只要在命令末尾指定 upload 命令,就可以在封装 sdist 和 bdist_wheel 程序包之后直接将它们设为上传对象。

绝大部分情况下,一个项目每次发布的程序包类型都基本一致。因此,我们可以把注册新版本、构建程序包、上传这一系列流程整合成一个命令。整合多条命令时,需要用到 alias 功能,代码如下。

  1. $ python setup.py alias release register sdist bdist_wheel upload
  2. $ python setup.py release

alias 命令会把设置保存在 setup.cfg 中,所以我们要把这个文件也提交到版本库里。共享 alias 可以一定程度上避免项目其他成员在发布时出现失误。

专栏 PyPI 的测试服务器

如果严格执行本书中的流程,各位的 guestbook 程序包就会被实际注册到 PyPI 上。但这毕竟是一个练习,我们不建议各位向 PyPI 服务器注册这些练习性质的东西(请在 PyPI 网站搜索 printer)。

所以,请各位在练习时使用 PyPI 的测试服务器 TestPyPI2。TestPyPI 是一个对所有人开放的服务器,专门用来供人们做实验。我们可以用它来练习向 PyPI 上传程序包以及从 PyPI 下载程序包。

具体使用方法请参考 TestPyPI 的说明页面3。

2https://testpypi.python.org/pypi

3https://wiki.python.org/moin/TestPyPI

描述程序包的详细信息

上传完成后,PyPI 会给该程序包分配一个 URL。例如 guestbook 程序包的 URL 是 https://pypi.python.org/pypi/guestbook。另外,setup.py 的 long_description 传值参数所指定的内容将会显示在 PyPI 页面上。

以刚才编写完成的 setup.py 为例来看,我们会发现 PyPI 页面上只显示了下载文件的列表,并没有任何详细说明。这是因为我们并没有指定 long_description。要知道,除了下载列表之外,还有很多对使用者有帮助的信息,所以我们应该将这些信息描述在 setup.py 中,让人们能在 PyPI 上看到它们。下面是一个描述示例。

  1. import os
  2. from setuptools import setup, find_packages
  3. def read_file(filename):
  4. basepath = os.path.dirname(os.path.dirname(__file__))
  5. filepath = os.path.join(basepath, filename)
  6. if os.path.exists(filepath):
  7. return open(filepath).read()
  8. else:
  9. return ''
  10. setup(
  11. name='guestbook',
  12. version='1.0.0',
  13. description='A guestbook web application.',
  14. long_description=read_file('README.rst'),
  15. author='< 你的名字>',
  16. author_email='< 你的邮箱地址>',
  17. url='https://bitbucket.org/< 你的Bitbucket 账户>/guestbook/',
  18. classifiers=[
  19. 'Development Status :: 4 - Beta',
  20. 'Framework :: Flask',
  21. 'License :: OSI Approved :: BSD License',
  22. 'Programming Language :: Python',
  23. 'Programming Language :: Python :: 2.7',
  24. ],
  25. packages=find_packages(),
  26. include_package_data=True,
  27. keywords=['web', 'guestbook'],
  28. license='BSD License',
  29. install_requires=[
  30. 'Flask',
  31. ],
  32. entry_points="""
  33. [console_scripts]
  34. guestbook = guestbook:main
  35. """,
  36. )

这里添加的项目有以下 4 个。

  • long_description

可描述多行说明。说明内容按照reStructuredText(reST)语法进行描述,PyPI会将其自动转换为HTML并显示在网站上。long_description的内容大多和README.rst相同。即便不同,我们也建议各位将长达几行的说明文章存放在其他文件中。在上面的例子里,我们设置了让long_description读取README.rst文件。

  • classifiers

从trove classifiers定义的项目中选取适当项目,以列表的形式列举在这里。这个列表包含的项目就是程序包在PyPI上的分类。用户可以在PyPI网站上通过分类筛选来寻找自己想要的程序包。

在上面的例子里,我们描述了许可证信息和Python版本等。如果我们想指定的分类并不在trove classifiers之中,那么指不指定它都无所谓。

  • keywords

以列表形式列举出易于搜索的单词,或者让使用者一眼就明白意思的词汇。

  • license

可随意描述许可证信息。前面我们只能用trove classifiers定义过的值来指定classifiers,但 license处却可以指定任意字符串。在上例中,我们将这部分描述为BSD License。

这里只对一部分会显示在 PyPI 上的 setup 函数的传值参数进行了介绍,此外还有许多这里并未提及的传值参数。

Python 官方文档

https://docs.python.org/2.7/distutils/setupscript.html#additional-meta-data

检查 setup 函数内指定的参数

将程序包实际上传到 PyPI 之后,我们需要打开 PyPI 页面查看一下效果。如果发现页面并没有将 long_description 的内容转换为 HTML,而是直接显示了 reST 文本,那么很可能是我们的 reST 描述出现了错误。为回避这一问题,最好在 upload 之前查一遍错。查错时可以用 docutils 附属的 rst2html.py 命令。docutils 是一个文字处理工具,它能将 reST 文本转换成其他多种格式。其安装方法如下。

  1. $ pip install docutils

安装好 docutils 后,用下述方法执行 rst2html.py 命令,查看文档内是否有错误描述。

  1. $ python setup.py --long-description | rst2html.py > devnull

另外,如果各位使用的是 Python 2.7 以上的版本,并且环境中安装了 docutils,那么可以用下述简短的命令来查错。文档没有问题的情况下只会显示 running check,有问题则会显示类似下例的内容。

  1. $ python setup.py check -r -s
  2. running check
  3. warning: check: No directive entry for "spam" in module "docutils.parsers.rst.languages.en".
  4. Trying "spam" as canonical directive name. (line 19)
  5. warning: check: Could not finish the parsing.
  6. error: Please correct your package.

check 命令用于检查 setup 函数各参数的设置是否正确。如果参数遗漏或指定有误,系统会报错或者发出警告。我们只要给 check 命令指定几个选项,就可以检查 long_description 中指定的文本是否符合 reStructuredText 语法了。

这个检测命令也可以添加到我们前面提过的 alias 设置当中,具体如下例所示。

  1. $ python setup.py alias release check -r -s register sdist bdist_egg upload

这样一来,在我们执行 python setup.py release 命令时,系统就会先进行 check。一旦 check 发现问题,后续的 register 命令将不再执行,有问题的程序包也就不会被发布出去了。

专栏 在 PyPI 上公开

不知各位有没有这样一种感觉:在 PyPI 上公开了程序包的 Python 工程师都是大牛!

“写好程序之后,先整理成 Python 的标准发布形式,然后公开到 PyPI 上,让全世界人拿去用!”这话说起来容易,但真正做起来时,许多人会不禁感到犹豫。原因主要有两个,一来是不知道怎么生成标准的发布包,二来是认为自己写的程序别人拿去也没什么用。当然可能还有心理上的抗拒,觉得 PyPI 上面满满的都是世界顶尖好用的程序,自己写的程序难登大雅之堂,没资格发布在 PyPI 上。

那么,我们回过头来想一下,在 PyPI 上公开程序包的最大动力又是什么呢?这个问题显然因人而异,但绝大多数人的出发点无外乎“希望得到程序的反馈信息”“希望能帮到别人”“向 Python 工程师同行们炫耀一下”“想听到别人的夸赞”之类。另外,我们平时也会隔三差五地向 PyPI 上传几个程序包,而一直支持我们的动机则是“想为组织或企业做宣传”“想让大家知道,让大家拿去用”“很多自己平时用的 OSS 都来自这里,所以想把自己做出的成果也放在这上面,为 OSS 界做一份贡献”。

在公开程序包时,“保证品质”“整理文档”“进行测试”这三项工作必不可少。这里,“不想让全世界的工程师看自己出丑”的心理因素只是一小方面,更大的原因是这个阶段能完善我们的程序库,使其回到一个干净的状态,避免在日常开发的过程中混入一些多余功能。当然,公开后获得的反馈也是其魅力之一。

不但个人编写的程序如此,工作中开发出来的程序同样是这个道理。拿我们 BePROUD 来说,bpmappers 和 bpssl 就是例子。因为它们向一般公众公开,所以要维持适当的功能和文档。我们同时还收到了公司内外两方面的反馈,这让我们能不断作出改进。

所以,让我们都来做在 PyPI 上公开了程序包的 Python 工程师大牛吧。这对技术上的要求并没有各位想象中那么高。

3.3.12 小结

发布采用 Python 编写的程序库或应用程序时,最标准的方法就是用 setup.py 进行封装。经 setup.py 封装后的发布包要注册到 PyPI 上,然后其他用户就可以通过 pip 轻松地安装了。另外,对于一些不打算公开的程序,我们也建议将其封装成可发布的状态,这样既能方便地放到其他环境下试运行,又能便于其他项目拿去重复利用。此外还要养成一个习惯,即在 README.rst 文件中写明项目概要、运行方法、设置等信息,以便重复利用程序包。

如果各位还想进一步了解 setup.py 的写法或封装的相关知识,可以参考 Python Packaging User Guide。

Python Packaging User Guide

https://packaging.python.org/en/latest/

3.4 小结

本章我们学习了 Python 项目的结构,以及如何对使用 PyPA 工具的开发环境进行设置。另外,我们对第 2 章编写的程序进行了结构调整,同时学习了如何把程序包上传至 PyPI。

对于要经常重建环境的情况(例如自动测试或向服务器部署)而言,本章介绍的上传流程更能发挥其效果。另外,从下一章开始,我们的介绍重点将从个人开发转移到团队开发,而这里学习的流程对团队开发同样有效。