本文由 walker 整理自《pytest 測試實戰·附錄D 打包和發佈 Python 項目》·Brian Okken 著·華中科技大學出版社
Windows 10 x64 Python 3.6.7 x64
項目的打包和發佈很重要。大部分 Python 開發者對這一塊並不熟悉,實際上,咱們須要嚴肅地看待這個問題。畢竟,共享代碼也是 Python 開發工做的一部分。所以,合理地使用 Python 內置的工具開共享代碼很重要。雖然這是一個很大的話題,但因爲篇幅的限制我沒法全面介紹。這裏只介紹常規的共享代碼的方法。掌握這些方法後,至少你沒必要再用電子郵件發送壓縮的文件和模塊了。
我將介紹如何設置項目讓它能夠經過 pip 安裝;如何以源代碼形式發佈項目;如何將項目打包成 wheel 文件。這些技巧足以讓你在小型團隊內部共享代碼。若是你還但願經過 PyPI 將代碼共享到互聯網上,請閱讀我推薦的文檔。如今讓咱們開始吧。html
Creating an Installable Module
先學習如何讓一個小項目能夠被 pip 安裝。咱們以一個只有單一模塊的項目爲例。實際項目一般不會這麼簡單,我選擇這個示例只是爲了展現如何建立一個可維護的項目,以及 setup.py 文件能夠多麼簡單。下面是一個簡單的目錄結構:python
some_module_proj |--setup.py |--some_module.py
咱們想要共享的代碼在 some_molule.py 文件裏:segmentfault
# D:\Python3Project\test\some_module_proj\some_module.py def some_func(): return 42
要使其能被 pip 安裝,咱們須要一個 setup.py 文件。下面是最簡潔的 setup.py 代碼:安全
# D:\Python3Project\test\some_module_proj\setup.py from setuptools import setup setup( name='some_module', py_modules=['some_module'] )
一個目錄、一個模塊、一個 setup.py 文件,就足以使項目可以被 pip 安裝了。bash
D:\Python3Project\test λ pip3 install .\some_module_proj\ Processing d:\python3project\test\some_module_proj Installing collected packages: some-module Running setup.py install for some-module ... done Successfully installed some-module-0.0.0
如今能夠在 Python 程序裏(或者從測試用例裏)使用 some_molule 了。ide
D:\Python3Project>python Python 3.6.7 (v3.6.7:6ec5cf24b7, Oct 20 2018, 13:35:33) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> from some_module import some_func >>> some_func() 42 >>> exit()
這是一種理想化的狀況。在實際工做中,更常見的狀況是將項目進行打包。下一節將介紹如何修改 setup.py 文件來完成項目打包。工具
Creating an Installable Package
先建立一個以包名名命的目錄,而後將 __init__.py 文件和相關模塊一同放入該目錄裏。學習
some_package_proj |--setup.py |--src |--some_package |--__init__.py |--some_module.py
some_module.py 文件的內容不變。__init__.py 文件須要把模塊的功能經過包命名空間暴露給外部。有不少方式能夠作到這一點,請閱讀 Python 文檔中關於該主題的兩節內容(https://docs.python.org/3/tutorial/modules.html#packages)。
若是在 __init__.py 文件裏這樣寫:測試
import some_package.some_module
那麼調用端的代碼必須指明 some_module。ui
import some_package some_package.some_module.some_func()
可是我認爲 some_module.py 是 API 功能的一部分,應該提供包這一層級的信息。因此,咱們應該這樣寫:
# D:\Python3Project\test\some_package_proj\src\some_package\__init__.py from some_package.some_module import * # or from .some_module import *
如今調用端代碼能夠這樣寫了:
import some_package some_package.some_func()
還須要對 setup.py 文件稍做修改。
# D:\Python3Project\test\some_package_proj\setup.py from setuptools import setup, find_packages setup( name='some_package', packages=find_packages(where='src'), package_dir={'': 'src'}, )
之後再調用就不須要提到 py_modules 了,只需指明包。如今它能夠被安裝了。
D:\Python3Project\test λ pip3 install .\some_package_proj Processing d:\python3project\test\some_package_proj Installing collected packages: some-package Running setup.py install for some-package ... done Successfully installed some-package-0.0.0
並且能夠直接使用。
D:\Python3Project\test\some_package_proj\src>python Python 3.6.7 (v3.6.7:6ec5cf24b7, Oct 20 2018, 13:35:33) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> from some_package import some_func >>> some_func() 42
項目能夠被安裝了,並且很容易進行模塊調用。你能夠在與 src 同級的目錄中添加 tests 目錄而且放入測試用例。但 setup.py 文件還缺乏重要內容,它還不能正常發佈源碼或建立 wheel 文件。不過,咱們只須要略做修改便可。
Creating a Source Distribution and Wheel
若是隻是我的使用,上一節介紹的方法足以建立源碼發佈包和 wheel 文件了。下面來試試。
pip3 install wheel
λ python3 setup.py sdist bdist_wheel running sdist running egg_info creating src\some_package.egg-info writing src\some_package.egg-info\PKG-INFO writing dependency_links to src\some_package.egg-info\dependency_links.txt writing top-level names to src\some_package.egg-info\top_level.txt writing manifest file 'src\some_package.egg-info\SOURCES.txt' reading manifest file 'src\some_package.egg-info\SOURCES.txt' writing manifest file 'src\some_package.egg-info\SOURCES.txt' warning: sdist: standard file not found: should have one of README, README.rst, README.txt, README.md ......
D:\Python3Project\test\some_package_proj λ dir /B dist some_package-0.0.0-py3-none-any.whl some_package-0.0.0.tar.gz
雖然出現了一些警告信息,但咱們仍是成功建立了 .whl 文件和 .tar.gz 文件。下面嘗試消除這些警告。
步驟以下。
同時還要添加如下信息。
README 文件告訴用戶如何使用你的包。url、做者信息告訴用戶遇到問題時與誰聯繫。軟件使用許可告訴用戶使用包的注意事項(分發、貢獻、複用代碼有哪些限制)。若是你不但願開源,應當在使用許可中寫清楚限制。若是開源,則推薦訪問 https://choosealicense.com/ 選擇合適的軟件許可。
添加這些信息用不了多長時間,下面是一個精簡的例子。
setup.py 文件:
# D:\Python3Project\test\some_package_proj\setup.py from setuptools import setup, find_packages setup( name='some_package', description='打包和分發示例', version='1.0', author='walker', author_email='walkerqt@foxmail.com', url='https://segmentfault.com/a/1190000021065589', packages=find_packages(where='src'), package_dir={'': 'src'}, )
許可條文應該放到 LICENSE 文件裏。(walker 抄的 MIT 許可)
# D:\Python3Project\test\some_package_proj\setup.py MIT License Copyright (c) 2014-present walker and other contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
README.md 文件內容以下:
D:\Python3Project\test\some_package_proj\README.md
# some_package: 打包和分發示例 `some_package` is the Python package to demostrate how easy it is to create installable, maintainable, shareable packages and distributions. \``` # 斜線爲本文轉義 >>> import some_package >>> some_package.some_func() 42 \``` That's it, really.
我習慣從現成的開源項目中複製 README 文件,刪掉那些我不須要的條文,而後根據本身項目的要求進行修改。
若是是一個嚴肅的項目,建議逐字輸入 README。我的項目複製粘貼修改便可。
再添加一個記錄項目變動的日誌文件。下面是一個例子:
D:\Python3Project\test\some_package_proj\CHANGELOG.md
# Changelog All notable changes to this project will be documented in this file. ## [1.0.0] - 2019-11-21 ### Added - Something be added. - Something be added. ### Changed - Something be changed. - Something be changed. ### Removed - Something be removed. ## [0.0.5] - 2019-11-15 ### Added - Something be added. ### Changed - Something be changed. ### Fixed - Something be fixed. ## [0.0.4] - 2019-11-14 ### Added - Something be added. ## [0.0.3] - 2019-11-13 ### Added - Something be added. ### Changed - Something be changed. ### Removed - Something be removed. ## [0.0.2] - 2019-11-12 ### Added - Something. ## [0.0.1] - 2019-11-11 ### Added - Initial version.
請訪問 https://keepachangelog.com ,閱讀編寫變動日誌的建議。
讓咱們看看這些改動是否足以消除警告。
D:\Python3Project\test\some_package_proj λ python3 setup.py sdist bdist_wheel running sdist running egg_info creating src\some_package.egg-info writing src\some_package.egg-info\PKG-INFO writing dependency_links to src\some_package.egg-info\dependency_links.txt writing top-level names to src\some_package.egg-info\top_level.txt writing manifest file 'src\some_package.egg-info\SOURCES.txt' reading manifest file 'src\some_package.egg-info\SOURCES.txt' writing manifest file 'src\some_package.egg-info\SOURCES.txt' running check creating some_package-1.0 creating some_package-1.0\src creating some_package-1.0\src\some_package creating some_package-1.0\src\some_package.egg-info copying files to some_package-1.0... copying README.rst -> some_package-1.0 copying setup.py -> some_package-1.0 copying src\some_package\__init__.py -> some_package-1.0\src\some_package copying src\some_package\some_module.py -> some_package-1.0\src\some_package copying src\some_package.egg-info\PKG-INFO -> some_package-1.0\src\some_package.egg-info copying src\some_package.egg-info\SOURCES.txt -> some_package-1.0\src\some_package.egg-info copying src\some_package.egg-info\dependency_links.txt -> some_package-1.0\src\some_package.egg-info copying src\some_package.egg-info\top_level.txt -> some_package-1.0\src\some_package.egg-info Writing some_package-1.0\setup.cfg creating dist Creating tar archive removing 'some_package-1.0' (and everything under it) running bdist_wheel running build running build_py creating build creating build\lib creating build\lib\some_package copying src\some_package\some_module.py -> build\lib\some_package copying src\some_package\__init__.py -> build\lib\some_package installing to build\bdist.win-amd64\wheel running install running install_lib creating build\bdist.win-amd64 creating build\bdist.win-amd64\wheel creating build\bdist.win-amd64\wheel\some_package copying build\lib\some_package\some_module.py -> build\bdist.win-amd64\wheel\.\some_package copying build\lib\some_package\__init__.py -> build\bdist.win-amd64\wheel\.\some_package running install_egg_info Copying src\some_package.egg-info to build\bdist.win-amd64\wheel\.\some_package-1.0-py3.6.egg-info running install_scripts adding license file "LICENSE" (matched pattern "LICEN[CS]E*") creating build\bdist.win-amd64\wheel\some_package-1.0.dist-info\WHEEL creating 'dist\some_package-1.0-py3-none-any.whl' and adding 'build\bdist.win-amd64\wheel' to it adding 'some_package/__init__.py' adding 'some_package/some_module.py' adding 'some_package-1.0.dist-info/LICENSE' adding 'some_package-1.0.dist-info/METADATA' adding 'some_package-1.0.dist-info/WHEEL' adding 'some_package-1.0.dist-info/top_level.txt' adding 'some_package-1.0.dist-info/RECORD' removing build\bdist.win-amd64\wheel
D:\Python3Project\test\some_package_proj λ dir /B dist some_package-0.0.0-py3-none-any.whl some_package-0.0.0.tar.gz
很好, 沒有警告了。
如今能夠把 .whl 文件和 .tar.gz 文件放到同一個目錄,而後運行 pip install。
D:\Python3Project\test\some_package_proj λ mkdir .\packages D:\Python3Project\test\some_package_proj λ copy .\dist\some_package-1.0-py3-none-any.whl .\packages\ 已複製 1 個文件。 D:\Python3Project\test\some_package_proj λ copy .\dist\some_package-1.0.tar.gz .\packages\ 已複製 1 個文件。
D:\Python3Project\test\some_package_proj λ pip3 install --no-index --find-links=.\packages\ some_package Looking in links: .\packages\ Processing d:\python3project\test\some_package_proj\packages\some_package-1.0-py3-none-any.whl Installing collected packages: some-package Successfully installed some-package-1.0 D:\Python3Project\test\some_package_proj λ pip3 install --no-index --find-links=.\packages\ some_package==1.0 Looking in links: .\packages\ Requirement already satisfied: some_package==1.0 in c:\program files\python36\lib\site-packages (1.0)
如今能夠爲本地項目建立本身的包,它安裝起來就像從 PyPI 安裝那樣容易。
Creating a PyPI-Installable Package
若是但願在 PyPI 上發佈本身的包,那麼須要在 setup.py 中添加更多的配置。同時還須要相似 Twine 這樣的工具把包推送到 PyPI。Twine 是一組工具,它可讓你更容易、更安全地與 PyPI 交互。它經過 HTTPS 認證保護 PyPI 密鑰信息,並負責將包上傳到 PyPI。
這些內容已經超出了本書的範圍,細節請查閱 Python Packaging User Guide(Python 程序以用戶分發指南)和 Python 文檔中關於 PyPI 的部分。