Python 庫打包分發、setup.py 編寫、混合 C 擴展打包的簡易指南(轉載)

轉載自:http://blog.konghy.cn/2018/04/29/setup-dot-py/html

Python 有很是豐富的第三方庫可使用,不少開發者會向 pypi 上提交本身的 Python 包。要想向 pypi 包倉庫提交本身開發的包,首先要將本身的代碼打包,才能上傳分發。python

1、distutils 簡介

distutils 是標準庫中負責創建 Python 第三方庫的安裝器,使用它可以進行 Python 模塊的安裝和發佈。distutils 對於簡單的分發頗有用,但功能缺乏。大部分Python用戶會使用更先進的setuptools模塊git

2、setuptools 簡介

setuptools 是 distutils 加強版,不包括在標準庫中。其擴展了不少功能,可以幫助開發者更好的建立和分發 Python 包。大部分 Python 用戶都會使用更先進的 setuptools 模塊。編程

Setuptools 有一個 fork 分支是 distribute。它們共享相同的命名空間,所以若是安裝了 distribute,import setuptools 時實際上將導入使用 distribute 建立的包。Distribute 已經合併回 setuptools。flask

還有一個大包分發工具是 distutils2,其試圖嘗試充分利用distutils,detuptools 和 distribute 併成爲 Python 標準庫中的標準工具。但該計劃並無達到預期的目的,且已是一個廢棄的項目。bash

所以,setuptools 是一個優秀的,可靠的 Pthon 包安裝與分發工具。如下設計到包的安裝與分發均針對 setuptools,並不保證 distutils 可用。ide

3、包格式

Python 庫打包的格式包括 Wheel 和 Egg。Egg 格式是由 setuptools 在 2004 年引入,而 Wheel 格式是由 PEP427 在 2012 年定義。使用 Wheel 和 Egg 安裝都不須要從新構建和編譯,其在發佈以前就應該完成測試和構建。svn

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

3.1 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'],                 # 包
)

3.2 參數概述

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

3.2.1 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 目錄中。

3.3 包含數據文件

  • 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

3.4 生成腳本

有兩個參數 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 參數表示自定製命令,後文詳述。

3.4.1 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

3.4.2 zip_safe

zip_safe 參數決定包是否做爲一個 zip 壓縮後的 egg 文件安裝,仍是做爲一個以 .egg 結尾的目錄安裝。由於有些工具不支持 zip 壓縮文件,並且壓縮後的包也不方便調試,因此建議將其設爲 False,即 zip_safe=False。

3.5 自定義命令

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,
    }
)

3.5.1 依賴關係

若是包依賴其餘的包,能夠指定 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",
]

3.6 分類信息

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',
]

4、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:用於包的上傳發布,後文詳述。

5、setup.cfg 文件

setup.cfg 文件用於提供 setup.py 的默認參數,詳細的書寫規則可參考:https://docs.python.org/3/distutils/configfile.html

6、版本命名

包版本的命名格式應爲以下形式:

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": 開發期間的發行版本號,可選

7、easy_install 與 pip

easy_insall 是 setuptool 包提供的第三方包安裝工具,而 pip 是 Python 中一個功能完備的包管理工具,是 easy_install 的改進版,提供更好的提示信息,刪除包等功能。

pip 相對於 easy_install 進行了如下幾個方面的改進:

  • 全部的包是在安裝以前就下載了,因此不可能出現只安裝了一部分的狀況
  • 在終端上的輸出更加友好
  • 對於動做的緣由進行持續的跟蹤。例如,若是一個包正在安裝,那麼 pip 就會跟蹤爲何這個包會被安裝
  • 錯誤信息會很是有用
  • 代碼簡潔精悍能夠很好的編程
  • 沒必要做爲 egg 存檔,能扁平化安裝(仍然保存 egg 元數據)
  • 原生的支持其餘版本控制系統(Git, Mercurial and Bazaar)
  • 加入卸載包功能
  • 能夠簡單的定義修改一系列的安裝依賴,還能夠可靠的賦值一系列依賴包

8、發佈包

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

9、庫包含 C 擴展的模塊

setup.py 文件示例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import subprocess

from setuptools import setup, Extension, find_packages
from setuptools.command.build_ext import build_ext


class CMakeExtension(Extension):
    def __init__(self, name, sourcedir=''):
        Extension.__init__(self, name, sources=[])
        self.sourcedir = os.path.abspath(sourcedir)


class CMakeBuild(build_ext):
    def run(self):
        for ext in self.extensions:
            self.build_extension(ext)

    def build_extension(self, ext):
        if not os.path.exists(self.build_temp):
            os.makedirs(self.build_temp)

        extdir = self.get_ext_fullpath(ext.name)
        if not os.path.exists(extdir):
            os.makedirs(extdir)

        # This is the temp directory where your build output should go
        install_prefix = os.path.abspath(os.path.dirname(extdir))
        cmake_args = '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={}'.format(install_prefix)

        subprocess.check_call(['cmake', ext.sourcedir, cmake_args], cwd=self.build_temp)
        subprocess.check_call(['cmake', '--build', '.'], cwd=self.build_temp)

setup(
    name='name',
    version='0.0.3',
    author='xxx',
    author_email='',
    description='',
    ext_modules=[CMakeExtension('.')],
    py_modules=['純py模塊的名稱'],
    cmdclass=dict(build_ext=CMakeBuild),
    zip_safe=False
)

publish.sh 示例:

echo start build
rm -rf dist/*
python setup.py sdist bdist_wheel
twine upload --repository-url http://hostname/repository/pypi-hosted/ dist/* -u username -p password

參考資料

相關文章
相關標籤/搜索