極簡 Python 打包指南

本文由 walker 整理自《pytest 測試實戰·附錄D 打包和發佈 Python 項目》·Brian Okken 著·華中科技大學出版社

walker 的實驗環境

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 文件。不過,咱們只須要略做修改便可。

建立源碼發佈包和 Wheel 文件

Creating a Source Distribution and Wheel

若是隻是我的使用,上一節介紹的方法足以建立源碼發佈包和 wheel 文件了。下面來試試。

  • 打包 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、README.rst、README.txt 或者 README.md 文件
  • 補充配置中的 url 數據
  • 補充做者和做者郵箱(或者維護者和維護者郵箱)信息

同時還要添加如下信息。

  • 版本號
  • 軟件使用許可
  • 變動記錄

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 安裝那樣容易。

建立能夠從 PyPI 安裝的包

Creating a PyPI-Installable Package

若是但願在 PyPI 上發佈本身的包,那麼須要在 setup.py 中添加更多的配置。同時還須要相似 Twine 這樣的工具把包推送到 PyPI。Twine 是一組工具,它可讓你更容易、更安全地與 PyPI 交互。它經過 HTTPS 認證保護 PyPI 密鑰信息,並負責將包上傳到 PyPI。
這些內容已經超出了本書的範圍,細節請查閱 Python Packaging User Guide(Python 程序以用戶分發指南)和 Python 文檔中關於 PyPI 的部分

相關文章
相關標籤/搜索