编写 setup 脚本
源代码相关的参数
数据文件相关的参数
入口点(entry point)
项目管理的主要工作是:
下面的介绍将按照以上方面展开。
Setuptools 是 disutils(Python 2.6+)的增强版,它使开发者可以更容易地构建和分发 Python 包,尤其是依赖其它包的包。
对用户来说,使用 Setuptools 构建和分发的包看起来与基于 distuils 的 Python 包一样。为了使用 Setuptools,用户不必安装或了解它们,开发者也不必在发行版中包含全部 Setuptools 工具。在用户没有安装 Setuptools 的情况下,只要源码包中包含 bootstrap 模块(一个 12K 的 .py 文件),那么在构建源码包时,就会自动地下载和安装 Setuptools。
功能亮点
setup() 参数可以更容易地扩展 disutils,在多个项目之间分发/重用扩展,而不必拷贝代码请按照 EasyInstall 安装说明来安装 Setuptools。需要特别强调的是:使用 easy_install 时,如果想要把包安装到 Python 的 site-packages 目录之外的地方,那么可以参考下面的自定义安装位置区域。
默认情况下,EasyInstall 会把包安装到 Python 的主 site-packages 目录,然后通过在该目录下自定义一个 .pth 文件的方式,来管理包。
但是经常有这样的需求:用户或开发者想使用 easy_install 将包安装到 site-packages 之外的地方。有许多方式来实现自定义安装,下面列出了最简单和最有意义的几种方式:
使用 --user 选项
从 Python 2.6 开始,easy_install 支持用户模式,这意味着可以把包安装到跟特定用户相关的位置,默认路径是 site.USER_BASE 变量的值。可以通过给 setup.py install 和 easy_install 指定 --user 选项来开启这种安装模式
使用 --user 选项和自定义的 PYTHONUSERBASE 环境变量
用户模式的安装路径可以通过设置 PYTHONUSERBASE 环境变量来定制,它会更新 site.USER_BASE 的值,为了在特定的应用程序之间隔离包,可以简单地把那个应用程序的 PYTHONUSERBASE 环境变量设置为特定的值
使用 virtualenv virtualenv 是第三方包,它通过“克隆” Python 安装(包括 Python 可执行程序、头文件、标准库)的方式,来创建隔离的 Python 环境。virtualenv 的发展始于用户安装模式存在之前。
virtualenv 也提供 easy_install、pip、wheel,并且它的 easy_install 的使用方式与正常的 easy_install 一样。virtualenv 提供许多用户安装模式没提供的特性,比如 virtualenv 能隐藏全局 site-packages。
请阅读 virtualenv 的文档来获得更多细节。
补充说明:
1, 可以通过执行如下命令:
xxxxxxxxxxpython -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"来获取 site-packages 的路径。
2,在计算 site-packages 路径时,Python 可执行程序会根据自身计算出 sys.prefix(也可以通过 PYTHONHOME 环境变量为 sys.prefix 设置值),然后根据 sys.prefix、操作系统、Python 版本计算出 site-packages 的位置
下面是一个最小化的 setup 脚本:
xxxxxxxxxxfrom setuptools import setup, find_packages
setup( name="HelloWorld", # 包名 version="0.0.1", # 版本 packages=find_packages() # 包列表)可以看到,通过使用 Setuptools,不需要做太多事情,就能够为项目生成 egg,将项目上传到 PYPI,自动地包含 setup.py 所在的目录中的所有包。
在把项目发布到 PYPI 之前,可以给 setup 脚本添加一些 metadata,来帮助使用者发现和了解项目。并且项目可能包含依赖、数据文件和脚本:
xxxxxxxxxxfrom setuptools import setup, find_packages
setup( name="HelloWorld", # 项目名 version="0.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"], # And 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.)1,常规命令:
python setup.py build
构建所有需要安装的东西,包括包、单独的模块、C 扩展、数据文件、脚本(从命令行启动的、包含 Python 源代码的文件)
python setup.py install
安装 build 目录中的所有东西
python setup.py clean
清理 build 命令和 bdist 命令生成的临时目录
python setup.py sdist
创建源码发行版,可以通过 --formats 指定格式,可选值包括:
python setup.py bdist
创建构建后的二进制发行版,可以通过 --formats 指定格式,可选值包括:
2,上传到 PyPI:
Setuptools 和 distutils 提供 register 和 upload 命令,用于向 PyPI 推送元数据(<project_name>.egg-info/ 目录下的文件)和发行版文件(dist/ 目录下的文件)。为了将包发布到 PyPI,需要执行下列操作:
去 PyPI (官方地址是 https://pypi.python.org/pypi)注册帐号(本文假定读者已有帐号)
在 $HOME 下创建 .pypirc 文件
通过这个文件可以配置 PyPI 源的 URL 以及用户名和密码,比如:
xxxxxxxxxx[distutils]index-servers = pypi other
[pypi]repository: https://upload.pypi.org/legacy/username: <username>password: <password>
[other]repository: <repository-url>username: <username>password: <password>使用 register 命令将发行版的元数据提交到 PyPI 注意:新的官方 PyPI 源无需预注册,直接上传文件即可。但是其它 PyPI 源可能仍然需要 register 过程
xxxxxxxxxxpython setup.py register -r pypi # -r 用来指定使用哪个 PyPI 源使用 upload 命令,将发行版文件推送到 PyPI
xxxxxxxxxxpython setup.py sdist bdist upload -r pypi # -r 用来指定使用哪个 PyPI 源packages 参数用于指定包名列表,Setuptools 会处理(构建、发布、安装等)该列表中列出的每个包中的所有纯 Python 模块(如果想要处理某个包的子包,那么应该在 packages 中列出该子包,比如 packages=["foo", "foo.bar"])。
package_dir 参数用于将包名映射到文件系统的目录上。其值是一个字典,其中 key 是包名(Setuptools 会自动地将父包的映射应用到子包上,因此无需在 package_dir 参数中指定子包的映射关系),value 是相对于项目根目录(即 setup.py 所在的目录)的目录。当包名为空时,value 所指定的目录代表“包的根目录”,Setuptools 会去“包的根目录”下寻找所有未指定映射关系的包。比如:
xxxxxxxxxxsetup( ... packages=["foo", "foo.bar", "baz"], package_dir={ "foo": "lib", "": "src", }, ...)在上面的例子中,Setuptools 会将 lib/ 目录当作包 foo,将 lib/bar/ 目录当作包 foo.bar,因此会寻找 lib/__init__.py 和 lib/bar/__init__.py。
包的根目录是 src/,Setuptools 会将 src/baz/ 目录当作包 baz,因此会寻找 src/baz/__init__.py。
(之所以寻找 __init__.py 文件,是因为只有包含 __init__.py 文件的目录才是合法的 Python 包)
对于简单的项目,人工地向 setup() 的 packages 参数添加包名就足够了。然而对于大型项目,保持包名列表的更新是非常大的负担。为了解决这个麻烦,可以通过 setuptools.find_packages() 函数来生成包名列表。
find_packages() 的第一个参数是源代码目录。如果省略该参数,那么它的缺省值是 setup.py 所在的目录。对于使用 src/ 或 lib/ 目录作为源代码树的根目录的项目,应该使用 "src" 或 "lib" 作为 find_packages() 的第一个参数(这类项目仍然需要指定 setup() 的 package_dir 参数,比如 package_dir={"": "src"})。
find_packages() 的另外两个参数是两个包名模式的列表:
find_packages() 会递归地遍历源代码目录,寻找 Python 包,并使用包含模式列表过滤,最终再移除掉被排除模式列表匹配的包。
在包含模式和排除模式中,可以包含通配符。比如 find_packages(exclude=["*.tests"]) 会排除所有名称的最后一部分是 tests 的包;find_packages(exclude=["*.tests", "*.tests.*"]) 会排除掉任何名称包含 tests 的子包。
之所以使用 find_packages() 来设置 setup() 的 package 参数,是因为即使项目增加顶级包或子包,也不必修改 setup 脚本。
可以通过 py_modules 参数列出要处理的所有纯 Python 模块。这些模块既可以在“包的根目录”下,也可以在某个包中。比如:
xxxxxxxxxxfrom setuptools import setup
setup( ... py_modules=["mod1", "pkg.subpkg.mod2"], package_dir={"": "src"}, # 包的根目录是 src/ ...)在上面的例子中,Setuptools 会去 src/ 目录下寻找 mod1.py,src/pkg/subpkg/ 目录下寻找 mod2.py。
通常情况下,需要将一些文件安装到包中,这些文件通常是跟包的实现紧密相关的数据或者是包含说明文档的文本文件。这些文件被称为“包数据”。
在 setup() 中,跟“包数据”相关的参数有:
include_package_data:
如果将 include_package_data 设置为 True,那么 Setuptools 会自动地安装包目录下的所有数据文件,这些数据文件必须在 CVS 或 Subversion 的控制之下,或者必须通过 distutils 的 MANIFEST.in 文件指定它们。
关于 MANIFEST.in 文件的详细介绍,可以参考官网。下面仅做简要的说明。
项目中的 MANIFEST.in 文件主要用来定义在发行版中包含哪些文件,MANIFEST.in 中的每行是一个命令,这个命令用来指定包含或排除哪些文件。比如:
xxxxxxxxxxinclude *.txtrecursive-include examples *.txt *.pyprune examples/sample?/build
能够出现在清单文件 MANIFEST.in 中的命令包括:
| 命令 | 描述 |
| include pat1 pat2 ... | 包含名称匹配列表中任意模式的所有文件 |
| exlude pat1 pat2 ... | 排除名称匹配列表中任意模式的所有文件 |
| recursive-include dir pat1 pat2 ... | 递归地包含 dir 目录下,名称匹配列表中的任意模式的所有文件 |
| recursive-exclude dir pat1 pat2 ... | 递归地排除 dir 目录下,名称匹配列表中的任意模式的所有文件 |
| prune dir | 排除 dir 下的所有文件 |
| graft dir | 包含 dir 下的所有文件 |
package_data:
如果数据文件不在 VCS 的控制之下,或者在一个不被支持的 VCS 的控制之下,或者想要细粒度地控制包含哪些文件,那么需要使用 package_data 关键字参数,比如:
xxxxxxxxxxfrom setuptools import setup, find_packages
setup( ... package_data = { # 包含任意包里的扩展名为 .txt 和 .rst 的所有文件 "": ["*.txt", "*.rst"], # 包含 hello 包中所有扩展名为 .msg 的文件 "hello": ["*.msg"], })package_data 参数是映射包名称到 glob 模式列表的字典。如果数据文件在包的子目录中,glob 模式可以包含子目录名称。比如包目录树如下:
xxxxxxxxxxsetup.pysrc/mypkg/__init__.pymypkg.txtdata/somefile.datotherdata.dat
那么可以这样编写 setup 文件:
xxxxxxxxxxfrom setuptools import setup, find_packages
setup( ... packages=find_packages('src'), # 包含 src/ 下的所有包及其子包 package_dir={'':'src'}, # 设置“包的根目录”为 src/
package_data={ # 包含任意包中,所有扩展名为 .txt 的文件 '': ['*.txt'], # 包含 mypkg 包的 data/ 子目录中的,所有扩展名为 .dat 的文件 'mypkg': ['data/*.dat'], })在 package_data 参数中,当包名为空时,那么对应的模式列表会被应用到所有包(即使这些包已经列出自己的模式),因此在上面的例子中,虽然没在 mypkg 的模式中列出 mypkg.txt,但仍然会包含它。
如果使用路径,那么必须使用 \/ 作为路径分隔符(即使是在 Windows 系统上),Setuptools 在 build 期间会自动地把 \/ 转换成平台相关的分隔符。
有时单独使用 include_package_data 或 package_data 参数,不能精确地定义包含哪些文件。比如想要在版本控制系统和源代码中包含 README.txt 文件,但是在安装时不包含它。对此 Setuptools 提供了 exclude_package_data 参数,其用法如下:
xxxxxxxxxxfrom setuptools import setup, find_packages
setup( ... packages = find_packages('src'), # include all packages under src package_dir = {'':'src'}, # tell distutils packages are under src
include_package_data = True, # include everything in source control
# 排除所有包中的 README.txt exclude_package_data = {'': ['README.txt']},)与 package_data 参数一样,exclude_package_data 也是映射包名到 glob 模式列表的字典,在使用这个选项时,空的键会把给定的模式列表应用到所有包,匹配这些模式的任何文件在安装时都会被排除,即使这些文件在 package_data 中被列出来或者被 include_package_data 包含进来。
综上所述,可以通过下面三个选项来包含“包数据”:
include_package_data
包含所有被 MANIFEST.in 匹配的或者受版本控制系统控制的文件
package_data
指定额外的模式来匹配没被 MANIFEST.in 匹配的或者不在版本控制系统中的文件
exclude_package_data
指定用于在安装包时不应该被包含的数据文件注意:
Setuptools 既支持将 Python 项目安装为 zip 文件,也支持将其安装为目录。如果有访问包中的数据文件或源代码的需求,那么可以将 Python 项目安装为目录。比如:
xxxxxxxxxxfrom setuptools import setup, find_packages
setup( name="test_setuptools", version="0.0.1a1", py_modules=["mymod"], zip_safe=False)在这种情形下,可以通过包或模块的 __file__ 属性来寻找数据文件。比如某个模块需要访问它所在目录下的 foo.config 文件:
xxxxxxxxxxfoo_config = open(os.path.join(os.path.dirname(__file__), "foo.config")).read()在基于 PEP 302 的导入钩子中,支持从 zip 文件和 egg 文件导入包或模块。在这种情况下,无法使用 __file__ 属性来定位数据文件。此时应该使用 PEP 302 的 get_data() 扩展。但是使用导入协议非常复杂,因此 egg 运行时系统提供了 pkg_resources 资源管理 API,用于访问数据文件。pkg_resources 模块也是 Setuptools 的一部分。
使用 pkg_resources 时,只需要把包名或模块名和资源文件名一起传递给 resource_string、resource_stream、resource_filename 函数。并且应该尽量使用 resource_string 和 resource_stream,而不是 resource_filename,因为它需要将资源文件抽取到临时目录中,这比返回文件内容或文件句柄代价大。
当资源文件被包含在包的子目录中时,无论在哪个操作系统平台,在资源名称中都必须使用 "/" 作为分隔符。
我们把不在包中的数据文件称为非包数据文件。在 distutils 中,可以通过 data_files 参数,将不在包中的普通数据文件安装到特定平台的某个位置(比如 /usr/share)。这些数据文件通常是配置文件模版、文档等。比如:
xxxxxxxxxxfrom distutils.core import setup
setup( ..., data_files=[ ('bitmaps', ['bm/b1.gif', 'bm/b2.gif']), ('config', ['cfg/data.cfg']), ('/etc/init.d', ['init-script']) ])data_files 的值是列表,列表中的每个元素都是二元组,二元组的第一元是将要安装到的位置,第二元是数据文件列表。
Setuptools 把数据文件绑定到 egg 文件或目录中(这是一个特性,而不是 bug)。当 data_files 的某个元素的第一元是相对路径时,那么它是相对发行版的根目录的。pkg_resources 模块的资源管理 API 也支持在某个包中访问其它包中的数据文件,只需要使用 Requirement 代替包名,比如:
xxxxxxxxxxfrom pkg_resources import Requirement, resource_filename
filename = resource_filename(Requirement.parse("MyProject"), "sample.conf")上面的例子会返回 “MyProject” 的发行版的根目录下的 “sample.conf” 的文件名。
在安装包时,Setuptools 会自动地安装依赖。跟依赖有关的信息会被包含在 PythonEgg 的元数据中。
对其它模块或包的依赖可以通过 setup() 函数的 install_requires 关键字参数来指定。其值必须是字符串列表,每个字符用于指定一个依赖包,同时也可以指定版本。
当只使用名称而不指定版本时,表示发行版可以依赖任意版本的模块或包。当依赖特定版本的包或模块时,可以在包名后面指定若干个修饰符,每个修饰符中包含比较操作符和版本号,可用的修饰符包括:
xxxxxxxxxx< > ==<= >= !=
多个修饰符之间使用逗号进行分隔。这些修饰符之间是 “AND” 的关系,即所有修饰符都应该被满足。比如:
| 修饰符 | 含义 |
| ==1.0 | 只兼容 1.0 版本 |
| >1.0, !=1.5.1, <2.0 | 兼容 1.0 到 2.0 之间,除去 1.5.1 的任意版本 |
项目也可以依赖不在 PyPI 上的包和模块,此时可以通过 dependency_links 参数指定一个 URL 列表,Setuptools 会搜索这些页面,来下载 egg 包或源发行版。比如:
xxxxxxxxxxsetup( ... dependency_links=[ "http://peak.telecommunity.com/snapshots/" ], ...)在 distutils 中,可以通过 setup() 函数的 scripts 关键字参数来指定要安装的脚本。比如:
xxxxxxxxxxfrom setuptools import setup, find_packages
setup( name="install_scripts", version="0.0.1", packages=find_packages(), scripts=["scripts/a"],
author="Tim Chow", author_email="744475502@qq.com", url="http://timd.cn/setuptools/")这样做有点“笨拙”,比如当真正运行的“主函数”是某个模块中的某个函数时,必须为它创建单独的脚本文件。
使用 Setuptools 可以自动地生成脚本。在 Windows 上,还会创建 .exe 文件,因此用户不必改变 PATHEXT 设置。为了使用 Setuptools 的这个特性,需要在 setup 脚本中定义入口点(entry points),比如下面的例子会创建两个控制台脚本 foo、bar,以及一个 GUI 脚本 baz:
xxxxxxxxxxsetup( # other arguments here... entry_points={ 'console_scripts': [ 'foo = my_package.some_module:main_func', 'bar = other_module:some_func', ], 'gui_scripts': [ 'baz = my_package_gui:start_func', ] })当项目被安装到非 Windows 平台上时,Setuptools 会创建 foo、bar、baz 脚本。执行 foo 脚本时,会调用 my_package.some_module 的 main_func;执行 bar 脚本时,会调用 other_module 的 some_func;执行 baz 脚本时,会调用 my_package_gui 的 start_func。这些函数都以无参的形式被调用(在函数中可以通过命令选项或环境变量接受参数),它们的返回值会被传递给 sys.exit()。
在 Windows 平台上,会创建 foo.exe、bar.exe、baz.exe 启动器。.exe 包装会找到合适的 Python 解释器,并使用它执行 .py 或 .pyw 文件。
有时某些项目有一些非必须的依赖,比如在 ReportLab 库被安装时,才提供 PDF 输出,或者在 docutils 库被安装时,才支持 reStructuredText,这种特性被称为 “extra”。在 Setuptools 中,可以通过 setup() 函数的 extras_require 关键字参数来指定可选的依赖。同时也可以通过 install_requires 关键字参数,强制被依赖的项目安装它的某些 extra。比如:
xxxxxxxxxxsetup( name="Project-A", ... extras_require={ 'PDF': ["ReportLab>=1.2", "RXP"], 'reST': ["docutils>=0.3"], })extras_require 关键字参数的值是映射 extra 特性名称到字符串列表的字典。列表中的每个字符串描述了一个可选的依赖。这些可选依赖不会被自动安装,除非直接或间接依赖该项目的其它项目强制要求安装(指定依赖时,在项目名后面的方括号中列出要强制安装的 extra);使用 EasyInstall 安装包时,也可以用这种方式指定要强制安装的 extra。比如:
xxxxxxxxxxsetup( name="Project-B", install_requires=["Project-A[PDF]"], ...)假设有朝一日 Project-A 对 PDF 的支持不再需要 ReportLab,那么可以将 PDF 的值设置为空列表,这样就无需更改 Project-B 的 setup 信息:
xxxxxxxxxxsetup( name="Project-A", ... extras_require={ 'PDF': [], 'reST': ["docutils>=0.3"], })在 entry points 中,也可以使用 extra 特性。比如:
xxxxxxxxxxsetup(name="Project-A",...entry_points={'console_scripts': ['rst2pdf = project_a.tools.pdfgen [PDF]','rst2html = project_a.tools.htmlgen',# more script entry points ...],})
在上面的例子中,Project-A 包含 “rst2pdf” 脚本,PDF 依赖只有在 “rst2pdf” 脚本运行时,才会被解析。
Setuptools 支持通过注册入口点(entry point)的方式,动态地发现服务和插件。比如一个博客工具支持多种输出插件,每种输出插件可以将博客内容输出成一种格式。此时可以定义一个叫 blogtool.parsers 的入口点组,然后把插件注册为一个入口点。在运行时,博客工具可以通过查找入口点,找到对应的输出插件。比如下面的例子是将 .rst 输出插件注册到 blogtool.parsers 组:
xxxxxxxxxxsetup( # ... entry_points={'blogtool.parsers': '.rst = some_module:SomeClass'})
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"))setup() 函数的 entry_points 参数的值要么是 .ini 风格的字符串,要么是映射入口点组名称到字符串或字符串列表的字典,其中每个字符串是一个入口点说明符,入口点说明符包含名称和值,名称和值之间用等号分隔。值包含用点分隔的模块名,模块名后面是冒号,冒号后面是点分隔的、出现在模块中的对象名;接下来是放在方括号中的 extra 列表(可选)。在加载入口点时,任何通过 extra 指定的依赖,都会被传递给 pkg_resources.require() 函数,因此当被依赖的包缺失时,会打印相应的错误信息。下面看一个简单的例子:
项目结构如下所示:
xxxxxxxxxx% tree ..├── dynamic_discovery_plugins│ └── __init__.py└── setup.py
1 directory, 2 filesdynamic_discovery_plugins/__init__.py 的内容如下:
xxxxxxxxxxfrom pkg_resources import iter_entry_points
for entry_point in iter_entry_points(group="dynamic_discovery_plugins.test_out", name=None): print(entry_point)
# 加载入口点 fun = entry_point.load()
fun("a nice day")setup.py 的内容如下:
xxxxxxxxxxfrom setuptools import setup, find_packages
setup( name="dynamic_discovery_plugins", version="0.0.1", packages=find_packages(),
entry_points={ "dynamic_discovery_plugins.test_out": [".test_print=test_plugins:test_print"], },
author="Tim Chow", author_email="744475502@qq.com", url="http://timd.cn/setuptools/")假设 test_plugins.py 已经在 sys.path 下:
xxxxxxxxxxdef test_print(clause): print("you say:", clause)在导入 dynamic_discovery_plugins 包时,将打印如下信息:
xxxxxxxxxx.test_print = test_plugins:test_printyou say: a nice day
偶尔有想使 egg 文件可以直接执行的情况。可以通过包含如下所示的入口点(entry point)的方式,来完成这件事:
xxxxxxxxxxsetup( # other arguments here... entry_points = { 'setuptools.installation': [ 'eggsecutable = my_package.some_module:main_func', ] })任何通过上面的 setup 脚本构建而来的 egg 文件,都会包含一个简短的头:从 my_package.some_module 导入并调用 main_func,在 unix-like 平台上可以通过 /bin/sh 或赋予这个 egg 文件可执行权限的方式,来执行这个 egg 文件。
这个特性的主要意图是支持在非 Windows 平台上安装 Setuptools 自己。值得注意的是带有 eggsecutable 头的 egg 文件不能被重命名,也不能通过符号链接来执行,否则会以错误退出。
在进行测试驱动开发时或运行自动化构建之前都需要执行单元测试。可以使用 test 命令在部署之前,运行项目的单元测试。为了使用这个命令,项目的单元测试必须被封装进 unittest 的 TestSuite 中。比如:
xxxxxxxxxxfrom setuptools import setup, find_packages
setup( ...
test_suite="integrating_unittest.tests", ...)当 test_suite 参数的值是包时,Setuptools 会递归地把该包下所有的模块和子包中的测试用例加载到整个 TestSuite 中(如果通过 test_loader 关键字指定了 TestLoader,那么这个处理规则可能会发生改变)。