Python打包系統簡單入門
最近把pyenv、pipenv這種都研究了一下,然後我發現一個嚴重的問題:就是我雖然看了半天這些工具,但是我對Python自己的打包系統卻完全沒有了解。所以這篇文章就來研究一下Python自帶的打包系統。
pip
先來詳細介紹一下pip的用法,平時基本上我們用pip的時候也就是一個pip install
。其實pip也有很多特性,在此先介紹一下常用的一些特性。此部分參考了pip文件,想了解更多的話可以看原文。
安裝
最常用的命令就是安裝了,除此以外還可以指定版本號:
$ pip install SomePackage # 不指定版本號,安裝最新版 $ pip install SomePackage==1.0.4 # 指定版本號 $ pip install 'SomePackage>=1.0.4' # 指定最小版本號 $ pip install -r requirements.txt # 從需求檔案安裝 $ pip install -e . # 從本地專案setup.py安裝
使用代理伺服器
當從官方的PyPI源安裝比較慢的時候,可以考慮使用代理伺服器,指定代理伺服器的方法有三種:
- 使用
--proxy
引數在命令列指定,代理格式為[user:[email protected]]proxy.server:port
。 - 在配置檔案中指定。
- 設定
http_proxy
,https_proxy
和no_proxy
環境變數。
使用需求檔案(requirements.txt)
在需要很多pip包的專案中,用pip一個個安裝包不是一個好辦法,這時候可以考慮使用需求檔案。
如果要生成需求檔案,用下面的命令。這會將當前Python環境中的所有包的當前版本狀態儲存下來,將來安裝的時候會精確還原到凍結的那個狀態。
pip freeze > requirements.txt
要從需求檔案中安裝,則是用下面的命令:
pip install -r requirements.txt
官方文件還給出了一個帶註釋的例項需求檔案:
# ####### example-requirements.txt ####### # ###### 沒有版本識別符號的包,會安裝最新版 ###### nose nose-cov beautifulsoup4 # ###### 帶版本識別符號的包 ###### # 版本識別符號的資料 https://www.python.org/dev/peps/pep-0440/#version-specifiers docopt == 0.6.1 # Version Matching. Must be version 0.6.1 keyring >= 4.1.1 # Minimum version 4.1.1 coverage != 3.5 # Version Exclusion. Anything except version 3.5 Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.* # ###### 還可以指定其他的需求檔案 ###### -r other-requirements.txt # # ###### 還可以指定本地貨網路上的特定包 ###### ./downloads/numpy-1.9.2-cp34-none-win32.whl http://wxpython.org/Phoenix/snapshot-builds/wxPython_Phoenix-3.0.3.dev1820+49a8884-cp34-none-win_amd64.whl # ###### Additional Requirements without Version Specifiers ###### # 和第一部分一樣,這裡這些部分沒有順序需求,可以隨意改變位置 rejected green #
版本識別符號用來指定包的版本,有以下幾個例子:
SomeProject
SomeProject == 1.3
SomeProject >=1.2,<.2.0
SomeProject[foo, bar]
SomeProject~=1.4.2
從6.0版本開始,pip也支援環境標記(也就是分號後面跟Python版本或者系統型別):
SomeProject ==5.4 ; python_version < '2.7'
SomeProject; sys_platform == 'win32'
解除安裝
解除安裝某個包使用下面的命令:
$ pip uninstall SomePackage
列出包
要列出所有已安裝的包:
$ pip list
docutils (0.9.1)
Jinja2 (2.6)
Pygments (1.5)
Sphinx (1.1.2)
要列出過時的包:
$ pip list --outdated
docutils (Current: 0.9.1 Latest: 0.10)
Sphinx (Current: 1.1.2 Latest: 1.1.3)
要列出某個已安裝的包的詳細資訊:
$ pip show sphinx
---
Name: Sphinx
Version: 1.1.3
Location: /my/env/lib/pythonx.x/site-packages
Requires: Pygments, Jinja2, docutils
搜尋
要搜尋一個包,用下面的命令,搜尋結果可能有很多:
$ pip search "query"
更新
要更新一個包,使用-U
或者--upgrade
引數:
pip install -U <pkg>
如果想更新所有的包,很遺憾,pip並沒有提供該功能,我在StackOverFlow上找到一個看起來比較簡單的解決辦法,就是在Python直譯器中執行下面的程式碼:
import pkg_resources
from subprocess import call
packages = [dist.project_name for dist in pkg_resources.working_set]
call("pip install --upgrade " + ' '.join(packages), shell=True)
以上就是pip的一些簡單用法,詳情可參考官方文件。
打包專案
下面就進入本文的正題,Python的打包系統上。基本上我們不需要完全瞭解打包系統,只要學會簡單的幾個點就可以打包自己的類庫了。打包需要distutils、setuptools、wheel等類庫,不過基本上我們只需要寫好其中最重要的setup.py,就可以完成打包工作了。distutils是官方的類庫,在當年有很廣泛的使用,不過到了現在很難用。distutuils類庫的核心就是setup函式,我們需要將專案的各種資訊作為引數傳遞給setup函式,然後就可以用相關命令建立專案分發包了。關於distutils的用法,可以參考官方文件。
當然現在專案基本都不用distutils了,有更好用的第三方替代品,那就是setuptools,它可以算作是distutils的加強版,功能更加強大、使用更加簡單,這就是這裡要介紹的。其實從文件就可以看出來,distutils畢竟時間比較早,有些介面設計的不太合理甚至有些反人類,setuptools的文件就簡單多了。
準備專案
為了做演示,首先需要準備一個專案,一個專案應該包括README和LICENSE等檔案,README檔案是Markdown格式的文字檔案,用於描述專案自身;LICENSE檔案是授權檔案,列出專案使用者應該遵循的各種條款。下圖是我的專案結構。
此外還可能存在幾個檔案:
- setup.cfg。對應的配置檔案,一般情況下可以不要。
- MANIFEST.in。清單檔案,當專案中需要一些沒辦法自動包括到原始碼分發包的檔案時,可能需要用到它。
具體檔案內容就不列出了。需要注意my_package/__init__.py
檔案中應該有如下一行標識包名:
name = 'yitian_first_package'
編寫setup.py檔案
用setuptools來編寫setup.py檔案是一件非常簡單的事情,而且有很多例子可供參考,我挑選了Kenneth Reitz(requests、pipenv等類庫的作者)寫的例子,做了一些修改並翻譯了一些註釋:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 注意 如果要使用上傳功能,需要安裝twine包:
# $ pip install twine
import io
import os
import sys
from shutil import rmtree
from setuptools import find_packages, setup, Command
# 包的元資訊
NAME = 'yitian_first_package'
DESCRIPTION = '專案的簡短描述,不超過200字元'
URL = 'https://github.com/techstay/python-study'
EMAIL = '[email protected]'
AUTHOR = '易天'
REQUIRES_PYTHON = '>=3.6.0'
VERSION = '0.1.0'
KEYWORDS = 'sample setuptools development'
# 專案依賴,也就是必須安裝的包
REQUIRED = [
'requests-html'
]
# 專案的可選依賴,可以不用安裝
EXTRAS = {
# 'fancy feature': ['django'],
}
# 剩下部分不用怎麼管 :)
# ------------------------------------------------
# 除了授權和授權檔案識別符號!
# 如果你改了License, 記得也相應修改Trove Classifier!
here = os.path.abspath(os.path.dirname(__file__))
# 匯入README檔案作為專案長描述.
# 注意 這需要README檔案存在!
try:
with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
long_description = '\n' + f.read()
except FileNotFoundError:
long_description = DESCRIPTION
# 當前面沒指定版本號的時候,將包的 __version__.py 模組載入進來
about = {}
if not VERSION:
with open(os.path.join(here, NAME, '__version__.py')) as f:
exec(f.read(), about)
else:
about['__version__'] = VERSION
class UploadCommand(Command):
"""上傳功能支援"""
description = 'Build and publish the package.'
user_options = []
@staticmethod
def status(s):
"""Prints things in bold."""
print('\033[1m{0}\033[0m'.format(s))
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
try:
self.status('Removing previous builds…')
rmtree(os.path.join(here, 'dist'))
except OSError:
pass
self.status('Building Source and Wheel (universal) distribution…')
os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable))
self.status('Uploading the package to PyPI via Twine…')
os.system('twine upload dist/*')
self.status('Pushing git tags…')
os.system('git tag v{0}'.format(about['__version__']))
os.system('git push --tags')
sys.exit()
# 神奇的操作,一個函式完事
setup(
name=NAME,
version=about['__version__'],
description=DESCRIPTION,
long_description=long_description,
long_description_content_type='text/markdown',
author=AUTHOR,
author_email=EMAIL,
python_requires=REQUIRES_PYTHON,
url=URL,
keywords=KEYWORDS,
# 專案中要包括和要排除的檔案,setuptools可以自動搜尋__init__.py檔案來找到包
packages=find_packages(exclude=('tests',)),
# 如果專案中包含任何不在包中的單檔案模組,需要新增py_modules讓setuptools能找到它們:
# py_modules=['yitian_first_package'],
# entry_points={
# 'console_scripts': ['mycli=mymodule:cli'],
# },
install_requires=REQUIRED,
extras_require=EXTRAS,
# 老舊的distutils需要手動新增專案中需要的非程式碼檔案,setuptools可以用下面引數自動新增(僅限包目錄下)
include_package_data=True,
# 如果是包的子目錄下,則需要手動新增
package_data={
'yitian_first_package': ['static/*.html']
},
license='MIT',
classifiers=[
# Trove classifiers
# Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
'License :: OSI Approved :: MIT License',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy'
],
# $ setup.py publish support.
cmdclass={
'upload': UploadCommand,
},
)
下面再講一些在註釋裡沒法詳細解釋的東西,官方文件的內容更豐富,有需要的可以檢視。示例檔案中其實還有幾個setup引數沒寫全,這裡再補充一下。
project_urls
project_urls引數可以列出一些相關專案的URL。
project_urls={
'Documentation': 'https://packaging.python.org/tutorials/distributing-packages/',
'Funding': 'https://donate.pypi.org',
'Say Thanks!': 'http://saythanks.io/to/example',
'Source': 'https://github.com/pypa/sampleproject/',
'Tracker': 'https://github.com/pypa/sampleproject/issues',
},
python_requires引數格式就是pip中指定包版本的識別符號,,指定我們專案支援的Python版本,這裡再補充幾個例子。
# 大版本號大於等於3
python_requires='>=3',
# 版本號大於等於3.3,但是不能超過4
python_requires='~=3.3',
# 支援2.6 2.7以及所有以3.3開頭的Python 3版本
python_requires='>=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4',
package_data和data_file
package_data和data_file引數用於指定資料檔案,也就是在專案中使用到的非程式碼檔案,一般情況下通過設定include_package_data=True
自動搜尋就夠用了,如果需要細粒度的控制,就要使用它們了,詳情見setuptools 文件 - Including Data Files。
package_data指定包括在包中的資料檔案,也就是“包資料檔案”,這些檔案會複製到包的相應目錄。
package_data={
'package_name': ['package_data.dat'],
},
data_files指定放在包外的資料檔案,這些檔案會被複制到專案根目錄下指定的相對目錄中。
data_files=[('my_data', ['data/data_file'])],
entry_points
entry_points引數指定一些入口點,可以看做是專案提供的一些額外功能,其中最常見的就是console_scripts,用於註冊指令碼介面。setuptools提供的工具鏈可以在安裝專案分發包的時候將這些介面轉化為真正的可執行指令碼,更多資訊參考setuptools文件 - Automatic Script Creation。
entry_points={
'console_scripts': [
'sample=sample:main',
],
},
版本號
下面是開發、A測、B測、釋出候選、最終釋出等情況的版本號例項。
1.2.0.dev1 # Development release
1.2.0a1 # Alpha Release
1.2.0b1 # Beta Release
1.2.0rc1 # Release Candidate
1.2.0 # Final Release
1.2.0.post1 # Post Release
15.10 # Date based release
23 # Serial release
開發模式
setup.py檔案寫完之後,專案就算是可打包狀態了。當然也可以繼續在專案上進行工作,這時候一般希望專案既可以作為包來安裝,又希望專案是可以編輯的,這時候就可以進入開發模式。這種情況下需要用下面的命令來安裝包,-e
選項全稱是--editable
,也就是可編輯的意思;.
表示當前目錄,也就是setup.py存在的那個目錄:
pip install -e .
該命令會安裝install_requires
中指定的所有包,以及console_scripts
部分指定的指令碼。依賴項會作為普通包來安裝,而專案本身會以可編輯狀態來安裝。特別的,如果只希望安裝專案本身而不安裝所有依賴包,用下面的命令:
pip install -e . --no-deps
如果有需要的話,還可以安裝VCS或者本地目錄中儲存的包來替代官方索引中的包。詳情請檢視文件。
打包專案
終於到了觀看成果的時候了,專案可以被打包成各種型別的可分發包,這裡只介紹幾種最常用的。
原始碼分發包(sdist)
這是最低等級的一種,基本上就是複製原始碼,不過因此在安裝的時候有一個必須的構建(可能包括編譯)過程來生成各種元資訊,哪怕專案是純的Python專案。用下面的命令來生成:
python setup.py sdist
Wheels(輪子)
在程式設計界各種第三方包不是被形象地稱作輪子嗎(著名梗:不要重複造輪子),這裡就是這個意思。輪子是一種二進位制分發包,是現在最推薦的分發包格式,輪子又可以分為好幾種輪子。當然,在構建輪子之前,還需要安裝wheel
包來提供支援。
pip install wheel
通用輪子。也就是專案中只存在Python程式碼,同時相容Python 2和Python 3的輪子,用下面的命令生成。
python setup.py bdist_wheel --universal
當然也可以在setup.cfg配置檔案中指定:
[bdist_wheel]
universal=1
純Python輪子。和通用輪子差不多,不過只支援Python 2或者Python 3.
python setup.py bdist_wheel
平臺輪子。這種輪子中不僅有Python程式碼,一般還包括但不限於C程式碼寫成的擴充套件等,因此它們只支援特定平臺。
python setup.py bdist_wheel
執行以上命令之後,會在dist
資料夾中生成打包好的可釋出包。
釋出專案
專案打包完畢,生成可可分發包之後,最後一步就是釋出專案了。幾乎所有的專案都被髮布到了Python Package Index(簡稱PyPI)上了,當然如果有需求的話還可以搭建自己的私人索引,不過這就是另一個話題了。
很有意思的是,Python官方還提供了一個測試索引,它是一個和PyPI完全一樣的測試網站,定期清理,可以讓我們方便的練習上傳專案,同時不用擔心會汙染官方倉庫。使用方法很簡單,先註冊一個賬戶。
上傳專案需要用到另一個類庫twine:
pip install twine
然後用下面的命令將包上傳到測試索引中,該命令會提示輸入剛才註冊用的使用者名稱和密碼:
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
稍等片刻,上傳應該就完成了。然後就可以在測試索引中找到我的專案了。當然由於測試索引會定期清理的緣故,可能過段時間專案和我的賬戶就都不存在了。
全部流程都熟悉之後,就可以在官方索引上註冊賬號,並將專案上傳上去,這樣一來,全世界的開發者都能用到你的專案了!