setuptools详解
setuptools是什么?
简单点来说,setuptools
是帮助我们进行构建分发包或者说是模块的一个工具,主要是面向开发者的,方便开发者将自己的模块或程序编译成package(包)并共享。例如在使用python进行开发过程中,我们pip install
或者使用源码(python setup.py install)安装的模块。
setuptools的安装
归根结底,setuptools是python的一个模块,我们可以使用pip进行安装或者升级:
pip install setuptools # 安装
pip install --upgrade setuptools # 升级
一般而言,当安装完python和pip后,默认会自带setuptools模块,无需再手动安装。
setuptools的使用
上述说到,setuptools方便了共享Python模块或者程序,那么共享Python库或程序的第一步是构建分发包。这包括添加一组包含元数据和配置的文件,以便指导setuptools如何构建发行版。
因此,setuptools的使用最关键的便是包含元数据及配置的文件,目前官方有三种类型的配置文件类型,分别为pyproject.toml
、setup.cfg
、setup.py
,目前比较常用的是setup.cfg
和setup.py
,相比较setup.py
,官方更推荐使用setup.cfg
。下面将介绍一下setup.cfg
配置文件中的源数据及配置:
- file: 文件格式,一般以相对位置
- list: 列表格式,示例如下:
classifiers =
Development Status :: 3 - Alpha
Environment :: Console
- section:从特定部分读取值,类似于ini文件格式,示例如下:
[options.extras_require]
test =
flake8>=3.6.0
flake8-blind-except
test2 =
pytest
pytest-cov
参考:https://setuptools.pypa.io/en/latest/references/keywords.html,
https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
metadata(元数据)
元数据中主要包含静态数据
- name:项目名称,str
- version:项目版本,file、str
- url:项目的官方网站, str
- download_url:项目的下载地址,str
- project_urls:项目链接, 主要包含项目的git地址(GitHub)、更新日志(Changelog)等, dict。示例如下:
project_urls =
Changelog = https://github.com/**/changelog.rst
GitHub = https://github.com/**
- author:项目的作者名称,str
- author_email: 项目作者的邮箱,str
- maintainer:项目维护者名称,str
- maintainer_email:项目维护者邮箱,str
- classifiers:程序所属的分类,主要包含程序的支持平台、所用语言、所用认证等,file,list。示例如下:
classifiers =
Development Status :: 3 - Alpha
Environment :: Console
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
Operating System :: MacOS
Operating System :: Microsoft :: Windows
Operating System :: POSIX
Programming Language :: Python
Topic :: Software Development :: Build Tools
- license:程序的授权信息, str
- license_files:程序的授权文件(不常用),list
- description:项目描述,file, str
- long_description:项目的详细描述,file, str
- long_description_content_type:项目的描述(不常用),str
- keywords:项目的关键字,可为多个,list
- platforms:所支持的平台,list
- provides:指定可以为哪些模块提供依赖,list
- requires:指定依赖的其他包,list
- obsoletes:描述此包之前的包的字符串列表,适用于当包修改过名称 list
option(配置)
- zip_safe:不压缩包,以目录的形式安装,bool,建议选择False
- setup_requires:指定运行setup时所需的依赖
- install_requires:安装时需要安装的依赖, list,示例如下:
install_requires =
coloredlogs; sys_platform == 'win32'
distlib
EmPy
- extras_require:当前包的额外特性需要依赖的包,file,section,示例如下:
[options.extras_require]
test =
flake8>=3.6.0
flake8-blind-except
flake8-builtins
- python_requires:指定项目依赖的 Python 版本,str
- entry_points:接入点,主要用于发现服务和插件,也可用于生成控制台脚本,比较重要的,file,section
- scripts:指定可执行的脚本,简单点说就是让项目变成一个工具,是entry_points的基础版本,list
- eager_resources:list
- dependency_links:指定依赖包的下载地址,list
- tests_require:测试所需的依赖包,list
- include_package_data:是否安装包中包含的数据文件,bool
- packages:列出项目内需要被打包的所有 package。一般使用 find:自动发现,find:,find_namespace:,list,find:代表自发现包,默认会自动发现当前目录的包
- package_dir:指定哪些目录下的文件被映射到哪个源码包,dict
- package_data:指定包内包含的数据文件,section
- exclude_package_data:排除的安装包中的数据文件,section
- namespace_packages:list
- py_modules:需要打包的 Python 单文件列表,list
- data_files:打包时需要打包的数据文件,如图片,配置文件等,section,已弃用
- ext_modules:指定扩展的模块,针对的是使用 C/C++ 底层语言所写的模块
setup.cfg常用模板
[metadata]
name = my_package
version = attr: my_package.VERSION
description = My package description
long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst
keywords = one, two
license = BSD 3-Clause License
classifiers =
Framework :: Django
Programming Language :: Python :: 3
[options]
zip_safe = False
include_package_data = True
packages = find:
install_requires =
requests
importlib-metadata;python_version<"3.8"
[options.package_data]
* = *.txt, *.rst
hello = *.msg
[options.entry_points]
console_scripts =
executable-name = my_package.module:function
[options.extras_require]
pdf = ReportLab>=1.2; RXP
rest = docutils>=0.3; pack ==1.1, ==1.3
[options.packages.find]
exclude =
examples*
tools*
docs*
my_package.tests*
setuptools最佳实践
- 开启包的自发现功能
它会发现本目录下的所有Python包。正常使用第一种即可, 需要注意包中需要__init__.py
。
第一种
[options]
packages = find:
第二种
另外include和exclude也可以使用正则
下面这种方式是针对于特定设置
[options.packages.find]
where=src # 查找的位置 默认为.,代表当前目录
include=mypackage* # 位于查找的位置包含的包 默认为*,代表全部的包
exclude=mypackage.tests* # 位于查找的位置去除的包 默认为空,不加载的包
注意:包需要在文件夹内包含__init__.py文件,如果包中没有__init__.py文件,find自发现包时将会忽略,使用find_namespace:可以识别自发现的包,不过建议使用find:。
当需要发现的包不在根目录时,可以使用package_dir指定某个包位于哪个文件夹,示例如下:
[options]
package_dir =
= src # 代表所有的包位于src文件夹下,与上述的where参数类似
[options]
package_dir =
mypkg = lib # 代表mypkg包位于lib文件夹下
mypkg.subpkg1 = lib1 # 代表mypkg.subpkg1位于lib1文件夹下
- 支持生成控制台脚本
如果将脚本指定为入口点,则会执行相应包中的内容,一大部分可以替代python -m module
的操作,示例如下,打包完成后会生成cli-name可执行程序,执行此可执行程序将会调用mypkg.mymodule包中的some_func:
[options.entry_points]
console_scripts =
hello-world = timmins:hello_world
setuptools生成控制台脚本示例
此处以hello, world为例,如图是hello,world示例的目录结构
│ setup.cfg
│ setup.py
│
└─hello
hello_impl.py
__init__.py
- setup.cfg:setup打包需要的声明式配置文件。
- setup.py: 打包脚本,在打包时会自动加载setup.cfg中的相关配置。
- hello:需要打包的package。
- hello_impl.py:实际的hello,world函数所在
- init.py:被主动识别为包必须,不可省略。
其中代码内容分别如下:
setup.cfg
[metadata]
name = hello-world
version = "1.0.0"
description = "helloworld description"
[option]
python_requires = >=3.6
packages = find:
zip_safe = false
# 此处申明了一个可执行程序名为hello-world,指向hello.hello_impl包中的say函数
[options.entry_points]
console_scripts =
hello-world = hello.hello_impl:say
setup.py
from setuptools import setup
setup()
hello_impl.py
def say():
print("hello world")
__init__.py
:此处是个空文件,仅作识别包用。
正常情况下我们需要输出的话需要写main方法并调用say方法,并且使用python hello_impl.py
来输出,当我们设置了entry_points的console_scripts后,在此目录中执行python setup.py install
会生成对应程序的exe文件并自动添加到python所在文件夹的Scripts文件夹中,之后可以直接在命令行中输入exe名称即可,此处就为hello-world。
注意: 在python中一个文件夹想要被识别为包,必须要加__init__.py
文件,哪怕是空文件也必须要添加,否则将无法正常识别到包及包中的函数。
setuptools打包命令
可通过如下命令查看到详细帮助
python setup.py --help-commands
详细参数:
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
check perform some checks on the package
upload upload binary package to PyPI
Extra commands:
build_sphinx Build Sphinx documentation
alias define a shortcut to invoke one or more commands
bdist_egg create an "egg" distribution
develop install package in 'development mode'
dist_info create a .dist-info directory
easy_install Find/get/install Python packages
egg_info create a distribution's .egg-info directory
install_egg_info Install an .egg-info directory for the package
rotate delete older distributions, keeping N newest files
saveopts save supplied options to setup.cfg or other config file
setopt set an option in setup.cfg or another config file
test run unit tests after in-place build (deprecated)
upload_docs Upload documentation to sites other than PyPi such as devpi
compile_catalog compile message catalogs to binary MO files
extract_messages extract localizable strings from the project code
init_catalog create a new catalog based on a POT file
update_catalog update message catalogs from a POT file
usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
or: setup.py --help [cmd1 cmd2 ...]
or: setup.py --help-commands
or: setup.py cmd --help
基于setuptools的entry_points如何进行扩展?
我们接着之前的hello,world示例,之前say方法仅仅是说了hello,world,我们希望之后可以说一些其他的内容,例如我们需要支持第三方开发者进行扩展,让第三方开发者可以在不修改源码的情况下可以自定义内容。
下面首先介绍一下entry_points:
entry_points
中文名为入口点,是一种元数据,可以在安装时由包公开,可以通过如下两种方式获取所有的entry_points:
- 使用importlib.metadata中的entry_points获取当前电脑中安装的所有entry_points,一般来说,一个python模块至少有一个entry_points。具体使用如下:
from importlib.metadata import entry_points
display_eps = entry_points()
for item in display_eps:
print(item) - 使用pkg_resources的iter_entry_points方法获取特定entry_points,需要注意的是返回的是一个生成器,需要使用遍历或者转换为列表、集合等才可以正常显示。
from pkg_resources import iter_entry_points
display_eps = list(iter_entry_points(group))
entry_points在python生态系统中有着非常重要的作用,尤其如下两点: - 支持生成控制台脚本,希望直接在命令行中执行的命令,例如上方的hello,world示例中将hello,world包装成一个可执行程序,方便直接在命令行中运行。
- 支持软件包的自定义插件。也可以作为软件包的解耦,例如ROS2官方推荐的编译工具colcon,colcon中的build过程,源代码包为colcon-core,其中只包含了基础的操作命令,其他的例如discovery、 package selection,详细参考:https://colcon.readthedocs.io/en/released/reference/verb/build.html?highlight=colcon-core#build-build-packages,均是通过entrypoints动态加载的。
介绍完entry_points后,下来进行对于hello-world示例的升级。
假设存在如下hello,world目录结构,此包就是提供给其他人进行扩展,核心就在于它本身基于entry_points获取所有的接入点及对应调用函数。
│ setup.cfg
│ setup.py
│
├─hello
│ │ hello_impl.py
│ │ __init__.py
setup.cfg
[metadata]
name = hello-world
version = "1.0.0"
description = "helloworld description"
[option]
python_requires = >=3.6
packages = find:
zip_safe = false
include_package_data = True
[options.entry_points]
console_scripts =
hello-world = hello.hello_impl:main
setup.py:与上述生成控制台脚本示例的setup一致,仅作安装导入setup.cfg配置使用。
__init__.py
:作为识别包用,空文件。
hello_impl.py
from pkg_resources import iter_entry_points
def get_entry_points():
ret = {}
group = "hello_world.hello"
# 获取此电脑python环境中的接入点为hello_world.hello的所有接入点
for entry_point in iter_entry_points(group):
# 对entry_point的load是获取注册接入点对应的function
ret[entry_point.name] = entry_point.load()
return ret
def main():
# 获取所有的接入点的方法并执行
for func in get_entry_points().values():
func()
基于上述的hello,world使用python setup.py develop
进行编译后可以产生一个hello-world可执行程序,执行时会发现没有任何的输出,因为没有相应的接入点导入。
下面注册一个接入点为hello_world.hello的hello-world2项目,目录结构与hello-world类似,如下:
│ setup.cfg
│ setup.py
│
└─hello2
hello_impl_custom.py
__init__.py
setup.cfg:与setup.cfg类似,区别在于项目名称。最主要的是注册了一个hello_world.hello的接入点,并且调用的是包hello2.hello_impl_custom的say方法。
[metadata]
name = hello-world2
version = "1.0.0"
description = "helloworld description"
[option]
python_requires = >=3.6
packages = find:
zip_safe = false
[options.entry_points]
hello_world.hello =
hello-world_custom_say2 = hello2.hello_impl_custom:say
console_scripts =
hello-world2 = hello2.hello_impl_custom:main
setup.py
:与上述生成控制台脚本示例的setup一致,仅作安装导入setup.cfg配置使用。
__init__.py
:作为识别包用,空文件
hello_impl_custom.py
def say():
print("hello_impl_custom say2")
def main():
say()
使用python setup.py develop编译完后,会生成hello-world2程序,这时电脑中会有一个接入点:
hello-world_custom_say2 = hello2.hello_impl_custom:say
这时,再次使用hello-world时会输出hello_impl_custom say2
,由此可见,虽然hello-world中没有对应输出内容,通过entry_points
动态加载了hello-world2
的对应内容。
对于entry_points来说,当你注册了一个接入点时,会在当前python环境下的新增一个新的接入点,所有的接入点会形成一个集合,当你需要使用接入点时,只需使用pkg_resources 获取符合自己的接入点,一般而言,所有python模块均有权限获取对应的接入点内容,获取结果的成功与否取决于包名是否正确,接入点的格式如下:
<user_defined_key> = <python_package:object_name>"
<user_defined_key>
是开发者定义的跟业务高度相关的 Key,正常保证不重复即可,<python_package:object_name> python_package
:为python的包名,注意是相对路径,object_name:为对象名,一般为函数名或者类名。
接入点的操作示意图如下:接入点可以被多个模块所添加,当然获取接入点也可以被多个模块所获取。第三方扩展支持其实就相当于添加接入点的新程序,支持扩展的程序则会在执行时每次从当前python环境中获取对应包的所有接入点,从而进行相应处理。
setup进行打包时如何添加除python文件外的资源文件
需要使用MANIFEST.in
文件,与setup.py
文件同级,其中存放需要添加的资源文件,主要使用include参数,详细参数可见MANIFEST.in参数介绍
需要注意的是添加资源文件以MANIFEST.in
作为开始的路径,且需要到达资源文件的上级目录,例如需要添加hello/test/test.ini文件,则可以进行如下配置:
include hello/test/*
如何打包成whl包?
安装wheel模块
pip install wheel
在setup.py所在路径进行打包
python .\setup.py sdist bdist_wheel