Python 庫打包分發(setup.py 編寫)簡易指南
Python 有非常豐富的第三方庫可以使用,很多開發者會向 pypi 上提交自己的 Python 包。要想向 pypi 包倉庫提交自己開發的包,首先要將自己的代碼打包,才能上傳分發。
distutils 簡介
distutils
是標准庫中負責建立 Python 第三方庫的安裝器,使用它能夠進行 Python 模塊的安裝和發布。distutils 對於簡單的分發很有用,但功能缺少。大部分Python用戶會使用更先進的setuptools模塊
setuptools 簡介
setuptools
是 distutils 增強版,不包括在標准庫中。其擴展了很多功能,能夠幫助開發者更好的創建和分發 Python 包。大部分 Python 用戶都會使用更先進的 setuptools 模塊。
Setuptools 有一個 fork 分支是 distribute。它們共享相同的命名空間,因此如果安裝了 distribute,import setuptools
時實際上將導入使用 distribute 創建的包。Distribute 已經合並回 setuptools。
還有一個大包分發工具是 distutils2,其試圖嘗試充分利用distutils,detuptools 和 distribute 並成為 Python 標准庫中的標准工具。但該計划並沒有達到預期的目的,且已經是一個廢棄的項目。
因此,setuptools 是一個優秀的,可靠的 Pthon 包安裝與分發工具。以下設計到包的安裝與分發均針對 setuptools,並不保證 distutils 可用。
包格式
Python 庫打包的格式包括 Wheel
和 Egg
。Egg 格式是由 setuptools 在 2004 年引入,而 Wheel 格式是由 PEP427 在 2012 年定義。使用 Wheel
和 Egg
安裝都不需要重新構建和編譯,其在發布之前就應該完成測試和構建。
Egg
和 Wheel
本質上都是一個 zip 格式包,Egg 文件使用 .egg
擴展名,Wheel 使用 .whl
擴展名。Wheel 的出現是為了替代 Egg,其現在被認為是 Python 的二進制包的標准格式。
以下是 Wheel 和 Egg 的主要區別:
- Wheel 有一個官方的 PEP427 來定義,而 Egg 沒有 PEP 定義
- Wheel 是一種分發格式,即打包格式。而 Egg 既是一種分發格式,也是一種運行時安裝的格式,並且是可以被直接 import
- Wheel 文件不會包含 .pyc 文件
- Wheel 使用和 PEP376 兼容的 .dist-info 目錄,而 Egg 使用 .egg-info 目錄
- Wheel 有着更豐富的命名規則。
- Wheel 是有版本的。每個 Wheel 文件都包含 wheel 規范的版本和打包的實現
- Wheel 在內部被 sysconfig path type 管理,因此轉向其他格式也更容易
詳細描述可見:Wheel vs Egg
setup.py 文件
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
find_packages
對於簡單工程來說,手動增加 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
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_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 命令
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.cfg
文件用於提供 setup.py 的默認參數,詳細的書寫規則可參考:https://docs.python.org/3/distutils/configfile.html
版本命名
包版本的命名格式應為如下形式:
N.N[.N]+[{a|b|c|rc}N[.N]+][.postN][.devN]
從左向右做一個簡單的解釋:
- "N.N": 必須的部分,兩個 "N" 分別代表了主版本和副版本號
- "[.N]": 次要版本號,可以有零或多個
- "{a|b|c|rc}": 階段代號,可選, a, b, c, rc 分別代表 alpha, beta, candidate 和 release candidate
- "N[.N]": 階段版本號,如果提供,則至少有一位主版本號,后面可以加無限多位的副版本號
- ".postN": 發行后更新版本號,可選
- ".devN": 開發期間的發行版本號,可選
easy_install 與 pip
easy_insall
是 setuptool 包提供的第三方包安裝工具,而 pip
是 Python 中一個功能完備的包管理工具,是 easy_install 的改進版,提供更好的提示信息,刪除包等功能。
pip 相對於 easy_install 進行了以下幾個方面的改進:
- 所有的包是在安裝之前就下載了,所以不可能出現只安裝了一部分的情況
- 在終端上的輸出更加友好
- 對於動作的原因進行持續的跟蹤。例如,如果一個包正在安裝,那么 pip 就會跟蹤為什么這個包會被安裝
- 錯誤信息會非常有用
- 代碼簡潔精悍可以很好的編程
- 不必作為 egg 存檔,能扁平化安裝(仍然保存 egg 元數據)
- 原生的支持其他版本控制系統(Git, Mercurial and Bazaar)
- 加入卸載包功能
- 可以簡單的定義修改一系列的安裝依賴,還可以可靠的賦值一系列依賴包
發布包
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
參考資料
- Building and Distributing Packages with Setuptools
- https://blog.zengrong.net/post/2169.html
- Differences between distribute, distutils, setuptools and distutils2?
- https://www.ibm.com/developerworks/cn/opensource/os-pythonpackaging/index.html
- http://python-packaging-zh.readthedocs.io/zh_CN/latest/index.html
- How to extend distutils with a simple post install script?
- Why use pip over easy_install?