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.tomlsetup.cfgsetup.py,目前比较常用的是setup.cfgsetup.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:

  1. 使用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)
  2. 使用pkg_resources的iter_entry_points方法获取特定entry_points,需要注意的是返回的是一个生成器,需要使用遍历或者转换为列表、集合等才可以正常显示。
    from pkg_resources import iter_entry_points
    display_eps = list(iter_entry_points(group))
    entry_points在python生态系统中有着非常重要的作用,尤其如下两点:
  3. 支持生成控制台脚本,希望直接在命令行中执行的命令,例如上方的hello,world示例中将hello,world包装成一个可执行程序,方便直接在命令行中运行。
  4. 支持软件包的自定义插件。也可以作为软件包的解耦,例如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
posted @ 2022-12-20 14:25  形同陌路love  阅读(2560)  评论(0编辑  收藏  举报