关于 python 中的 setup.py
# 前言
其实对于 setup.py 和 setup.cfg 的关注是从 OpenStack 的源码包中开始的,OpenStack 每个组件的发布时都是一个 tar.gz 包,同样,我们直接从 github 上 clone 代码后也会发现两个文件的存在。当阅读 Nova 或 Ceilometer(其他组件可能也会涉及)的代码时,发现 setup.cfg 中内容对于代码的理解有很大的影响。那么,到底 setup.py 和 setup.cfg 是干什么的?
# setup.py
我们从例子开始。假设你要分发一个叫 foo 的模块,文件名 foo.py,那么 setup.py 内容如下:
from distutils.core import setup
setup(name='foo',
version='1.0',
py_modules=['foo'],
)
2
3
4
5
然后,运行python setup.py sdist
为模块创建一个源码包
root@network:/kong/setup# python setup.py sdist
running sdist
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
warning: sdist: manifest template 'MANIFEST.in' does not exist (using default file list)
warning: sdist: standard file not found: should have one of README, README.txt
writing manifest file 'MANIFEST'
creating foo-1.0
making hard links in foo-1.0...
hard linking foo.py -> foo-1.0
hard linking setup.py -> foo-1.0
creating dist
Creating tar archive
removing 'foo-1.0' (and everything under it)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在当前目录下,会创建dist
目录,里面有个文件名为foo-1.0.tar.gz
,这个就是可以分发的包(如果使用命令python setup.py bdist_egg
,那么会在 dist 目录中生成 foo-1.0-py2.7.egg 包,setup.py 中第一句引入需要改为from setuptools import setup
)。使用者拿到这个包后,解压,到 foo-1.0 目录下执行:python setup.py install
,那么,foo.py 就会被拷贝到 python 类路径下,可以被导入使用(如果安装是 egg 文件,会把 egg 文件拷贝到 dist-packages 目录下)。
root@network:/kong/setup/dist/foo-1.0# python setup.py install
running install
running build
running build_py
creating build
creating build/lib.linux-x86_64-2.7
copying foo.py -> build/lib.linux-x86_64-2.7
running install_lib
copying build/lib.linux-x86_64-2.7/foo.py -> /usr/local/lib/python2.7/dist-packages
byte-compiling /usr/local/lib/python2.7/dist-packages/foo.py to foo.pyc
running install_egg_info
Removing /usr/local/lib/python2.7/dist-packages/foo-1.0.egg-info
Writing /usr/local/lib/python2.7/dist-packages/foo-1.0.egg-info
root@network:/kong/setup/dist/foo-1.0# ll /usr/local/lib/python2.7/dist-packages/foo
foo-1.0.egg-info foo.py foo.pyc
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对于 Windows,可以执行python setup.py bdist_wininst
生成一个 exe 文件;若要生成 RPM 包,执行python setup.py bdist_rpm
,但系统必须有 rpm 命令的支持。可以运行下面的命令查看所有格式的支持:
root@network:/kong/setup# python setup.py bdist --help-formats
List of available distribution formats:
--formats=rpm RPM distribution
--formats=gztar gzip'ed tar file
--formats=bztar bzip2'ed tar file
--formats=ztar compressed tar file
--formats=tar tar file
--formats=wininst Windows executable installer
--formats=zip ZIP file
--formats=msi Microsoft Installer
2
3
4
5
6
7
8
9
10
11
setup 函数还有一些参数:
1、packages
告诉 Distutils 需要处理那些包(包含__init__.py
的文件夹)
2、package_dir
告诉 Distutils 哪些目录下的文件被映射到哪个源码包,感觉好像是一个相对路径的定义。一个例子:package_dir = {'': 'lib'}
,表示以 lib 为主目录。
3、ext_modules
是一个包含 Extension 实例的列表,Extension 的定义也有一些参数。
4、ext_package
定义 extension 的相对路径
5、requires
定义依赖哪些模块
6、provides
定义可以为哪些模块提供依赖
7、scripts
指定 python 源码文件,可以从命令行执行。在安装时指定--install-script
8、package_data
通常包含与包实现相关的一些数据文件或类似于 readme 的文件。
package_data = {'': ['*.txt'], 'mypkg': ['data/*.dat'],}
表示包含所有目录下的 txt 文件和 mypkg/data 目录下的所有 dat 文件。
9、data_files
指定其他的一些文件(如配置文件)
setup(...,
data_files=[('bitmaps', ['bm/b1.gif', 'bm/b2.gif']),
('config', ['cfg/data.cfg']),
('/etc/init.d', ['init-script'])]
)
2
3
4
5
规定了哪些文件被安装到哪些目录中。如果目录名是相对路径,则是相对于sys.prefix
或sys.exec_prefix
的路径。如果没有提供模板,会被添加到 MANIFEST 文件中。
执行 sdist 命令时,默认会打包哪些东西呢?
- 所有由
py_modules
或packages
指定的源码文件 - 所有由
ext_modules
或libraries
指定的 C 源码文件 - 由
scripts
指定的脚本文件 - 类似于 test/test*.py 的文件
- README.txt 或 README,setup.py,setup.cfg
- 所有
package_data
或data_files
指定的文件
还有一种方式是写一个 manifest template,名为MANIFEST.in
,定义如何生成 MANIFEST 文件,内容就是需要包含在分发包中的文件。一个 MANIFEST.in 文件如下:
include *.txt
recursive-include examples *.txt *.py
prune examples/sample?/build
2
3
# setup.cfg
setup.cfg 提供一种方式,可以让包的开发者提供命令的默认选项,同时为用户提供修改的机会。对 setup.cfg 的解析,是在 setup.py 之后,在命令行执行前。
setup.cfg 文件的形式类似于
[command]
option=value
...
2
3
4
其中,command
是 Distutils 的命令参数,option
是参数选项,可以通过python setup.py --help build_ext
方式获取。
需要注意的是,比如一个选项是–foo-bar,在 setup.cfg 中必须改成 foo_bar 的格式
符合 Distutils2 的 setup.cfg 有些不同。包含一些 sections:
1、global
定义 Distutils2 的全局选项,可能包含 commands,compilers,setup_hook(定义脚本,在 setup.cfg 被读取后执行,可以修改 setup.cfg 的配置,pbr 就用到了这个)
2、metadata
3、files
- packages_root:根目录
- packages
- modules
- scripts
- extra_files
# Setuptools
上面的 setup.py 和 setup.cfg 都是遵循 python 标准库中的 Distutils,而 setuptools 工具针对 Python 官方的 distutils 做了很多针对性的功能增强,比如依赖检查,动态扩展等。很多高级功能我就不详述了,自己也没有用过,等用的时候再作补充。详情可参见这里 (opens new window)。
一个典型的遵循 setuptools 的脚本:
from setuptools import setup, find_packages
setup(
name = "HelloWorld",
version = "0.1",
packages = find_packages(),
scripts = ['say_hello.py'],
# Project uses reStructuredText, so ensure that the docutils get
# installed or upgraded on the target machine
install_requires = ['docutils>=0.3'],
package_data = {
# If any package contains *.txt or *.rst files, include them:
'': ['*.txt', '*.rst'],
# include any *.msg files found in the 'hello' package, too:
'hello': ['*.msg'],
},
# metadata for upload to PyPI
author = "Me",
author_email = "me@example.com",
description = "This is an Example Package",
license = "PSF",
keywords = "hello world example examples",
url = "http://example.com/HelloWorld/", # project home page, if any
# could also include long_description, download_url, classifiers, etc.
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
setuptools 相对 distutils,增强的关键字:
include_package_data
:为 True 时自动添加受版本控制的数据文件,可替代package_data
,同时,exclude_package_data
可以排除某些文件。当你需要加入没有被版本控制的文件时,还是老老实实使用package_data
吧。
install_requires
:代替require
函数。表示当前包的安装依赖于哪些分发包,这些信息会写入 egg 的元信息中,这些包在安装时会自动(从 PyPI)下载并安装。如果包在 PyPI 中找不到,则会从dependency_links
标识的 URL 中获取。
extras_require
:当前包的高级/额外特性需要依赖的分发包。
entry_points
:这个很经典。见下面的讲解。
setup_requires
: 安装脚本执行时需要依赖的分发包,主要用于构建过程。注意,这里列出的包不会自动安装,如果需要,同时要在install_requires
中指定。
dependency_links
:URL 地址。这些地址在安装setup_requires
或tests_require
指定的包时使用。会写入 egg 的 metadata 信息中。
# 如何让一个 egg 可被执行
setup(
# other arguments here...
entry_points = {
'setuptools.installation': [
'eggsecutable = my_package.some_module:main_func',
]
}
)
2
3
4
5
6
7
8
9
# 如何定义一个可选特性
setup(
name="Project-A",
...
extras_require = {
'PDF': ["ReportLab>=1.2", "RXP"],
'reST': ["docutils>=0.3"],
}
)
2
3
4
5
6
7
8
表示如果系统安装 docutils 了,那么可提供 reST 特性。当然,docutils 包不会自动安装,只有第三方包依赖本包的 reST 特性时,才会下载安装。
特性如何使用呢?需要与 entry points 结合使用,在上一个 setup.py 脚本中增加:
setup(
name="Project-A",
...
entry_points = {
'console_scripts': [
'rst2pdf = project_a.tools.pdfgen [reST]',
'rst2html = project_a.tools.htmlgen',
# more script entry points ...
],
}
)
2
3
4
5
6
7
8
9
10
11
表示如果要使用 rst2pdf 脚本,就要安装 docutils。
或者被其他 project 依赖:install_requires = ["Project-A[PDF]"]
# 插件式开发
我想大家最熟悉的就是这个特性了吧。比如一个博客系统想用不同的插件支持不同的语言输出格式,那么就可以定义一个“entry point group”,不同的插件就可以注册“entry point”,插件注册的示例:
setup(
# ...
entry_points = {'blogtool.parsers': ['.rst = some_module:a_func']}
)
# 或者
setup(
# ...
entry_points = """
[blogtool.parsers]
.rst = some.nested.module:SomeClass.some_classmethod [reST]
""",
extras_require = dict(reST = "Docutils>=0.3.5")
)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 管理依赖
我们写依赖声明的时候需要在 setup.py 中写好抽象依赖(install_requires),在 requirements.txt 中写好具体的依赖,但是我们并不想维护两份依赖文件,这样会让我们很难做好同步。 requirements.txt 可以更好地处理这种情况,我们可以在有 setup.py 的目录里写下一个这样的 requirements.txt
--index https://pypi.python.org/simple/plain
-e .
2
3
4
这样 pip install -r requirements.txt 可以照常工作,它会先安装该文件路径下的包,然后继续开始解析抽象依赖,结合 –index 选项后转换为具体依赖然后再安装他们。
这个办法可以让我们解决一种类似这样的情形:比如你有两个或两个以上的包在一起开发但是是分开发行的,或者说你有一个尚未发布的包并把它分成了几个部分。如果你的顶层的包 依然仅仅按照“名字”来依赖的话,我们依然可以使用 requirements.txt 来安装开发版本的依赖包:
--index https://pypi.python.org/simple/
-e https://github.com/foo/bar.git#egg=bar
-e .
2
3
4
5
这会首先从 https://github.com/foo/bar.git 来安装包 bar , 然后进行到第二行 -e . ,开始安装 setup 中的抽象依赖,但是包 bar 已经安装过了, 所以 pip 会跳过安装。
# Differences between distribute, distutils, setuptools and distutils2
Distutils is the standard tool used for packaging. It works rather well for simple needs, but is limited and not trivial to extend.
Setuptools is a project born from the desire to fill missing distutils functionality and explore new directions. In some subcommunities, it’s a de facto standard. It uses monkey-patching and magic that is frowned upon by Python core developers.
Distribute is a fork of Setuptools that was started by developers feeling that its development pace was too slow and that it was not possible to evolve it. Its development was considerably slowed when distutils2 was started by the same group. 2013-August update: distribute is merged back into setuptools and discontinued.
Distutils2 is a new distutils library, started as a fork of the distutils codebase, with good ideas taken from setup tools (of which some were thoroughly discussed in PEPs), and a basic installer inspired by pip. The actual name you use to import Distutils2 is packaging in the Python 3.3+ standard library, or distutils2 in 2.4+ and 3.1–3.2. (A backport will be available soon.) Distutils2 did not make the Python 3.3 release, and it was put on hold.
# PBR
pbr (opens new window)是 setuptools 的辅助工具,最初是为 OpenStack 开发(https://launchpad.net/pbr),基于d2to1 (opens new window)。
A library for managing setuptools packaging needs in a consistent manner.
pbr 会读取和过滤 setup.cfg 中的数据,然后将解析后的数据提供给 setup.py 作为参数。包含如下功能:
1、从 git 中获取 Version、AUTHORS and ChangeLog 信息
2、Sphinx Autodoc。pbr 会扫描 project,找到所有模块,生成 stub files
3、Requirements。pbr 会读取 requirements.txt,生成 setup 函数需要的install_requires/tests_require/dependency_links
这里需要注意,在 requirements.txt 文件的头部可以使用:
--index https://pypi.python.org/simple/
,这一行把一个抽象的依赖声明如 requests==1.2.0 转变为一个具体的依赖声明 requests 1.2.0 from pypi.python.org/simple/
4、long_description。从 README.rst, README.txt or README file 中生成long_description
参数
使用 pbr 很简单:
from setuptools import setup
setup(
setup_requires=['pbr'],
pbr=True,
)
2
3
4
5
6
7
使用 pbr 时,setup.cfg 中有一些配置。在[files]中,有三个 key:
packages
:指定需要包含的包,行为类似于 setuptools.find_packages
namespace_packages
:指定 namespace packages
data_files
: 指定目的目录和源文件路径,一个示例:
[files]
data_files =
etc/pbr = etc/pbr/*
etc/neutron =
etc/api-paste.ini
etc/dhcp-agent.ini
etc/init.d = neutron.init
2
3
4
5
6
7
8
[entry_points]
段跟 setuptools 的方式相同。
# Babel
A collection of tools for internationalizing Python applications
Babel (opens new window)是 Python 的一个国际化工具包,提供了对 distutils 或 setuptools 的支持,包含一些命令。
1、compile_catalog
类似于 msgfmt 工具,takes a message catalog from a PO file and compiles it to a binary MO file.
$ ./setup.py compile_catalog --directory foobar/locale --locale pt_BR
running compile_catalog
compiling catalog to foobar/locale/pt_BR/LC_MESSAGES/messages.mo
2
3
4
2、extract_messages
类似于 xgettext,it can extract localizable messages from a variety of difference source files, and generate a PO (portable object) template file from the collected messages.
$ ./setup.py extract_messages --output-file foobar/locale/messages.pot
running extract_messages
extracting messages from foobar/__init__.py
extracting messages from foobar/core.py
...
writing PO template file to foobar/locale/messages.pot
2
3
4
5
6
7
3、update_catalog
类似于 msgmerge,it updates an existing translations catalog based on a PO template file (POT).
# setup.py 和 pip
表面上,python setup.py install
和pip install
都是用来安装 python 包的,实际上,pip
提供了更多的特性,更易于使用。体现在以下几个方面:
- pip 会自动下载依赖,而如果使用 setup.py,则需要手动搜索和下载;
- pip 会自动管理包的信息,使卸载/更新更加方便和容易,使用
pip uninstall
即可。而使用 setup.py,必须手动删除,有时容易出错。 - pip 提供了对
virtualenv
更好的整合。
# 结语
OK,讲了这么多琐碎的东西,现在去看看 Nova 或 Ceilometer 的 setup 脚本,是不是一下清晰了很多?!但说实话,setup.py 的使用,我还不能讲的特别清楚,需要在后续的实战中学习。
# 参考文档
http://docs.python.org/2/distutils/introduction.html (opens new window)
http://pythonhosted.org/setuptools/ (opens new window)
# 原文链接
关于 python 中的 setup.py (opens new window)
# 推荐阅读
花了两天,终于把 Python 的 setup.py 给整明白了 - 知乎 (opens new window) pep-0518 (opens new window) Python 打包指南 2021 | Frost's Blog (opens new window)