返回介绍

3.3 文件结构与发布程序包

发布于 2024-01-21 17:11:03 字数 27917 浏览 0 评论 0 收藏 0

编写完程序之后,我们还要面对从封装成包到发布的复杂流程。在本部分中,我们会尝试把第 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 sdist 和 python setup.py install 等与包相关的操作了。另外,将程序包注册到 PyPI 的操作也需要通过 setup.py 进行。

◉ setup.py 的命令

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

LIST 3.32 setup.py

from setuptools import setup
setup(name='guestbook')

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

LIST 3.33 setup.py 的指令一览

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

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

LIST 3.34 python setup.py

$ python setup.py sdist
running sdist
running egg_info
-( 中间省略)-
warning: sdist: standard file not found: should have one of README, README.rst, README.txt
running check
warning: check: missing required meta-data: url
warning: check: missing meta-data: either (author and author_email) or (maintainer and maintainer_email) must be supplied
creating guestbook-0.0.0
-( 中间省略)-
creating dist
Creating tar archive
removing 'guestbook-0.0.0' (and everything under it)
$ ls dist/
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 项目内只有一个文件时的结构示例

/home/bpbook/projectname
   +-- MANIFEST.in
   +-- README.rst
   +-- packagename.py
   +-- setup.py

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

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

/home/bpbook/projectname
   +-- MANIFEST.in
   +-- README.rst
   +-- packagename/
   |  +-- __init__.py
   |  +-- module.py
   |  +-- templates/
   |     +-- index.html
   +-- setup.py

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

文件路径

说明

guestbook.py

服务器程序

guestbook.dat

提交数据文件

static/main.css

CSS 文件

templates/index.html

输出 HTML 的模板,用于显示“提交 / 留言列表”的页面

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

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

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

/home/bpbook/guestbook/
   +-- LICENSE.txt
   +-- MANIFEST.in
   +-- README.rst
   +-- guestbook
   |  +-- __init__.py
   |  +-- static/main.css
   |  +-- templates/index.html
   +-- 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

from setuptools import setup, find_packages
setup(
  name='guestbook',
  version='1.0.0',
  packages=find_packages(),
  include_package_data=True,
  install_requires=[
    'Flask',
  ],
)

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

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

· name

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

· version

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

· packages

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

packages=[
  'guestbook', 'guestbook.server', 'guestbook.server.dir',
  'guestbook.storage', ...
],

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

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

recursive-include guestbook *.html *.css
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 环境及安装

$ cd ..
$ virtualenv .venv

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

LIST 3.42 目录结构

/home/bpbook/guestbook/
   +-- .venv/
   +-- LICENSE.txt
   +-- MANIFEST.in
   +-- guestbook
   |  +-- __init__.py
   |  +-- static/main.css
   |  +-- templates/index.html
   +-- setup.py

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

LIST 3.43 搭建 virtualenv 环境及 editable 安装

$ source .venv/bin/activate
(.venv)$ pip install -e .
Obtaining file:///home/bpbook/guestbook
  Running setup.py (path:/home/bpbook/guestbook/setup.py) egg_info for package from file:///home/bpbook/guestbook
Installing collected packages: guestbook
-(中间省略:安装依赖包)-
  Running setup.py develop for guestbook
  Creating /home/bpbook/.venv/lib/python2.7/site-packages/guestbook.egg-link(link to .)
  Adding guestbook 1.0.0 to easy-install.pth file
  Installed /home/bpbook/guestbook
-(中间省略)-
Successfully installed guestbook
Cleaning up...
(.venv)$ pip freeze
Flask==0.10.1
Jinja2==2.7.3
MarkupSafe==0.23
Werkzeug==0.9.6
guestbook==1.0.0
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 生成命令

from setuptools import setup
setup(
  name='guestbook',
  version='1.0.0',
  packages=find_packages(),
  include_package_data=True,
  install_requires=[
    'Flask',
  ],
  entry_points="""
    [console_scripts]
    guestbook = guestbook:main
  """,
)

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

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

LIST 3.45 guestbook/__init__.py

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

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

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

LIST 3.46 重新安装

(.venv)$ pip install -e ./guestbook
-(中间省略)-
Successfully installed guestbook
Cleaning up...
(.venv)$ ls .venv/bin/guestbook
guestbook
(.venv)$ guestbook
 * Running on http://127.0.0.1:8000/
 * 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

$ python setup.py sdist
running sdist
running egg_info
writing requirements to guestbook.egg-info/requires.txt
writing guestbook.egg-info/PKG-INFO
writing top-level names to guestbook.egg-info/top_level.txt
writing dependency_links to guestbook.egg-info/dependency_links.txt
reading manifest file 'guestbook.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
writing manifest file 'guestbook.egg-info/SOURCES.txt'
running check
creating guestbook-1.0.0
-( 中间省略)-
making hard links in guestbook-1.0.0...
hard linking LICENSE.txt -> guestbook-1.0.0
-( 中间省略)-
Creating tar archive
removing 'guestbook-1.0.0' (and everything under it)
$ ls dist/
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 留言板项目的目录结构

/home/bpbook/guestbook/
   +-- .venv/
   +-- LICENSE.txt
   +-- MANIFEST.in
   +-- guestbook
   |  +-- __init__.py
   |  +-- static/main.css
   |  +-- templates/index.html
   +-- guestbook.dat
   +-- guestbook.egg-info/
   +-- setup.py

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

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

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

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

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

LIST 3.49 注册到版本库

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

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

LIST 3.50 .hgignore

.*.egg-info ^guestbook.dat$ ^.venv$

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

LIST 3.51 提交“.hgignore”

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

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

LIST 3.52 hg push

$ 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 示例

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

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

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

① clone 项目的版本库

② 搭建项目专用的 virtualenv 环境

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

LIST 3.54 设置流程

$ hg clone https://bitbucket.org/beproud/guestbook
$ cd guestbook
$ virtualenv .venv
$ source .venv/bin/activate
(.venv)$ pip install .
(.venv)$ guestbook
 * 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

===================
留言板应用
===================
目的
=====
练习开发通过 Web 浏览器提交留言的 Web 应用程序
工具版本
====================
:Python:   2.7.8
:pip:    1.5.6
:virtualenv: 1.11.6
安装与启动方法
=======================
从版本库获取代码,然后在该目录下搭建 virtualenv 环境::
  $ hg clone https://bitbucket.org/beproud/guestbook
  $ cd guestbook
  $ virtualenv .venv
  $ source .venv/bin/activate
  (.venv)$ pip install .
  (.venv)$ guestbook
   * Running on http://127.0.0.1:5000/
开发流程
=========
用于开发的安装
------------------
1. 检测
2. 按以下流程安装
    (.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 替换了程序包,这一步该如何告知其他人

(.venv)$ pip uninstall flask
(.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 重建环境

(.venv)$ virtualenv --clear .venv  # 删除.venv 环境内的全部依赖库
(.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. 更新 ``setup.py`` 的 ``install_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

(.venv)$ pip freeze > requirements.txt

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

LIST 3.60 requirements.txt

Flask==0.10.1 Jinja2==2.7.3 MarkupSafe==0.23 Werkzeug==0.9.6 guestbook==1.0.0 itsdangerous==0.24

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

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

LIST 3.61 用 requirements.txt 进行安装

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

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

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

LIST 3.62 README.rst

开发流程
=========
变更依赖库时
---------------------
1. 更新 ``setup.py`` 的 ``install_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

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

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

LIST 3.64 生成 wheel 程序包

$ python setup.py bdist_wheel
running bdist_wheel
-( 中间省略)-
creating _build/bdist.linux-x86_64/wheel/guestbook-1.0.0.dist-info/WHEEL
$ ls dist/
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 选项,代码如下。

$ python setup.py bdist_wheel --universal
--(中间省略)--
$ ls dist
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 注册程序包

$ python setup.py register

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

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

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

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

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

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

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

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

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

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

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

$ python setup.py sdist bdist_wheel upload

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

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

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

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

专栏 PyPI 的测试服务器

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

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

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

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

3 https://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 上看到它们。下面是一个描述示例。

import os
from setuptools import setup, find_packages
def read_file(filename):
  basepath = os.path.dirname(os.path.dirname(__file__))
  filepath = os.path.join(basepath, filename)
  if os.path.exists(filepath):
    return open(filepath).read()
  else:
    return ''
setup(
  name='guestbook',
  version='1.0.0',
  description='A guestbook web application.',
  long_description=read_file('README.rst'),
  author='< 你的名字>',
  author_email='< 你的邮箱地址>',
  url='https://bitbucket.org/< 你的Bitbucket 账户>/guestbook/',
  classifiers=[
    'Development Status :: 4 - Beta',
    'Framework :: Flask',
    'License :: OSI Approved :: BSD License',
    'Programming Language :: Python',
    'Programming Language :: Python :: 2.7',
  ],
  packages=find_packages(),
  include_package_data=True,
  keywords=['web', 'guestbook'],
  license='BSD License',
  install_requires=[
    'Flask',
  ],
  entry_points="""
    [console_scripts]
    guestbook = guestbook:main
  """,
)

这里添加的项目有以下 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 文本转换成其他多种格式。其安装方法如下。

$ pip install docutils

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

$ python setup.py --long-description | rst2html.py > /dev/null

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

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

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

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

$ 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/

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文