Python 有很是丰富的第三方库可使用,不少开发者会向 pypi 上提交本身的 Python 包。要想向 pypi 包仓库提交本身开发的包,首先要将本身的代码打包,才能上传分发。python
distutils 是标准库中负责创建 Python 第三方库的安装器,使用它可以进行 Python 模块的安装和发布。distutils 对于简单的分发颇有用,但功能缺乏。大部分Python用户会使用更先进的setuptools模块git
setuptools 是 distutils 加强版,不包括在标准库中。其扩展了不少功能,可以帮助开发者更好的建立和分发 Python 包。大部分 Python 用户都会使用更先进的 setuptools 模块。编程
Setuptools 有一个 fork 分支是 distribute。它们共享相同的命名空间,所以若是安装了 distribute,import setuptools 时实际上将导入使用 distribute 建立的包。Distribute 已经合并回 setuptools。flask
还有一个大包分发工具是 distutils2,其试图尝试充分利用distutils,detuptools 和 distribute 并成为 Python 标准库中的标准工具。但该计划并无达到预期的目的,且已是一个废弃的项目。bash
所以,setuptools 是一个优秀的,可靠的 Pthon 包安装与分发工具。如下设计到包的安装与分发均针对 setuptools,并不保证 distutils 可用。ide
Python 库打包的格式包括 Wheel 和 Egg。Egg 格式是由 setuptools 在 2004 年引入,而 Wheel 格式是由 PEP427 在 2012 年定义。使用 Wheel 和 Egg 安装都不须要从新构建和编译,其在发布以前就应该完成测试和构建。svn
Egg 和 Wheel 本质上都是一个 zip 格式包,Egg 文件使用 .egg 扩展名,Wheel 使用 .whl 扩展名。Wheel 的出现是为了替代 Egg,其如今被认为是 Python 的二进制包的标准格式。函数
如下是 Wheel 和 Egg 的主要区别:工具
详细描述可见:Wheel vs Egg
Python 库打包分发的关键在于编写 setup.py 文件。setup.py 文件编写的规则是从 setuptools 或者 distuils 模块导入 setup 函数,并传入各种参数进行调用。
# coding:utf-8 from setuptools import setup # or # from distutils.core import setup setup( name='demo', # 包名字 version='1.0', # 包版本 description='This is a test of the setup', # 简单描述 author='huoty', # 做者 author_email='sudohuoty@163.com', # 做者邮箱 url='https://www.konghy.com', # 包的主页 packages=['demo'], # 包 )
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 | 指定哪些目录下的文件被映射到哪一个源码包 |
requires | 指定依赖的其余包 |
provides | 指定能够为哪些模块提供依赖 |
install_requires | 安装时须要安装的依赖包 |
entry_points | 动态发现服务和插件,下面详细讲 |
setup_requires | 指定运行 setup.py 文件自己所依赖的包 |
dependency_links | 指定依赖包的下载地址 |
extras_require | 当前包的高级/额外特性须要依赖的分发包 |
zip_safe | 不压缩包,而是以目录的形式安装 |
更多参数可见:https://setuptools.readthedocs.io/en/latest/setuptools.html
对于简单工程来讲,手动增长 packages 参数是容易。而对于复杂的工程来讲,可能添加不少的包,这是手动添加就变得麻烦。Setuptools 模块提供了一个 find_packages 函数,它默认在与 setup.py 文件同一目录下搜索各个含有 init.py 的目录作为要添加的包。
find_packages(where='.', exclude=(), include=('*',))
find_packages 函数的第一个参数用于指定在哪一个目录下搜索包,参数 exclude 用于指定排除哪些包,参数 include 指出要包含的包。
默认默认状况下 setup.py 文件只在其所在的目录下搜索包。若是不用 find_packages,想要找到其余目录下的包,也能够设置 package_dir 参数,其指定哪些目录下的文件被映射到哪一个源码包,如: package_dir={'': 'src'} 表示 “root package” 中的模块都在 src 目录中。
package_data:该参数是一个从包名称到 glob 模式列表的字典。若是数据文件包含在包的子目录中,则 glob 能够包括子目录名称。其格式通常为 {'package_name': ['files']},好比:package_data={'mypkg': ['data/*.dat'],}。
include_package_data:该参数被设置为 True 时自动添加包中受版本控制的数据文件,可替代 package_data,同时,exclude_package_data 能够排除某些文件。注意当须要加入没有被版本控制的文件时,仍是仍然须要使用 package_data 参数才行。
data_files:该参数一般用于包含不在包内的数据文件,即包的外部文件,如:配置文件,消息目录,数据文件。其指定了一系列二元组,即(目的安装目录,源文件) ,表示哪些文件被安装到哪些目录中。若是目录名是相对路径,则相对于安装前缀进行解释。
manifest template:manifest template 即编写 MANIFEST.in 文件,文件内容就是须要包含在分发包中的文件。一个 MANIFEST.in 文件以下:
include *.txt recursive-include examples *.txt *.py prune examples/sample?/build MANIFEST.in 文件的编写规则可参考:https://docs.python.org/3.6/distutils/sourcedist.html
有两个参数 scripts 参数或 console_scripts 可用于生成脚本。
entry_points 参数用来支持自动生成脚本,其值应该为是一个字典,从 entry_point 组名映射到一个表示 entry_point 的字符串或字符串列表,如:
setup( # other arguments here... entry_points={ 'console_scripts': [ 'foo=foo.entry:main', 'bar=foo.entry:main', ], } )
scripts 参数是一个 list,安装包时在该参数中列出的文件会被安装到系统 PATH 路径下。如:
scripts=['bin/foo.sh', 'bar.py']
用以下方法能够将脚本重命名,例如去掉脚本文件的扩展名(.py、.sh):
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( # other arguments here... cmdclass={ "install_scripts": InstallScripts } )
其中,cmdclass 参数表示自定制命令,后文详述。
ext_modules 参数用于构建 C 和 C++ 扩展扩展包。其是 Extension 实例的列表,每个 Extension 实例描述了一个独立的扩展模块,扩展模块能够设置扩展包名,头文件、源文件、连接库及其路径、宏定义和编辑参数等。如:
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
zip_safe 参数决定包是否做为一个 zip 压缩后的 egg 文件安装,仍是做为一个以 .egg 结尾的目录安装。由于有些工具不支持 zip 压缩文件,并且压缩后的包也不方便调试,因此建议将其设为 False,即 zip_safe=False。
Setup.py 文件有不少内置的的命令,可使用 python setup.py --help-commands 查看。若是想要定制本身须要的命令,能够添加 cmdclass 参数,其值为一个 dict。实现自定义命名须要继承 setuptools.Command 或者 distutils.core.Command 并重写 run 方法。
from setuptools import setup, Command class InstallCommand(Command): description = "Installs the foo." user_options = [ ('foo=', None, 'Specify the foo to bar.'), ] def initialize_options(self): self.foo = None def finalize_options(self): assert self.foo in (None, 'myFoo', 'myFoo2'), 'Invalid foo!' def run(self): install_all_the_things() setup( ..., cmdclass={ 'install': InstallCommand, } )
若是包依赖其余的包,能够指定 install_requires 参数,其值为一个 list,如:
install_requires=[ 'requests>=1.0', 'flask>=1.0' ]
指定该参数后,在安装包时会自定从 pypi 仓库中下载指定的依赖包安装。
此外,还支持从指定连接下载依赖,即指定 dependency_links 参数,如:
dependency_links = [ "http://packages.example.com/snapshots/foo-1.0.tar.gz", "http://example2.com/p/bar-1.0.tar.gz", ]
classifiers 参数说明包的分类信息。全部支持的分类列表见:https://pypi.org/pypi?%3Aaction=list_classifiers
示例:
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.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', ]
setup.py 文件有不少内置命令可供使用,查看全部支持的命令:
python setup.py --help-commands
此处列举一些经常使用命令:
build:构建安装时所需的全部内容
sdist:构建源码分发包,在 Windows 下为 zip 格式,Linux 下为 tag.gz 格式 。执行 sdist 命令时,默认会被打包的文件:
全部 py_modules 或 packages 指定的源码文件 全部 ext_modules 指定的文件 全部 package_data 或 data_files 指定的文件 全部 scripts 指定的脚本文件 README、README.txt、setup.py 和 setup.cfg文件 该命令构建的包主要用于发布,例如上传到 pypi 上。
bdist:构建一个二进制的分发包。
bdist_egg:构建一个 egg 分发包,常常用来替代基于 bdist 生成的模式
install:安装包到系统环境中。
develop:以开发方式安装包,该命名不会真正的安装包,而是在系统环境中建立一个软连接指向包实际所在目录。这边在修改包以后不用再安装就能生效,便于调试。
register、upload:用于包的上传发布,后文详述。
setup.cfg 文件用于提供 setup.py 的默认参数,详细的书写规则可参考:https://docs.python.org/3/distutils/configfile.html
包版本的命名格式应为以下形式:
N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]
从左向右作一个简单的解释:
easy_insall 是 setuptool 包提供的第三方包安装工具,而 pip 是 Python 中一个功能完备的包管理工具,是 easy_install 的改进版,提供更好的提示信息,删除包等功能。
pip 相对于 easy_install 进行了如下几个方面的改进:
PyPI(Python Package Index) 是 Python 官方维护的第三方包仓库,用于统一存储和管理开发者发布的 Python 包。
若是要发布本身的包,须要先到 pypi 上注册帐号。而后建立 ~/.pypirc 文件,此文件中配置 PyPI 访问地址和帐号。如的.pypirc文件内容请根据本身的帐号来修改。
典型的 .pypirc 文件
[distutils] index-servers = pypi [pypi] username:xxx password:xxx
接着注册项目:
python setup.py register
该命令在 PyPi 上注册项目信息,成功注册以后,能够在 PyPi 上看到项目信息。最后构建源码包发布便可:
python setup.py sdist upload
setup.py 文件示例:
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import subprocess from setuptools import setup, Extension, find_packages from setuptools.command.build_ext import build_ext class CMakeExtension(Extension): def __init__(self, name, sourcedir=''): Extension.__init__(self, name, sources=[]) self.sourcedir = os.path.abspath(sourcedir) class CMakeBuild(build_ext): def run(self): for ext in self.extensions: self.build_extension(ext) def build_extension(self, ext): if not os.path.exists(self.build_temp): os.makedirs(self.build_temp) extdir = self.get_ext_fullpath(ext.name) if not os.path.exists(extdir): os.makedirs(extdir) # This is the temp directory where your build output should go install_prefix = os.path.abspath(os.path.dirname(extdir)) cmake_args = '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={}'.format(install_prefix) subprocess.check_call(['cmake', ext.sourcedir, cmake_args], cwd=self.build_temp) subprocess.check_call(['cmake', '--build', '.'], cwd=self.build_temp) setup( name='name', version='0.0.3', author='xxx', author_email='', description='', ext_modules=[CMakeExtension('.')], py_modules=['纯py模块的名称'], cmdclass=dict(build_ext=CMakeBuild), zip_safe=False )
publish.sh 示例:
echo start build rm -rf dist/* python setup.py sdist bdist_wheel twine upload --repository-url http://hostname/repository/pypi-hosted/ dist/* -u username -p password