最近把pyenv、pipenv這種都研究了一下,而後我發現一個嚴重的問題:就是我雖然看了半天這些工具,可是我對Python本身的打包系統卻徹底沒有了解。因此這篇文章就來研究一下Python自帶的打包系統。html
先來詳細介紹一下pip的用法,平時基本上咱們用pip的時候也就是一個pip install
。其實pip也有不少特性,在此先介紹一下經常使用的一些特性。此部分參考了pip文檔,想了解更多的話能夠看原文。python
最經常使用的命令就是安裝了,除此之外還能夠指定版本號:git
$ 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源安裝比較慢的時候,能夠考慮使用代理服務器,指定代理服務器的方法有三種:github
--proxy
參數在命令行指定,代理格式爲[user:passwd@]proxy.server:port
。http_proxy
, https_proxy
和no_proxy
環境變量。在須要不少pip包的項目中,用pip一個個安裝包不是一個好辦法,這時候能夠考慮使用需求文件。shell
若是要生成需求文件,用下面的命令。這會將當前Python環境中的全部包的當前版本狀態保存下來,未來安裝的時候會精確還原到凍結的那個狀態。django
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 #
版本標識符用來指定包的版本,有如下幾個例子:markdown
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文件是受權文件,列出項目使用者應該遵循的各類條款。下圖是個人項目結構。
此外還可能存在幾個文件:
具體文件內容就不列出了。須要注意my_package/__init__.py
文件中應該有以下一行標識包名:
name = 'yitian_first_package'
用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 = 'lovery521@gmail.com' 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參數能夠列出一些相關項目的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參數用於指定數據文件,也就是在項目中使用到的非代碼文件,通常狀況下經過設置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參數指定一些入口點,能夠看作是項目提供的一些額外功能,其中最多見的就是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或者本地目錄中保存的包來替代官方索引中的包。詳情請查看文檔。
終於到了觀當作果的時候了,項目能夠被打包成各類類型的可分發包,這裏只介紹幾種最經常使用的。
這是最低等級的一種,基本上就是複製源代碼,不過所以在安裝的時候有一個必須的構建(可能包括編譯)過程來生成各類元信息,哪怕項目是純的Python項目。用下面的命令來生成:
python setup.py sdist
在編程界各類第三方包不是被形象地稱做輪子嗎(著名梗:不要重複造輪子),這裏就是這個意思。輪子是一種二進制分發包,是如今最推薦的分發包格式,輪子又能夠分爲好幾種輪子。固然,在構建輪子以前,還須要安裝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/*
稍等片刻,上傳應該就完成了。而後就能夠在測試索引中找到個人項目了。固然因爲測試索引會按期清理的緣故,可能過段時間項目和個人帳戶就都不存在了。
所有流程都熟悉以後,就能夠在官方索引上註冊帳號,並將項目上傳上去,這樣一來,全世界的開發者都能用到你的項目了!