您当前位置:首页 深入 Python 3

难度级别:♦♦♦♦♢

打包 Python 库

您会发现,羞愧就像痛苦一样,您只会感受到一次。
— 梅特伊侯爵夫人,危险的关系

 

深入探讨

真正的艺术家会发布作品。或者正如史蒂夫·乔布斯所说。您是否想发布 Python 脚本、库、框架或应用程序?太好了。世界需要更多 Python 代码。Python 3 附带了一个名为 Distutils 的打包框架。Distutils 是很多东西:构建工具(为您)、安装工具(为您的用户)、包元数据格式(用于搜索引擎)等等。它与 Python 包索引(“PyPI”)集成,后者是开源 Python 库的中央存储库。

所有这些 Distutils 的方面都围绕着安装脚本,传统上称为 setup.py。事实上,您已经在本书中看到了几个 Distutils 安装脚本。您使用 Distutils 在 HTTP Web 服务 中安装了 httplib2,并在 案例研究:将 chardet 移植到 Python 3 中再次安装了 chardet

在本章中,您将学习 chardethttplib2 的安装脚本如何工作,并且您将逐步完成发布自己的 Python 软件的过程。

# chardet's setup.py
from distutils.core import setup
setup(
    name = "chardet",
    packages = ["chardet"],
    version = "1.0.2",
    description = "Universal encoding detector",
    author = "Mark Pilgrim",
    author_email = "[email protected]",
    url = "http://chardet.feedparser.org/",
    download_url = "http://chardet.feedparser.org/download/python3-chardet-1.0.1.tgz",
    keywords = ["encoding", "i18n", "xml"],
    classifiers = [
        "Programming Language :: Python",
        "Programming Language :: Python :: 3",
        "Development Status :: 4 - Beta",
        "Environment :: Other Environment",
        "Intended Audience :: Developers",
        "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)",
        "Operating System :: OS Independent",
        "Topic :: Software Development :: Libraries :: Python Modules",
        "Topic :: Text Processing :: Linguistic",
        ],
    long_description = """\
Universal character encoding detector
-------------------------------------

Detects
 - ASCII, UTF-8, UTF-16 (2 variants), UTF-32 (4 variants)
 - Big5, GB2312, EUC-TW, HZ-GB-2312, ISO-2022-CN (Traditional and Simplified Chinese)
 - EUC-JP, SHIFT_JIS, ISO-2022-JP (Japanese)
 - EUC-KR, ISO-2022-KR (Korean)
 - KOI8-R, MacCyrillic, IBM855, IBM866, ISO-8859-5, windows-1251 (Cyrillic)
 - ISO-8859-2, windows-1250 (Hungarian)
 - ISO-8859-5, windows-1251 (Bulgarian)
 - windows-1252 (English)
 - ISO-8859-7, windows-1253 (Greek)
 - ISO-8859-8, windows-1255 (Visual and Logical Hebrew)
 - TIS-620 (Thai)

This version requires Python 3 or later; a Python 2 version is available separately.
"""
)

chardethttplib2 是开源的,但您没有义务在任何特定许可下发布您自己的 Python 库。本章中描述的过程适用于任何 Python 软件,无论其许可证如何。

Distutils 无法为您完成的事项

发布第一个 Python 包是一个艰巨的过程。(发布第二个会更容易一些。)Distutils 试图尽可能地自动化它,但有一些事情您必须亲自完成。

目录结构

要开始打包您的 Python 软件,您需要整理好文件和目录。httplib2 目录如下所示

httplib2/                 
|
+--README.txt             
|
+--setup.py               
|
+--httplib2/              
   |
   +--__init__.py
   |
   +--iri2uri.py
  1. 创建一个根目录来存放所有内容。将其命名为与您的 Python 模块相同的名称。
  2. 为了适应 Windows 用户,您的“自述文件”应包含 .txt 扩展名,并且应使用 Windows 风格的回车符。仅仅因为使用了一个从命令行运行并包含其自身宏语言的精美文本编辑器,并不意味着您需要给您的用户制造麻烦。(您的用户使用记事本。很悲哀,但这是事实。)即使您使用的是 Linux 或 Mac OS X,您的精美文本编辑器无疑也具有保存具有 Windows 风格回车符的文件的选项。
  3. 您的 Distutils 安装脚本应命名为 setup.py,除非您有充分的理由不这样做。您没有充分的理由不这样做。
  4. 如果您的 Python 软件是一个单独的 .py 文件,您应该将其与您的“自述文件”和安装脚本一起放在根目录中。但是 httplib2 不是单个 .py 文件;它是 一个多文件模块。但没关系!只需将 httplib2 目录放在根目录中,这样您就在 httplib2/ 根目录中的 httplib2/ 目录中有一个 __init__.py 文件。这不是问题;事实上,它将简化您的打包过程。

chardet 目录看起来略有不同。与 httplib2 一样,它也是 一个多文件模块,因此在 chardet/ 根目录中有一个 chardet/ 目录。除了 README.txt 文件之外,chardetdocs/ 目录中还有 HTML 格式的文档。docs/ 目录包含几个 .html.css 文件,以及一个 images/ 子目录,其中包含几个 .png.gif 文件。(这在后面很重要。)此外,为了符合 (L)GPL 许可软件的惯例,它还有一个名为 COPYING.txt 的单独文件,其中包含 LGPL 的完整文本。


chardet/
|
+--COPYING.txt
|
+--setup.py
|
+--README.txt
|
+--docs/
|  |
|  +--index.html
|  |
|  +--usage.html
|  |
|  +--images/ ...
|
+--chardet/
   |
   +--__init__.py
   |
   +--big5freq.py
   |
   +--...

编写安装脚本

Distutils 安装脚本是 Python 脚本。理论上,它可以做 Python 能做的一切。实际上,它应该尽可能地少做,以尽可能标准的方式进行。安装脚本应该是枯燥的。您的安装过程越奇特,您的错误报告就越奇特。

每个 Distutils 安装脚本的第一行总是相同的

from distutils.core import setup

这将导入 setup() 函数,它是 Distutils 的主要入口点。95% 的所有 Distutils 安装脚本都只包含对 setup() 的一次调用,除此之外什么都没有。(我完全是编造了这个统计数据,但如果您的 Distutils 安装脚本做的不仅仅是调用 Distutils setup() 函数,那么您应该有充分的理由。您有充分的理由吗?我不这么认为。)

setup() 函数可以接受数十个参数。为了让所有人都保持理智,您必须使用 命名参数 来表示每个参数。这不仅仅是一种约定;它是一个硬性要求。如果尝试使用非命名参数调用 setup() 函数,您的安装脚本将崩溃。

以下命名参数是必需的

虽然不是必需的,但我建议您在安装脚本中也包含以下内容

安装脚本元数据在 PEP 314 中定义。

现在让我们看一下 chardet 安装脚本。它具有所有这些必需和推荐的参数,以及我尚未提到的一个参数:packages

from distutils.core import setup
setup(
    name = 'chardet',
    packages = ['chardet'],
    version = '1.0.2',
    description = 'Universal encoding detector',
    author='Mark Pilgrim',
    ...
)

packages 参数突出了分发过程中不幸的词汇重叠。我们一直在谈论“包”作为您正在构建(以及可能在 Python “包”索引中列出)的东西。但这不是这个 packages 参数所指的。它指的是 chardet 模块是 一个多文件模块,有时被称为…“包”。packages 参数告诉 Distutils 包含 chardet/ 目录、它的 __init__.py 文件以及构成 chardet 模块的所有其他 .py 文件。这很重要;关于文档和元数据的这些美好谈话,如果您忘记包含实际代码,那就毫无意义了!

对您的包进行分类

Python 包索引(“PyPI”)包含数千个 Python 库。适当的分类元数据将使人们更容易找到您的库。PyPI 允许您按分类器浏览包。您甚至可以选择多个分类器来缩小搜索范围。分类器不是您可以忽略的不可见元数据!

要对您的软件进行分类,请将 classifiers 参数传递给 Distutils setup() 函数。classifiers 参数是一个字符串列表。这些字符串不是自由格式的。所有分类器字符串都应该来自 PyPI 上的这个列表。

分类器是可选的。您可以在没有任何分类器的情况下编写 Distutils 安装脚本。不要那样做。您应该始终至少包含以下分类器

我还建议您包含以下分类器

良好包分类器的示例

例如,以下是 Django 的分类器,Django 是一个生产就绪的、跨平台的、BSD 许可的 Web 应用程序框架,它运行在您的 Web 服务器上。(Django 尚未与 Python 3 兼容,因此未列出 Programming Language :: Python :: 3 分类器。)

Programming Language :: Python
License :: OSI Approved :: BSD License
Operating System :: OS Independent
Development Status :: 5 - Production/Stable
Environment :: Web Environment
Framework :: Django
Intended Audience :: Developers
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: Dynamic Content
Topic :: Internet :: WWW/HTTP :: WSGI
Topic :: Software Development :: Libraries :: Python Modules

以下是 chardet 的分类器,chardet案例研究:将 chardet 移植到 Python 3 中介绍的字符编码检测库。chardet 是测试版质量的、跨平台的、Python 3 兼容的、LGPL 许可的,并且旨在供开发人员将其集成到他们自己的产品中。

Programming Language :: Python
Programming Language :: Python :: 3
License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
Operating System :: OS Independent
Development Status :: 4 - Beta
Environment :: Other Environment
Intended Audience :: Developers
Topic :: Text Processing :: Linguistic
Topic :: Software Development :: Libraries :: Python Modules

以下是 httplib2 的分类器,httplib2HTTP Web 服务 章节中介绍的库。httplib2 是测试版质量的、跨平台的、MIT 许可的,并且旨在供 Python 开发人员使用。

Programming Language :: Python
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Development Status :: 4 - Beta
Environment :: Web Environment
Intended Audience :: Developers
Topic :: Internet :: WWW/HTTP
Topic :: Software Development :: Libraries :: Python Modules

使用清单指定其他文件

默认情况下,Distutils 将在您的发行包中包含以下文件

这将涵盖httplib2项目中的所有文件。但是对于chardet项目,我们还希望包含COPYING.txt许可证文件以及包含图像和HTML文件的整个docs/目录。要告诉Distutils在构建chardet发行包时包含这些附加文件和目录,你需要一个清单文件

清单文件是一个名为MANIFEST.in的文本文件。将其放在项目的根目录中,与README.txtsetup.py并排。清单文件不是Python脚本;它们是包含一系列以Distutils定义的格式编写的“命令”的文本文件。清单命令允许你包含或排除特定的文件和目录。

这是chardet项目的完整清单文件

include COPYING.txt                                
recursive-include docs *.html *.css *.png *.gif    
  1. 第一行不言自明:包含来自项目根目录的COPYING.txt文件。
  2. 第二行稍微复杂一些。recursive-include命令接受一个目录名和一个或多个文件名。文件名不限于特定文件;它们可以包含通配符。这行意味着“看到项目根目录中的docs/目录了吗?在其中(递归地)查找.html.css.png.gif文件。我想要在我的发行包中包含所有这些文件。”

所有清单命令都保留你在项目目录中设置的目录结构。该recursive-include命令不会将一堆.html.png文件放到发行包的根目录中。它将保留现有的docs/目录结构,但只包含该目录中与给定通配符匹配的文件。(我之前没有提到,但chardet文档实际上是用XML编写的,并由单独的脚本转换为HTML。我不想在发行包中包含XML文件,只想要HTML和图像。)

清单文件有自己的独特格式。有关详细信息,请参阅指定要分发的文件和清单模板命令。

重申一下:只有当你想要包含Distutils默认情况下不包含的文件时,你才需要创建一个清单文件。如果你确实需要一个清单文件,它应该只包含Distutils无法自行找到的文件和目录。

检查你的安装脚本是否有错误

有很多东西需要跟踪。Distutils带有内置的验证命令,该命令检查你的安装脚本中是否包含所有必需的元数据。例如,如果你忘记包含version参数,Distutils会提醒你。

c:\Users\pilgrim\chardet> c:\python31\python.exe setup.py check
running check
warning: check: missing required meta-data: version

一旦你包含了一个version参数(以及所有其他必需的元数据),check命令将如下所示

c:\Users\pilgrim\chardet> c:\python31\python.exe setup.py check
running check

创建源代码发行版

Distutils支持构建多种类型的发行包。至少,你应该构建一个“源代码发行版”,其中包含你的源代码、你的Distutils安装脚本、你的“自述”文件以及你想包含的任何其他文件。要构建源代码发行版,请将sdist命令传递给你的Distutils安装脚本。

c:\Users\pilgrim\chardet> c:\python31\python.exe setup.py sdist
running sdist
running check
reading manifest template 'MANIFEST.in'
writing manifest file 'MANIFEST'
creating chardet-1.0.2
creating chardet-1.0.2\chardet
creating chardet-1.0.2\docs
creating chardet-1.0.2\docs\images
copying files to chardet-1.0.2...
copying COPYING -> chardet-1.0.2
copying README.txt -> chardet-1.0.2
copying setup.py -> chardet-1.0.2
copying chardet\__init__.py -> chardet-1.0.2\chardet
copying chardet\big5freq.py -> chardet-1.0.2\chardet
...
copying chardet\universaldetector.py -> chardet-1.0.2\chardet
copying chardet\utf8prober.py -> chardet-1.0.2\chardet
copying docs\faq.html -> chardet-1.0.2\docs
copying docs\history.html -> chardet-1.0.2\docs
copying docs\how-it-works.html -> chardet-1.0.2\docs
copying docs\index.html -> chardet-1.0.2\docs
copying docs\license.html -> chardet-1.0.2\docs
copying docs\supported-encodings.html -> chardet-1.0.2\docs
copying docs\usage.html -> chardet-1.0.2\docs
copying docs\images\caution.png -> chardet-1.0.2\docs\images
copying docs\images\important.png -> chardet-1.0.2\docs\images
copying docs\images\note.png -> chardet-1.0.2\docs\images
copying docs\images\permalink.gif -> chardet-1.0.2\docs\images
copying docs\images\tip.png -> chardet-1.0.2\docs\images
copying docs\images\warning.png -> chardet-1.0.2\docs\images
creating dist
creating 'dist\chardet-1.0.2.zip' and adding 'chardet-1.0.2' to it
adding 'chardet-1.0.2\COPYING'
adding 'chardet-1.0.2\PKG-INFO'
adding 'chardet-1.0.2\README.txt'
adding 'chardet-1.0.2\setup.py'
adding 'chardet-1.0.2\chardet\big5freq.py'
adding 'chardet-1.0.2\chardet\big5prober.py'
...
adding 'chardet-1.0.2\chardet\universaldetector.py'
adding 'chardet-1.0.2\chardet\utf8prober.py'
adding 'chardet-1.0.2\chardet\__init__.py'
adding 'chardet-1.0.2\docs\faq.html'
adding 'chardet-1.0.2\docs\history.html'
adding 'chardet-1.0.2\docs\how-it-works.html'
adding 'chardet-1.0.2\docs\index.html'
adding 'chardet-1.0.2\docs\license.html'
adding 'chardet-1.0.2\docs\supported-encodings.html'
adding 'chardet-1.0.2\docs\usage.html'
adding 'chardet-1.0.2\docs\images\caution.png'
adding 'chardet-1.0.2\docs\images\important.png'
adding 'chardet-1.0.2\docs\images\note.png'
adding 'chardet-1.0.2\docs\images\permalink.gif'
adding 'chardet-1.0.2\docs\images\tip.png'
adding 'chardet-1.0.2\docs\images\warning.png'
removing 'chardet-1.0.2' (and everything under it)

这里要注意几点

c:\Users\pilgrim\chardet> dir dist
 Volume in drive C has no label.
 Volume Serial Number is DED5-B4F8

 Directory of c:\Users\pilgrim\chardet\dist

07/30/2009  06:29 PM    <DIR>          .
07/30/2009  06:29 PM    <DIR>          ..
07/30/2009  06:29 PM           206,440 chardet-1.0.2.zip
               1 File(s)        206,440 bytes
               2 Dir(s)  61,424,635,904 bytes free

创建图形安装程序

在我看来,每个Python库都应该为Windows用户提供图形安装程序。它很容易制作(即使你没有使用Windows),Windows用户也会对此表示感谢。

Distutils可以为你创建图形Windows安装程序,方法是将bdist_wininst命令传递给你的Distutils安装脚本。

c:\Users\pilgrim\chardet> c:\python31\python.exe setup.py bdist_wininst
running bdist_wininst
running build
running build_py
creating build
creating build\lib
creating build\lib\chardet
copying chardet\big5freq.py -> build\lib\chardet
copying chardet\big5prober.py -> build\lib\chardet
...
copying chardet\universaldetector.py -> build\lib\chardet
copying chardet\utf8prober.py -> build\lib\chardet
copying chardet\__init__.py -> build\lib\chardet
installing to build\bdist.win32\wininst
running install_lib
creating build\bdist.win32
creating build\bdist.win32\wininst
creating build\bdist.win32\wininst\PURELIB
creating build\bdist.win32\wininst\PURELIB\chardet
copying build\lib\chardet\big5freq.py -> build\bdist.win32\wininst\PURELIB\chardet
copying build\lib\chardet\big5prober.py -> build\bdist.win32\wininst\PURELIB\chardet
...
copying build\lib\chardet\universaldetector.py -> build\bdist.win32\wininst\PURELIB\chardet
copying build\lib\chardet\utf8prober.py -> build\bdist.win32\wininst\PURELIB\chardet
copying build\lib\chardet\__init__.py -> build\bdist.win32\wininst\PURELIB\chardet
running install_egg_info
Writing build\bdist.win32\wininst\PURELIB\chardet-1.0.2-py3.1.egg-info
creating 'c:\users\pilgrim\appdata\local\temp\tmp2f4h7e.zip' and adding '.' to it
adding 'PURELIB\chardet-1.0.2-py3.1.egg-info'
adding 'PURELIB\chardet\big5freq.py'
adding 'PURELIB\chardet\big5prober.py'
...
adding 'PURELIB\chardet\universaldetector.py'
adding 'PURELIB\chardet\utf8prober.py'
adding 'PURELIB\chardet\__init__.py'
removing 'build\bdist.win32\wininst' (and everything under it)
c:\Users\pilgrim\chardet> dir dist
c:\Users\pilgrim\chardet>dir dist
 Volume in drive C has no label.
 Volume Serial Number is AADE-E29F

 Directory of c:\Users\pilgrim\chardet\dist

07/30/2009  10:14 PM    <DIR>          .
07/30/2009  10:14 PM    <DIR>          ..
07/30/2009  10:14 PM           371,236 chardet-1.0.2.win32.exe
07/30/2009  06:29 PM           206,440 chardet-1.0.2.zip
               2 File(s)        577,676 bytes
               2 Dir(s)  61,424,070,656 bytes free

为其他操作系统构建可安装的包

Distutils可以帮助你为Linux用户构建可安装的包。在我看来,这可能不值得你花时间。如果你想将你的软件分发到Linux,你最好花时间与专门从事为主要Linux发行版打包软件的社区成员合作。

例如,我的chardet库位于Debian GNU/Linux存储库中(因此也位于Ubuntu存储库中)。我对此没有任何贡献;这些包只是有一天突然出现了。Debian社区有自己的打包Python库的策略,Debian python-chardet包旨在遵循这些约定。由于该包位于Debian的存储库中,因此Debian用户将收到安全更新和/或新版本,具体取决于他们选择的系统范围设置来管理自己的计算机。

Distutils构建的Linux包不提供任何这些优势。你的时间最好花在别处。

将你的软件添加到Python包索引中

将软件上传到Python包索引是一个三步过程。

  1. 注册你自己
  2. 注册你的软件
  3. 上传你用setup.py sdistsetup.py bdist_*创建的包

要注册你自己,请访问PyPI用户注册页面。输入你想要的用户名和密码,提供一个有效的电子邮件地址,然后点击Register按钮。(如果你有PGPGPG密钥,你也可以提供它。如果你没有密钥或不知道这是什么意思,不要担心。)检查你的电子邮件;几分钟内,你应该收到来自PyPI的包含验证链接的消息。点击链接完成注册过程。

现在你需要在PyPI上注册你的软件并上传它。你可以在一步中完成所有这些操作。

c:\Users\pilgrim\chardet> c:\python31\python.exe setup.py register sdist bdist_wininst upload 
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]: 1 
Username: MarkPilgrim 
Password:
Registering chardet to http://pypi.python.org/pypi 
Server response (200): OK
running sdist 
... output trimmed for brevity ...
running bdist_wininst 
... output trimmed for brevity ...
running upload 
Submitting dist\chardet-1.0.2.zip to http://pypi.python.org/pypi
Server response (200): OK
Submitting dist\chardet-1.0.2.win32.exe to http://pypi.python.org/pypi
Server response (200): OK
I can store your PyPI login so future submissions will be faster.
(the login will be stored in c:\home\.pypirc)
Save your login (y/N)?n 
  1. 当你第一次发布你的项目时,Distutils会将你的软件添加到Python包索引中,并为它提供自己的URL。每次之后,它只会使用你在setup.py参数中所做的任何更改来更新项目元数据。接下来,它会构建一个源代码发行版(sdist)和一个Windows安装程序(bdist_wininst),然后将它们上传到PyPI(upload)。
  2. 输入1或直接按ENTER选择“使用你现有的登录信息”。
  3. 输入你在PyPI用户注册页面上选择的用户名和密码。Distuils不会回显你的密码;它甚至不会用星号代替字符。只需输入你的密码并按ENTER
  4. Distutils在Python包索引中注册你的包…
  5. …构建你的源代码发行版…
  6. …构建你的Windows安装程序…
  7. …并将它们都上传到Python包索引。
  8. 如果你想自动执行发布新版本的过程,你需要将你的PyPI凭据保存在本地文件中。这完全不安全,而且完全可选。

恭喜你,你现在在Python包索引上有了自己的页面!地址是http://pypi.python.org/pypi/NAME,其中NAME是你传递给setup.py文件中的name参数的字符串。

如果你想发布新版本,只需更新你的setup.py以包含新版本号,然后再次运行相同的上传命令

c:\Users\pilgrim\chardet> c:\python31\python.exe setup.py register sdist bdist_wininst upload

Python打包的多种未来可能性

Distutils并不是Python打包的全部,但截至本文撰写之时(2009年8月),它是唯一在Python 3中有效的打包框架。Python 2中还有其他一些框架;有些专注于安装,有些专注于测试和部署。这些框架中的一些或全部可能会在将来被移植到Python 3中。

这些框架专注于安装

这些框架专注于测试和部署

进一步阅读

关于Distutils

关于其他打包框架

© 2001–11 Mark Pilgrim