03-Python 打包工具 setuptools setup.py 文件详解

0、setup.py 示例

先来个简单示例,下面依次讲解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from setuptools import setup, find_packages

setup(
name="mytest",
version="1.0",
author="flp",
author_email="flepeng@163.com",
description="这只是一次测试",
url="http://iswbm.com/",
packages=find_packages(),
packages=['devops', "devops.dev", "devops.ops"],
classifiers = [],
data_files=[],
package_data={},
exclude_package_data={}
)

1、基础参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from setuptools import setup, find_packages

setup(
name="mytest", # 指定项目名称,我们在后期打包时,这就是打包的包名称,当然打包时的名称可能还会包含下面的版本号
version="1.0", # 指定版本号
author="flp", # 作者
author_email="flepeng@163.com", # 作者邮箱
description="这只是一次测试", # 这是对当前项目的一个描述
url="http://iswbm.com/", # 项目主页

# 你要安装的包,通过 setuptools.find_packages 找到当前目录下有哪些包。
# 也可以制定目录,如下,搜索名称为(xyz)的包,以及xyz的下层目录的包搜索出来,并一起打成egg文件包。
# find_packages(xyz)
packages=find_packages()

# 指定包名,即你需要打包的包名称,要实际在你本地存在哟,它会将指定包名下的所有"*.py"文件进行打包哟,但不会递归去拷贝所有的子包内容。
# 综上所述,我们如果想要把一个包的所有"*.py"文件进行打包,应该在packages列表写下所有包的层级关系哟~这样就开源将指定包路径的所有".py"文件进行打包!
packages=['devops', "devops.dev", "devops.ops"],
)

setup 函数常用的参数如下:

参数 说明
name 包名称
version 包版本
author 程序的作者
author_email 程序的作者的邮箱地址
maintainer 维护者
maintainer_email 维护者的邮箱地址
url 程序的官网地址
license 程序的授权信息
description 程序的简单描述
long_description 程序的详细描述
platforms 程序适用的软件平台列表
classifiers 程序的所属分类列表
keywords 程序的关键字列表
packages 需要处理的包目录(通常为包含 init.py 的文件夹)
py_modules 需要打包的 Python 单文件列表
download_url 程序的下载地址
cmdclass 添加自定义命令
package_data 指定包内需要包含的数据文件
include_package_data 自动包含包内所有受版本控制(cvs/svn/git)的数据文件
exclude_package_data 当 include_package_data 为 True 时该选项用于排除部分文件
data_files 打包时需要打包的数据文件,如图片,配置文件等
ext_modules 指定扩展模块
scripts 指定可执行脚本,安装时脚本会被安装到系统 PATH 路径下
package_dir 指定哪些目录下的文件被映射到哪个源码包
entry_points 动态发现服务和插件,下面详细讲
python_requires 指定运行时需要的Python版本
requires 指定依赖的其他包
provides 指定可以为哪些模块提供依赖
install_requires 安装时需要安装的依赖包
extras_require 当前包的高级/额外特性需要依赖的分发包
tests_require 在测试时需要使用的依赖包
setup_requires 指定运行 setup.py 文件本身所依赖的包
dependency_links 指定依赖包的下载地址
zip_safe 不压缩包,而是以目录的形式安装

更多参数可见:https://setuptools.readthedocs.io/en/latest/setuptools.html

2、classifiers 程序分类信息

classifiers 参数说明包的分类信息。

所有支持的分类列表见:https://pypi.org/pypi?%3Aaction=list_classifiers

示例:

1
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
from setuptools import setup, find_packages

setup(
classifiers = [
# 发展时期,常见的如下
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
'Development Status :: 3 - Alpha',

# 开发的目标用户
'Intended Audience :: Developers',

# 属于什么类型
'Topic :: Software Development :: Build Tools',

# 许可证信息
'License :: OSI Approved :: MIT License',

# 目标 Python 版本
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
]
)

3、依赖包安装

一个项目库可能会依赖于很多其他库,比如我们安装pandas,该库依赖于numpy。那我们用pip conda这些命令安装时,从来不用操心哪些依赖包需要安装,它们的版本限制是怎么样的,而这些信息是setuptools打包分发库时就确定的。

针对依赖包安装与版本管理这项功能,setup函数提供了一些参数install_requiressetup_requirestests_requireextras_require

1
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
30
31
32
from setuptools import setup, find_packages


setup(
...

# 1、表明当前模块依赖哪些包,若环境中没有,则会从pypi中下载安装
install_requires=['docutils>=0.3'],

# 2、setup.py 本身要依赖的包,这通常是为一些setuptools的插件准备的配置。这里列出的包,不会自动安装。
setup_requires=['pbr'],

# 3、仅在测试时需要使用的依赖,在正常发布的代码中是没有用的。
# 在执行python setup.py test时,可以自动安装这三个库,确保测试的正常运行。
tests_require=[
'pytest>=3.3.1',
'pytest-cov>=2.5.1',
],

# 4、 用于安装setup_requires或tests_require里的软件包。这些信息会写入egg的 metadata 信息中
dependency_links=[
"http://example2.com/p/foobar-1.0.tar.gz",
],

# 5、install_requires 在安装模块时会自动安装依赖包
# 而 extras_require 不会,这里仅表示该模块会依赖这些包
# 但是这些包通常不会使用到,只有当你深度使用模块时,才会用到,这里需要你手动安装
extras_require={
'PDF': ["ReportLab>=1.2", "RXP"],
'reST': ["docutils>=0.3"],
}
)

关于 install_requires, 有以下五种常用的表示方法:

  1. 'argparse',只包含包名。 这种形式只检查包的存在性,不检查版本。方便,但不利于控制风险。
  2. 'setuptools==38.2.4',指定版本。 这种形式把风险降到了最低,确保了开发、测试与部署的版本一致,不会出现意外。 缺点是不利于更新,每次更新都需要改动代码。
  3. 'docutils >= 0.3',这是比较常用的形式。 当对某个库比较信任时,这种形式可以自动保持版本为最新。
  4. 'Django >= 1.11, != 1.11.1, <= 2',这是比较复杂的形式。 如这个例子,保证了Django的大版本在1.11和2之间,也即1.11.x;并且,排除了已知有问题的版本1.11.1(仅举例)。 对于一些大型、复杂的库,这种形式是最合适的。
  5. 'requests[security, socks] >= 2.18.4',这是包含了额外的可选依赖的形式。 正常安装requests会自动安装它的install_requires中指定的依赖,而不会安装securitysocks这两组依赖。 这两组依赖是定义在它的extras_require中。 这种形式,用在深度使用某些库时。

4、安装环境的限制

有些库并不是在所有的 Python 版本中都适用的,若一个库安装在一个未兼容的 Python 环境中,理论上不应该在使用时才报错,而应该在安装过程就使其失败,提示禁止安装。

这样的功能,可以使用 python_requires 来实现。

1
2
3
4
setup(
...
python_requires='>=2.7, <=3',
)

5、生成脚本

有时候我们的库包含了一些非常重要的功能,每次都提供python XXX.py来运行不太方便,最好是把脚本放入系统环境path,以命令行的形式来执行。比如tensorRT就提供了trtexec命令。

那么setup函数提供了 entry_pointsscripts 这两个参数。它们的区别在于:

  • entry_points是把python文件中的函数自动生成为可执行脚本
  • scripts是把 **.sh、.py 等可执行脚本**生成到系统path中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from setuptools import setup


setup(
# 把python中的函数自动生成为一个可执行的脚本,放在 /usr/bin/foo 目录下
# 如下:把fool.main文件中的main函数自动生成为一个可执行脚本,可以通过命令foo执行该脚本
entry_points={
'console_scripts': [
'foo = foo.main:main'
]
},

# 将 bin/foo.sh 和 bar.py 脚本,生成到系统 PATH中
# 执行 python setup.py install 后会生成 /usr/bin/foo.sh 和 /usr/bin/bar.py
scripts=['bin/foo.sh', 'bar.py']
)

上面的 scripts 里有的脚本中有 shpy 后缀,那么安装后,setuptools 会原封不动的移动到 /usr/bin 中,并添加可执行权限。

若你想对这些文件再作一些更改,比如去掉多余的后缀,可以这样做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from setuptools.command.install_scripts import install_scripts

class InstallScripts(install_scripts):

def run(self):
setuptools.command.install_scripts.install_scripts.run(self)

# Rename some script files
for script in self.get_outputs():
if basename.endswith(".py") or basename.endswith(".sh"):
dest = script[:-3]
else:
continue
print("moving %s to %s" % (script, dest))
shutil.move(script, dest)

setup(
...
scripts=['bin/foo.sh', 'bar.py'],

cmdclass={
"install_scripts": InstallScripts
}
)

6、ext_modules C/C++ 扩展

ext_modules 参数用于构建 C 和 C++ 扩展扩展包。其是 Extension 实例的列表,每一个 Extension 实例描述了一个独立的扩展模块,扩展模块可以设置扩展包名,头文件、源文件、链接库及其路径、宏定义和编辑参数等。如:

1
2
3
4
5
6
7
8
9
setup(
# other arguments here...
ext_modules=[
Extension('foo',
glob(path.join(here, 'src', '*.c')),
libraries = [ 'rt' ],
include_dirs=[numpy.get_include()])
]
)

详细了解可参考:https://docs.python.org/3.6/distutils/setupscript.html#preprocessor-options

7、指定release

setup.py 里只能指定 version,而不能指定 release,如果你需要变更版本号,可以使用 --release 参数进行指定

1
python setup.py bdist_rpm --release=20200617

8、自定义命令行为

自定义命令行为是setuptools进阶知识。setuptools包括许多命令,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Standard commands:
build build everything needed to install
build_py "build" pure Python modules (copy to build directory)
build_ext build C/C++ extensions (compile/link to build directory)
build_clib build C/C++ libraries used by Python extensions
build_scripts "build" scripts (copy and fixup #! line)
clean clean up temporary files from 'build' command
install install everything from build directory
install_lib install all Python modules (extensions and pure Python)
install_headers install C/C++ header files
install_scripts install scripts (Python or otherwise)
install_data install data files
sdist create a source distribution (tarball, zip file, etc.)
register register the distribution with the Python package index
bdist create a built (binary) distribution
bdist_dumb create a "dumb" built distribution
bdist_rpm create an RPM distribution
bdist_wininst create an executable installer for MS Windows
upload upload binary package to PyPI
Extra commands:
见:https://pythonhosted.org/an_example_pypi_project/setuptools.html

这些命令具体是由定义在setuptools.command中的类执行的。比如python setup.py bdist由setuptools.command.bdist类来执行。因此我们可以继承于setuptools.command中的类来执行自定义的命令行为。比如pytorch的Build.Extension就继承于setuptools.command.build_ext。具体怎么继承并改写这个command类就需要阅读源码了。

继承完command类后,需要通过cmdclass参数告诉setuptools,该参数为一个字典,key为str命令名,value为继承于command类。

1
2
3
4
5
6
7
from setuptools import setup
import setuptools.command.build_ext as build_ext

class BuildExtension(build_ext,object):
…………

setup( cmdclass={'build_ext': BuildExtension} )

03-Python 打包工具 setuptools setup.py 文件详解
https://flepeng.github.io/021-Python-13-pip、包管理-02-打包工具-setuptools-03-Python-打包工具-setuptools-setup-py-文件详解/
作者
Lepeng
发布于
2021年7月31日
许可协议